1
1
mirror of https://github.com/kanaka/mal.git synced 2024-08-16 09:10:48 +03:00

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.
This commit is contained in:
Nicolas Boulenguez 2019-05-14 19:31:00 +02:00
parent 1ca3ee3dcd
commit 9678065e17
2 changed files with 134 additions and 17 deletions

View File

@ -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))`?

View File

@ -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