mirror of
https://github.com/kanaka/mal.git
synced 2024-09-11 13:55:55 +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:
parent
1ca3ee3dcd
commit
9678065e17
@ -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
|
Once you have a working implementation, you may want to implement
|
||||||
parts of the process inside the MAL language itself. This has no other
|
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
|
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
|
names, and the usual tests (with REGRESS=1) will check them. The
|
||||||
`runtest.py` script provide a convenient command-line parameter to
|
`runtest.py` script provide a convenient command-line parameter to
|
||||||
pass a command like 'load-file' before running the testsuite.
|
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
|
- Implement `nil?`, `true?`, `false?`, `empty?` and `sequential` with
|
||||||
submit new solutions, or new exercises.
|
another built-in function.
|
||||||
|
|
||||||
- Implement `nil?`, `true?`, `false?` and `sequential?` with other
|
|
||||||
built-in functions.
|
|
||||||
|
|
||||||
- Implement `>`, `<=` and `>=` with `<`.
|
- Implement `>`, `<=` and `>=` with `<`.
|
||||||
|
|
||||||
@ -26,6 +45,9 @@ submit new solutions, or new exercises.
|
|||||||
- Implement `count`, `nth`, `map`, `concat` and `conj` with the empty
|
- Implement `count`, `nth`, `map`, `concat` and `conj` with the empty
|
||||||
constructor `()`, `empty?`, `cons`, `first` and `rest`.
|
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.
|
Let `count` and `nth` benefit from tail call optimization.
|
||||||
|
|
||||||
Try to replace explicit recursions with calls to `reduce` and `foldr`.
|
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.
|
- Implement `let*` as a macro that uses `fn*` and recursion.
|
||||||
The same remark applies.
|
The same remark applies.
|
||||||
|
A macro is necessary because a function would attempt to evaluate
|
||||||
|
the first argument.
|
||||||
|
|
||||||
- Implement `apply`.
|
- Implement `apply`.
|
||||||
|
|
||||||
@ -60,3 +84,39 @@ submit new solutions, or new exercises.
|
|||||||
- Implement quoting within MAL.
|
- Implement quoting within MAL.
|
||||||
|
|
||||||
- Implement macros 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))`?
|
@ -5,12 +5,15 @@
|
|||||||
(def! nil? (fn* [x] (= x nil )))
|
(def! nil? (fn* [x] (= x nil )))
|
||||||
(def! true? (fn* [x] (= x true )))
|
(def! true? (fn* [x] (= x true )))
|
||||||
(def! false? (fn* [x] (= x false)))
|
(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] (< b a) ))
|
||||||
(def! <= (fn* [a b] (if (< b a) false true)))
|
(def! <= (fn* [a b] (not (< b a))))
|
||||||
(def! >= (fn* [a b] (if (< a b) false true)))
|
(def! >= (fn* [a b] (not (< a b))))
|
||||||
|
|
||||||
(def! hash-map (fn* [& xs] (apply assoc {} xs)))
|
(def! hash-map (fn* [& xs] (apply assoc {} xs)))
|
||||||
(def! list (fn* [& xs] xs))
|
(def! list (fn* [& xs] xs))
|
||||||
@ -22,13 +25,11 @@
|
|||||||
(fn* [xs] (if (nil? xs) 0 (reduce inc_left 0 xs)))))
|
(fn* [xs] (if (nil? xs) 0 (reduce inc_left 0 xs)))))
|
||||||
(def! nth
|
(def! nth
|
||||||
(fn* [xs index]
|
(fn* [xs index]
|
||||||
(if (empty? xs)
|
(if (or (empty? xs) (< index 0))
|
||||||
(throw "nth: index out of range")
|
(throw "nth: index out of range")
|
||||||
(if (< index 0)
|
(if (zero? index)
|
||||||
(throw "nth: index out of range")
|
(first xs)
|
||||||
(if (zero? index)
|
(nth (rest xs) (dec index))))))
|
||||||
(first xs)
|
|
||||||
(nth (rest xs) (dec index)))))))
|
|
||||||
(def! map
|
(def! map
|
||||||
(fn* [f xs]
|
(fn* [f xs]
|
||||||
(let* [iter (fn* [x acc] (cons (f x) acc))]
|
(let* [iter (fn* [x acc] (cons (f x) acc))]
|
||||||
@ -44,15 +45,21 @@
|
|||||||
(reduce flip_cons xs ys)))))
|
(reduce flip_cons xs ys)))))
|
||||||
|
|
||||||
(def! do2 (fn* [& xs] (nth xs (dec (count xs)))))
|
(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
|
(defmacro! let2
|
||||||
;; Must be a macro because the first argument must not be evaluated.
|
|
||||||
(fn* [binds form]
|
(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)
|
(if (empty? binds)
|
||||||
form
|
form
|
||||||
;; This let* increases the readability, but the values could
|
;; This let* increases the readability, but the values could
|
||||||
;; easily be replaced below.
|
;; easily be replaced below.
|
||||||
(let* [key (first 0)
|
(let* [key (first binds)
|
||||||
val (nth binds 1)
|
val (nth binds 1)
|
||||||
more (rest (rest binds))]
|
more (rest (rest binds))]
|
||||||
`((fn* [~key] (let2 ~more ~form)) ~val)))))
|
`((fn* [~key] (let2 ~more ~form)) ~val)))))
|
||||||
@ -68,3 +75,53 @@
|
|||||||
(map q x)
|
(map q x)
|
||||||
(cons (q x) acc)))]
|
(cons (q x) acc)))]
|
||||||
(fn* [& xs] (eval (foldr iter nil xs)))))
|
(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
|
||||||
|
Loading…
Reference in New Issue
Block a user