exercises: rebase, improve quasiquote and let*, avoid circular dependencies.
authorNicolas Boulenguez <nicolas.boulenguez@free.fr>
Thu, 30 May 2019 15:48:53 +0000 (17:48 +0200)
committerNicolas Boulenguez <nicolas.boulenguez@free.fr>
Thu, 30 May 2019 17:00:45 +0000 (19:00 +0200)
* Avoid including `lib/*.mal` to prevent unexpected circular
  dependencies (bitten by `foldr` using `nth`).
* Ask `quote` before `let*`, the latter requires the former.
  Answer previous questions without `let*`.
* Tell the reader that `let*` has various levels of difficulty.
* Drop implicit dependency on `or`, soon out of step files.
* Allow simple recursion in `let*` via a combinator.

docs/exercises.md
examples/exercises.mal
lib/reducers.mal

index 6fc3963..2f5cb2c 100644 (file)
@@ -27,9 +27,7 @@ implementations does not matter much.
 
 You may easily check your answers by passing them directly to the
 interpreter. They will hide the built-in functions carrying the same
-names, and the usual tests (with REGRESS=1) will check them. The
-`runtest.py` script provide a convenient command-line parameter to
-pass a command like 'load-file' before running the testsuite.
+names, and the usual tests will check them.
 ```
 make REGRESS=1 TEST_OPTS='--hard --pre-eval=\(load-file\ \"../answer.mal\"\)' test^IMPL^stepA
 ```
@@ -60,13 +58,17 @@ make REGRESS=1 TEST_OPTS='--hard --pre-eval=\(load-file\ \"../answer.mal\"\)' te
   form will hide your implementation, so in order to test it, you will
   need to give it another name and adapt the test accordingly.
 
-- Implement `let*` as a macro that uses `fn*` and recursion.
+- Implement quoting with macros.
+  The same remark applies.
+
+- Implement most of `let*` as a macro that uses `fn*` and recursion.
   The same remark applies.
   A macro is necessary because a function would attempt to evaluate
   the first argument.
 
-- Implement quoting with macros.
-  The same remark applies.
+  Once your answer passes most tests and you understand which part is
+  tricky, you should search for black magic recipes on the web. Few of
+  us mortals are known to have invented a full solution on their own.
 
 - Implement `apply`.
 
index 4185692..42f1e64 100644 (file)
@@ -1,7 +1,14 @@
 ;; These are the answers to the questions in ../docs/exercise.md.
 
-(load-file "../lib/folds.mal")          ; foldr reduce
-(load-file "../lib/trivial.mal")        ; dec identity
+;; In order to avoid unexpected circular dependencies among solutions,
+;; this files attempts to be self-contained.
+(def! identity (fn* [x] x))
+(def! reduce (fn* (f init xs)
+  (if (empty? xs) init (reduce f (f init (first xs)) (rest xs)))))
+(def! foldr (fn* [f init xs]
+  (if (empty? xs) init (f (first xs) (foldr f init (rest xs))))))
+
+;; Reimplementations.
 
 (def! nil?   (fn* [x] (= x nil  )))
 (def! true?  (fn* [x] (= x true )))
@@ -10,7 +17,7 @@
 
 (def! sequential?
   (fn* [x]
-    (or (list? x) (vector? x))))
+    (if (list? x) true (vector? x))))
 
 (def! >  (fn* [a b]      (< b a) ))
 (def! <= (fn* [a b] (not (< b a))))
 (def! swap! (fn* [a f & xs] (reset! a (apply f (deref a) xs))))
 
 (def! count
-  (let* [inc_left (fn* [acc _] (inc acc))]
-    (fn* [xs] (if (nil? xs) 0 (reduce inc_left 0 xs)))))
-;; (def! nth
-;;   (fn* [xs index]
-;;     (if (or (empty? xs) (< index 0))
-;;       (throw "nth: index out of range")
-;;       (if (zero? index)
-;;         (first xs)
-;;         (nth (rest xs) (dec index))))))
+  (fn* [xs]
+    (if (nil? xs)
+      0
+      (reduce (fn* [acc _] (+ 1 acc)) 0 xs))))
+(def! nth
+  (fn* [xs index]
+    (if (if (<= 0 index) (not (empty? xs))) ; logical and
+      (if (= 0 index)
+        (first xs)
+        (nth (rest xs) (- index 1)))
+      (throw "nth: index out of range"))))
 (def! map
   (fn* [f xs]
-    (let* [iter (fn* [x acc] (cons (f x) acc))]
-      (foldr iter () xs))))
+    (foldr (fn* [x acc] (cons (f x) acc)) () xs)))
 (def! concat
-  (let* [concat2 (fn* [xs ys] (foldr cons ys xs))]
-    (fn* [& xs] (foldr concat2 () xs))))
+   (fn* [& xs]
+    (foldr (fn* [xs ys] (foldr cons ys xs)) () xs)))
 (def! conj
-  (let* [flip_cons (fn* [xs x] (cons x xs))]
-    (fn* [xs & ys]
-      (if (vector? xs)
-        (apply vector (concat xs ys))
-        (reduce flip_cons xs ys)))))
+  (fn* [xs & ys]
+    (if (vector? xs)
+      (apply vector (concat xs ys))
+      (reduce (fn* [xs x] (cons x xs)) xs ys))))
 
