Skip to content

Instantly share code, notes, and snippets.

@scgilardi
Last active February 15, 2020 14:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scgilardi/7128626669531ae0867194f047a4e523 to your computer and use it in GitHub Desktop.
Save scgilardi/7128626669531ae0867194f047a4e523 to your computer and use it in GitHub Desktop.
compares lazy vs. eager, into+transducer vs. doall
(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]))
@scgilardi
Copy link
Author

one could also "hide the awkward" in the tradition of keechma.toolbox.ui:

(defn comps>
  [data->comp data-coll]
  (seq (into [] (map data->comp) data-coll)))

(defn render
  [ctx]
  (let [route-status (keyword (route> ctx :status))
        is-editing? (λ [id] (= id (:id (sub> ctx :edit-todo))))
        todo-item (λ [{id :id :as todo}]
                    ^{:key id} [comp> ctx :todo-item todo (is-editing? id)])]
    [:ul.todo-list
     (comps> todo-item (sub> ctx :todos-by-status route-status))]))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment