Last active
February 15, 2020 14:24
-
-
Save scgilardi/7128626669531ae0867194f047a4e523 to your computer and use it in GitHub Desktop.
compares lazy vs. eager, into+transducer vs. doall
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns keechma-todomvc.components.todo-list | |
"# Todo List component" | |
(:require [keechma-todomvc.ui :refer [<comp comp> route> sub>]])) | |
(defn render | |
"## Renders a list of currently visible todos | |
`todo` visiblity is controlled by the current `route`. | |
### Component Deps | |
- `:todo-item` Each list item is rendered by a `:todo-item` component | |
that receives the `todo` and the calculated `is-editing?` value as | |
arguments. | |
### Subscription Deps | |
- `:todos-by-status` returns `todos` with a `status` | |
- `:edit-todo` returns the `todo` currently being edited, or nil" | |
[ctx] | |
(let [route-status (keyword (route> ctx :status)) | |
is-editing? (fn [id] (= id (:id (sub> ctx :edit-todo)))) | |
todo-item (fn [{id :id :as todo}] | |
^{:key id} [comp> ctx :todo-item todo (is-editing? id)])] | |
[:ul.todo-list | |
;; The 4 expressions below all produce an equivalent result: a | |
;; seq of components that reagent will interpret correctly as a | |
;; collection of items to be expanded inline (rather than as an | |
;; in-line component "call", the special meaning of a vector in a | |
;; reagent component body). | |
;; --- | |
;; 1a. and 1b. invoke less machinery internally. They are a more | |
;; direct path from todos to realized items involving fewer | |
;; function calls and less memory. Each todo is retrieved, | |
;; transformed, and the result is conj'd onto a vector. 1a. and | |
;; 1b. should be identical in performance and memory use. | |
;; 1a. eager, inline transducer | |
(seq (into [] (map todo-item) (sub> ctx :todos-by-status route-status))) | |
;; 1b. eager, transducer separated out for clarity | |
(let [todo->item-transducer (map todo-item)] | |
(seq (into [] todo->item-transducer (sub> ctx :todos-by-status route-status)))) | |
;; --- | |
;; --- | |
;; 2a. and 2b. start out by creating a lazy seq and then convert | |
;; it to a realized seq by walking the lazy seq. The walk causes | |
;; each link in the linked list of lazy seq items to call a | |
;; function and cache the result. 2a. and 2b. should also be very | |
;; similar in performance and memory use. 2a. does do some more | |
;; work and has no benefits over 2b. | |
;; 2a. lazy, realized using into + seq | |
(seq (into [] (map todo-item (sub> ctx :todos-by-status route-status)))) | |
;; 2b. lazy, realized using doall | |
(doall (map todo-item (sub> ctx :todos-by-status route-status))) | |
;; -- | |
;; To get an idea of the difference between 1. eager and 2. lazy | |
;; regarding how much code is invoked interally, I compared some | |
;; similar expressions on Clojure on the JVM using Criterium. I | |
;; found that with a trivial transformation, the eager method | |
;; took less than half the time to execute. Of course both methods | |
;; are very fast and the time difference would be noticeable | |
;; only with >> 100,000 elements so there is no practical | |
;; performance difference in UI code. | |
;; My undertanding is that one reason transducers were introduced | |
;; to Clojure is to make the efficiency and deterministic | |
;; execution benefits of using `reduce` easier to express in | |
;; code. The new arities of transformation functions like `map` | |
;; and `filter` and the new capability of `into` to accept a | |
;; transducer accomplish that. | |
;; I think as a practical matter it comes down to a choice on | |
;; style and programmer convenience. On that point I think the | |
;; explicit "seq" call in 1a. and 1b. is awkward and the subtle | |
;; distinction in code between 1a. and 2a. makes it easy to | |
;; confuse them. | |
;; Overall I'm coming to like 2b. best for its simplicity in | |
;; expression and familiarity. In the role of the code as a | |
;; communication medium between developers, it's probably the | |
;; best of the 4. One decent argument against it is that one | |
;; might forget the `doall` in some cases and see hard to | |
;; diagnose strange behavior. 1a. is better for that as it | |
;; removes any possible ambiguity about when `todo-item` is | |
;; called. | |
;; There are some Clojure functions that I regard as convenient, | |
;; but when I see them in production code, I think "there's | |
;; probably a better way to do this, consider reworking the | |
;; code". Among those are `doall`, `dorun`, and `flatten`. | |
])) | |
(def component | |
(<comp :renderer render | |
:component-deps [:todo-item] | |
:subscription-deps [:todos-by-status | |
:edit-todo])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
one could also "hide the awkward" in the tradition of keechma.toolbox.ui: