diff --git a/README.md b/README.md index 80c381c4..3769a06c 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,24 @@ extra performance and small bundle size. - `println` is a synonym for `console.log` - `pr-str` and `prn` coerce values to a string using `JSON.stringify` +### seqs + +Clava does not implement Clojure seqs. Instead it uses the JavaScript +[iteration protocols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) +to work with collections. What this means in practice is the following: + +- `seq` takes a collection and returns an Iterable of that collection, or nil if it's empty +- `iterable` takes a collection and returns an Iterable of that collection, even if it's empty +- `seqable?` can be used to check if you can call either one + +Most collections are iterable already, so `seq` and `iterable` will simply +return them; an exception are objects created via `{:a 1}`, where `seq` and +`iterable` will return the result of `Object.entries`. + +`first`, `rest`, `map`, `reduce` et al. call `iterable` on the collection before +processing, and functions that typically return seqs instead return an array of +the results. + ## Open questions - TC39 records and tuples are immutable but not widely supported. It's not yet sure how they will fit within clava. diff --git a/core.js b/core.js index 390ec992..e4a2d101 100644 --- a/core.js +++ b/core.js @@ -1,8 +1,6 @@ export function assoc_BANG_(m, k, v, ...kvs) { if (kvs.length % 2 !== 0) { - throw new Error( - "Illegal argument: assoc expects an odd number of arguments." - ); + throw new Error('Illegal argument: assoc expects an odd number of arguments.'); } if (m instanceof Map) { @@ -19,7 +17,7 @@ export function assoc_BANG_(m, k, v, ...kvs) { } } else { throw new Error( - "Illegal argument: assoc! expects a Map, Array, or Object as the first argument." + 'Illegal argument: assoc! expects a Map, Array, or Object as the first argument.' ); } @@ -29,7 +27,7 @@ export function assoc_BANG_(m, k, v, ...kvs) { export function assoc(o, k, v, ...kvs) { if (!(o instanceof Object)) { throw new Error( - "Illegal argument: assoc expects a Map, Array, or Object as the first argument." + 'Illegal argument: assoc expects a Map, Array, or Object as the first argument.' ); } @@ -100,11 +98,11 @@ function assoc_in_with(f, fname, o, keys, value) { } export function assoc_in(o, keys, value) { - return assoc_in_with(assoc, "assoc-in", o, keys, value); + return assoc_in_with(assoc, 'assoc-in', o, keys, value); } export function assoc_in_BANG_(o, keys, value) { - return assoc_in_with(assoc_BANG_, "assoc-in!", o, keys, value); + return assoc_in_with(assoc_BANG_, 'assoc-in!', o, keys, value); } export function conj_BANG_(...xs) { @@ -130,7 +128,7 @@ export function conj_BANG_(...xs) { } } else { throw new Error( - "Illegal argument: conj! expects a Set, Array, Map, or Object as the first argument." + 'Illegal argument: conj! expects a Set, Array, Map, or Object as the first argument.' ); } @@ -167,7 +165,7 @@ export function conj(...xs) { } throw new Error( - "Illegal argument: conj expects a Set, Array, Map, or Object as the first argument." + 'Illegal argument: conj expects a Set, Array, Map, or Object as the first argument.' ); } @@ -217,31 +215,44 @@ export function get(coll, key, otherwise = undefined) { return key in coll ? coll[key] : otherwise; } -export function iterable_QMARK_(x) { +export function seqable_QMARK_(x) { // String is iterable but doesn't allow `m in s` - return x instanceof String || Symbol.iterator in x; + return typeof x === 'string' || x === null || x === undefined || Symbol.iterator in x; } export function iterable(x) { - if (iterable_QMARK_(x)) { + // nil puns to empty iterable, support passing nil to first/rest/reduce, etc. + if (x === null || x === undefined) { + return []; + } + if (seqable_QMARK_(x)) { return x; } return Object.entries(x); } +export function seq(x) { + let iter = iterable(x); + // return nil for terminal checking + if (iter.length === 0 || iter.size === 0) { + return null; + } + return iter; +} + export function first(coll) { // destructuring uses iterable protocol - let [first] = coll; + let [first] = iterable(coll); return first; } export function second(coll) { - let [_, v] = coll; + let [_, v] = iterable(coll); return v; } export function rest(coll) { - let [_, ...rest] = coll; + let [_, ...rest] = iterable(coll); return rest; } @@ -293,7 +304,7 @@ export function map(f, coll) { } export function str(...xs) { - return xs.join(""); + return xs.join(''); } export function not(expr) { @@ -307,13 +318,11 @@ export function nil_QMARK_(v) { export const PROTOCOL_SENTINEL = {}; function pr_str_1(x) { - return JSON.stringify(x, (_key, value) => - value instanceof Set ? [...value] : value - ); + return JSON.stringify(x, (_key, value) => (value instanceof Set ? [...value] : value)); } export function pr_str(...xs) { - return xs.map(pr_str_1).join(" "); + return xs.map(pr_str_1).join(' '); } export function prn(...xs) { diff --git a/resources/clava/core.edn b/resources/clava/core.edn index dde6dd00..fb4a6c93 100644 --- a/resources/clava/core.edn +++ b/resources/clava/core.edn @@ -1 +1 @@ -#{reduce first dissoc atom dec map assoc_in reset_BANG_ rest PROTOCOL_SENTINEL reduced_QMARK_ range disj iterable conj get disj_BANG_ vec second prn assoc_BANG_ println Atom nth nil_QMARK_ assoc_in_BANG_ iterable_QMARK_ dissoc_BANG_ not pr_str swap_BANG_ vector mapv subvec reduced inc str apply map_indexed deref assoc conj_BANG_ re_matches} \ No newline at end of file +#{seq reduce first dissoc atom dec map assoc_in reset_BANG_ rest PROTOCOL_SENTINEL reduced_QMARK_ range disj seqable_QMARK_ iterable conj get disj_BANG_ vec second prn assoc_BANG_ println Atom nth nil_QMARK_ assoc_in_BANG_ dissoc_BANG_ not pr_str swap_BANG_ vector mapv subvec reduced inc str apply map_indexed deref assoc conj_BANG_ re_matches} \ No newline at end of file diff --git a/test/clava/compiler_test.cljs b/test/clava/compiler_test.cljs index a51d38e8..d1a623e1 100644 --- a/test/clava/compiler_test.cljs +++ b/test/clava/compiler_test.cljs @@ -528,14 +528,26 @@ (is (eq 3 (jsv! '(get {"my-key" 1} "bad-key" 3)))))) (deftest first-test + (is (= nil (jsv! '(first nil)))) + (is (= nil (jsv! '(first [])))) + (is (= nil (jsv! '(first #{})))) + (is (= nil (jsv! '(first {})))) + (is (= nil (jsv! '(first (js/Map. []))))) (is (= 1 (jsv! '(first [1 2 3])))) (is (= 1 (jsv! '(first #{1 2 3})))) - (is (eq #js [1 2] (jsv! '(first (js/Map. [[1 2] [3 4]])))))) + (is (eq #js [1 2] (jsv! '(first (js/Map. [[1 2] [3 4]]))))) + (is (eq "a" (jsv! '(first "abc"))))) (deftest rest-test + (is (eq () (jsv! '(rest nil)))) + (is (eq () (jsv! '(rest [])))) + (is (eq () (jsv! '(rest #{})))) + (is (eq () (jsv! '(rest {})))) + (is (eq () (jsv! '(rest (js/Map. []))))) (is (eq #js [2 3] (jsv! '(rest [1 2 3])))) (is (eq #{2 3} (jsv! '(rest #{1 2 3})))) - (is (eq #js [#js [3 4]] (jsv! '(rest (js/Map. [[1 2] [3 4]])))))) + (is (eq #js [#js [3 4]] (jsv! '(rest (js/Map. [[1 2] [3 4]]))))) + (is (eq '("b" "c") (jsv! '(rest "abc"))))) (deftest reduce-test (testing "no val" @@ -595,5 +607,31 @@ (is (jsv! '(reduced? (reduced 5)))) (is (= 4 (jsv! '(deref (reduced 4)))))) +(deftest seq-test + (is (= "abc" (jsv! '(seq "abc")))) + (is (eq '(1 2 3) (jsv! '(seq [1 2 3])))) + (is (eq '([:a 1] [:b 2]) (jsv! '(seq {:a 1 :b 2})))) + (is (eq (js/Set. [1 2 3]) + (jsv! '(seq #{1 2 3})))) + (is (eq (js/Map. #js[#js[1 2] #js[3 4]]) + (jsv! '(seq (js/Map. [[1 2] [3 4]]))))) + (testing "empty" + (is (= nil (jsv! '(seq nil)))) + (is (= nil (jsv! '(seq [])))) + (is (= nil (jsv! '(seq {})))) + (is (= nil (jsv! '(seq #{})))) + (is (= nil (jsv! '(seq (js/Map.)))))) + (is (eq #js [0 2 4 6 8] + (jsv! '(loop [evens [] + nums (range 10)] + (if-some [x (first nums)] + (recur (if (case x + (0 2 4 6 8 10) true + false) + (conj evens x) + evens) + (rest nums)) + evens)))))) + (defn init [] (cljs.test/run-tests 'clava.compiler-test))