From 9678065e17957340e1ad45e61ebcf6f0e917371f Mon Sep 17 00:00:00 2001 From: Nicolas Boulenguez Date: Tue, 14 May 2019 19:31:00 +0200 Subject: [PATCH] Exercises: improve, add questions about folds. Rename the question file for consistency with answer file. Give an explicit command to copy/paste for tests. Warn about defining `nth` with `cond`. Fix typo in let2. Use tools from core.mal in answers. --- docs/{exercise.md => exercises.md} | 70 ++++++++++++++++++++++++-- examples/exercises.mal | 81 +++++++++++++++++++++++++----- 2 files changed, 134 insertions(+), 17 deletions(-) rename docs/{exercise.md => exercises.md} (54%) diff --git a/docs/exercise.md b/docs/exercises.md similarity index 54% rename from docs/exercise.md rename to docs/exercises.md index 3f49933f..dddad9c8 100644 --- a/docs/exercise.md +++ b/docs/exercises.md @@ -1,3 +1,22 @@ +# Exercises to learn MAL + +The process introduces LISP by describing the internals of selected +low-level constructs. As a complementary and more traditional +approach, you may want to solve the following exercises in the MAL +language itself, using any of the existing implementations. + +You are encouraged to use the shortcuts defined in the step files +(`not`...) and ``core.mal`` (`reduce`...) whenever you find that they +increase the readability. + +The difficulty is progressive in each section, but they focus on +related topics and it is recommended to start them in parallel. + +Some solutions are given in the `examples` directory. Feel free to +submit new solutions, or new exercises. + +## Replace parts of the process with native constructs + Once you have a working implementation, you may want to implement parts of the process inside the MAL language itself. This has no other purpose than learning the MAL language. Once it exists, a built-in @@ -11,12 +30,12 @@ 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. +``` +make REGRESS=1 TEST_OPTS='--hard --pre-eval=\(load-file\ \"../answer.mal\"\)' test^IMPL^stepA +``` -Some solutions are given in the `examples` directory. Feel free to -submit new solutions, or new exercises. - -- Implement `nil?`, `true?`, `false?` and `sequential?` with other - built-in functions. +- Implement `nil?`, `true?`, `false?`, `empty?` and `sequential` with + another built-in function. - Implement `>`, `<=` and `>=` with `<`. @@ -26,6 +45,9 @@ submit new solutions, or new exercises. - Implement `count`, `nth`, `map`, `concat` and `conj` with the empty constructor `()`, `empty?`, `cons`, `first` and `rest`. + You may use `or` to make the definition of `nth` a bit less ugly, + but avoid `cond` because its definition refers to `nth`. + Let `count` and `nth` benefit from tail call optimization. Try to replace explicit recursions with calls to `reduce` and `foldr`. @@ -36,6 +58,8 @@ submit new solutions, or new exercises. - Implement `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 `apply`. @@ -60,3 +84,39 @@ submit new solutions, or new exercises. - Implement quoting within MAL. - Implement macros within MAL. + +## More folds + +- Compute the sum of a sequence of numbers. +- Compute the product of a sequence of numbers. + +- Compute the logical conjunction ("and") and disjunction ("or") of a + sequence of MAL values interpreted as boolean values. For example, + `(conjunction [true 1 0 "" "a" nil true {}])` + should evaluate to `false` or `nil` because of the `nil` element. + + Why are folds not the best solution here, in terms of average + performances? + +- Does "-2-3-4" translate to `(reduce - 0 [2 3 4])`? + +- Suggest better solutions for + `(reduce str "" xs)` and + `(reduce concat [] xs)`. + +- What does `(reduce (fn* [acc _] acc) xs)` nil answer? + +- The answer is `(fn* [xs] (reduce (fn* [_ x] x) nil xs))`. + What was the question? + +- What is the intent of + `(reduce (fn* [acc x] (if (< acc x) x acc)) 0 xs)`? + + Why is it the wrong answer? + +- Though `(sum (map count xs))` or `(count (apply concat xs))` can be + considered more readable, implement the same effect with a single loop. +- Compute the maximal length in a list of lists. + +- How would you name + `(fn* [& fs] (foldr (fn* [f acc] (fn* [x] (f (acc x)))) identity fs))`? diff --git a/examples/exercises.mal b/examples/exercises.mal index 2ba330f5..be51eb6d 100644 --- a/examples/exercises.mal +++ b/examples/exercises.mal @@ -5,12 +5,15 @@ (def! nil? (fn* [x] (= x nil ))) (def! true? (fn* [x] (= x true ))) (def! false? (fn* [x] (= x false))) +(def! empty? (fn* [x] (= x [] ))) -(def! sequential? (fn* [x] (if (list? x) true (if (vector? x) true false)))) +(def! sequential? + (fn* [x] + (or (list? x) (vector? x)))) -(def! > (fn* [a b] (< b a) )) -(def! <= (fn* [a b] (if (< b a) false true))) -(def! >= (fn* [a b] (if (< a b) false true))) +(def! > (fn* [a b] (< b a) )) +(def! <= (fn* [a b] (not (< b a)))) +(def! >= (fn* [a b] (not (< a b)))) (def! hash-map (fn* [& xs] (apply assoc {} xs))) (def! list (fn* [& xs] xs)) @@ -22,13 +25,11 @@ (fn* [xs] (if (nil? xs) 0 (reduce inc_left 0 xs))))) (def! nth (fn* [xs index] - (if (empty? xs) + (if (or (empty? xs) (< index 0)) (throw "nth: index out of range") - (if (< index 0) - (throw "nth: index out of range") - (if (zero? index) - (first xs) - (nth (rest xs) (dec index))))))) + (if (zero? index) + (first xs) + (nth (rest xs) (dec index)))))) (def! map (fn* [f xs] (let* [iter (fn* [x acc] (cons (f x) acc))] @@ -44,15 +45,21 @@ (reduce flip_cons xs ys))))) (def! do2 (fn* [& xs] (nth xs (dec (count xs))))) +(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 - ;; Must be a macro because the first argument must not be evaluated. (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 0) + (let* [key (first binds) val (nth binds 1) more (rest (rest binds))] `((fn* [~key] (let2 ~more ~form)) ~val))))) @@ -68,3 +75,53 @@ (map q x) (cons (q x) acc)))] (fn* [& xs] (eval (foldr iter nil xs))))) + +(def! sum (fn* [xs] (reduce + 0 xs))) +(def! product (fn* [xs] (reduce * 1 xs))) + +(def! conjunction + (let* [and2 (fn* [acc x] (if acc x false))] + (fn* [xs] + (reduce and2 true xs)))) +(def! disjunction + (let* [or2 (fn* [acc x] (if acc true x))] + (fn* [xs] + (reduce or2 false xs)))) +;; It would be faster to stop the iteration on first failure +;; (conjunction) or success (disjunction). Even better, `or` in the +;; stepA and `and` in `core.mal` stop evaluating their arguments. + +;; Yes, -2-3-4 means (((0-2)-3)-4). + +;; `(reduce str "" xs)` is equivalent to `apply str xs` +;; and `(reduce concat () xs)` is equivalent to `apply concat xs`. +;; The built-in iterations are probably faster. + +;; `(reduce (fn* [acc _] acc) nil xs)` is equivalent to `nil`. + +;; For (reduce (fn* [acc x] x) nil xs))), see do3 above. + +;; `(reduce (fn* [acc x] (if (< acc x) x acc)) 0 xs)` computes the +;; maximum of a list of non-negative integers. It is hard to find an +;; initial value fitting all purposes. + +(def! sum_len + (let* [add_len (fn* [acc x] (+ acc (count x)))] + (fn* [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] + (reduce update_max 0 xs)))) + +(def! compose + (let* [compose2 (fn* [f acc] (fn* [x] (f (acc x))))] + (fn* [& fs] + (foldr compose2 identity fs)))) +;; ((compose f1 f2) x) is equivalent to (f1 (f2 x)) +;; This is the mathematical composition. For practical purposes, `->` +;; and `->>` defined in `core.mal` are more efficient and general. + +;; This `nil` is intentional so that the result of doing `load-file` is +;; `nil` instead of whatever happens to be the last definiton. +nil