-(def! do2 (fn* [& xs] (nth xs (dec (count xs)))))
+(def! do2 (fn* [& xs] (nth xs (- (count xs) 1))))
 (def! do3 (fn* [& xs] (reduce (fn* [acc x] x) nil xs)))
 ;; do2 will probably be more efficient when lists are implemented as
 ;; arrays with direct indexing, but when they are implemented as
 ;; linked lists, do3 may win because it only does one traversal.
 
-(defmacro! let2
+(defmacro! quote (fn* [ast] (list (fn* [] ast))))
+(def! _quasiquote_iter (fn* [x acc]
+  (if (if (list? x) (= (first x) 'splice-unquote)) ; logical and
+    (list 'concat (first (rest x)) acc)
+    (list 'cons (list 'quasiquote x) acc))))
+(defmacro! quasiquote (fn* [ast]
+  (if (list? ast)
+    (if (= (first ast) 'unquote)
+      (first (rest ast))
+      (foldr _quasiquote_iter () ast))
+    (if (vector? ast)
+      ;; TODO: once tests are fixed, replace 'list with 'vector.
+      (list 'apply 'list (foldr _quasiquote_iter () ast))
+      (list 'quote ast)))))
+
+;; FIXME: mutual recursion.
+;; http://okmij.org/ftp/Computation/fixed-point-combinators.html
+(def! _c_combinator (fn* [x] (x x)))
+(def! _d_combinator (fn* [f] (fn* [x] (f (fn* [v] ((x x) v))))))
+(defmacro! let*
   (fn* [binds form]
-    ;;  Each expression may refer to previous definitions, so a single
-    ;;  function with many parameters would not have the same effect
-    ;;  than a composition of functions with one parameter each.
     (if (empty? binds)
       form
-      ;;  This let* increases the readability, but the values could
-      ;;  easily be replaced below.
-      (let* [key  (first binds)
-             val  (nth binds 1)
-             more (rest (rest binds))]
-        `((fn* [~key] (let2 ~more ~form)) ~val)))))
-
-(defmacro! quote2 (fn* [ast] (list (fn* [] ast))))
-(defmacro! quasiquote2
-  (fn* [ast]
-    (let* [
-      is-pair (fn* [x] (if (sequential? x) (not (empty? x))))
-      f (fn* [ast]                      ; evaluating its arguments
-        (if (is-pair ast)
-          (let* [a0 (first ast)]
-            (if (= 'unquote a0)
-              (nth ast 1)
-              (if (if (is-pair a0) (= 'splice-unquote (first a0)))
-                (list 'concat (nth a0 1) (f (rest ast)))
-                (list 'cons (f a0) (f (rest ast))))))
-          (list 'quote ast)))
-      ]
-      (f ast))))
+      (list (list 'fn* [(first binds)] (list 'let* (rest (rest binds)) form))
+            (list '_c_combinator
+                  (list '_d_combinator
+                       (list 'fn* [(first binds)] (first (rest binds)))))))))
 
 (def! apply
   ;; Replace (f a b [c d]) with ('f 'a 'b 'c 'd) then evaluate the
@@ -94,6 +98,8 @@
                   (cons (q x) acc)))]
     (fn* [& xs] (eval (foldr iter nil xs)))))
 
+;; Folds
+
 (def! sum     (fn* [xs] (reduce + 0 xs)))
 (def! product (fn* [xs] (reduce * 1 xs)))
 
 (def! sum_len
   (let* [add_len (fn* [acc x] (+ acc (count x)))]
     (fn* [xs]
-      (reduce add_len  0 xs))))
+      (reduce add_len 0 xs))))
 (def! max_len
   (let* [update_max (fn* [acc x] (let* [l (count x)] (if (< acc l) l acc)))]
     (fn* [xs]
index 76a3cac..943a325 100644 (file)
@@ -12,7 +12,8 @@
       (reduce f (f init (first xs)) (rest xs)))))
 
 ;; Right fold (f x1 (f x2 (.. (f xn init)) ..))
-;; The natural implementation for `foldr` is not tail-recursive, so we
+;; The natural implementation for `foldr` is not tail-recursive, and
+;; the one based on `reduce` constructs many intermediate functions, so we
 ;; rely on efficient `nth` and `count`.
 (def! foldr