Fork me on GitHub
#hoplon
<
2017-07-02
>
chromalchemy01:07:14

I am trying out cljs 1.9.671 and getting this warning for:

(let [mycell (cell :a)]
    (elem 
      (case-tpl mycell
        :a "hello"
        :b "world"
        "default")))
WARNING: Wrong number of args (0) passed to cljs.core/atom

thedavidmeister02:07:35

@flyboarder i'm only up to 1.9.521

thedavidmeister02:07:53

waiting for the spec.alpha stuff to die down a bit

thedavidmeister02:07:49

i've had a lot of problems in general trying to stay on the latest cljs version so i only update occasionally

chromalchemy02:07:56

Ok, I wanted to try up to 1.9.660 for faster build times. 1.9.660 seems to cut it by 15 seconds!

chromalchemy02:07:14

1.9.660 Doesn't give that warning on the case-tpl. It does give some warnins about hoplon.core and ui.attrs.

chromalchemy04:07:58

Does anyone have insight into why the last 2 elements don't update?

(let
    [ data            (cell [1 2 3])
      query-fn        (fn [] (last @data))
      reactive-query  (cell= (query-fn))
      update-data     (fn [] (swap! data #(conj % 7)))]
    [
      (elem :click #(update-data) "Update Data")
      (elem reactive-query)
      (elem (cell= (query-fn)))])
If I pass the cell explicitly in the formula it updates. But if the cell is buried in the called (query) function, it doesn't.

flyboarder05:07:20

@chromalchemy you want to refactor and not have children in cells

flyboarder05:07:22

Really you want their state in cells and use template macros to expand that state on an element

thedavidmeister07:07:54

@chromalchemy the cell= macro can only see the symbols inside it, it's not recursively looking through all the called functions and trying to figure out whether the data in their scope is the same as data in the macro's scope

thedavidmeister07:07:39

if you want to have a function trigger when data changes, use do-watch or just a regular add-watch

thedavidmeister07:07:10

just make your query-fn take data as an argument

thedavidmeister07:07:07

hoplon works a lot better if you try to write everything so that you pass in arguments rather than try to couple things to the scope you happen to be writing them in

thedavidmeister07:07:14

(let
    [ data            (cell [1 2 3])
      query-fn        (fn [data] (last data))
      reactive-query  (cell= (query-fn data))
      update-data     (fn [] (swap! data #(conj % 7)))]
    [
      (elem :click #(update-data) "Update Data")
      (elem reactive-query)
      (elem (cell= (query-fn data)))])

chromalchemy10:07:16

Thanks guys! @thedavidmeister I simply passed the data as an argument, as you said, to fix the bug. I'll look at do-watch and add-watch too. It was a subtle bug that took me a long time to identify because everything would render properly on page load and reload, just no reactive update 😣

thedavidmeister10:07:16

@chromalchemy my personal rule is to try and never rely on state/data outside the scope of the function i'm working on

thedavidmeister10:07:49

as well as avoiding bugs like this, it means that a huge amount of my work is portable to other projects too

dm310:07:57

cell= is a macro, it can only see the cells in the immediate expansion of the code within itself

dm310:07:01

I agree with the David (meister) here πŸ™‚

chromalchemy10:07:13

Good point. I'll take that to heart. Isn't that one of the proposed benefits of Javelin though, referencing state in a more flat way?

thedavidmeister10:07:36

not sure what you mean by "more flat way"

dm310:07:36

it’s just a consequence of how macros work

chromalchemy10:07:47

I don't have good intuition about how macros expand. Is defc= any different?

thedavidmeister10:07:51

state and scope are different things

thedavidmeister10:07:16

definitely you can chunk your state up into little re-usable and composable bits

thedavidmeister10:07:34

but scope is a whole other discussion

thedavidmeister10:07:01

generally no, javelin isn't encouraging global scope, or avoiding referential transparency in your functions

thedavidmeister10:07:37

actually it works 10x better if you leverage local scope as much as possible

chromalchemy10:07:52

@thedavidmeister I guess what I meant by "flat" is that if you define a cell a the top level, you can reference it anywhere, like a global let, kinda. So is that cell outside the scope of some other function you define further down the page?

thedavidmeister10:07:16

that's not really the goal, no

thedavidmeister10:07:19

of course you can do that

thedavidmeister10:07:32

but it has downsides and the upsides tend to fade away as you get into more complex setups

thedavidmeister10:07:13

the idea is that you can take the state of your program, which is really just data describing "what is going on right now"

thedavidmeister10:07:44

and instead of having either this mega thing that's super complicated and dangerous (because everything can edit everything else)

thedavidmeister10:07:12

or having state implicit in the behaviour of your application (all kinds of bad things happen)

thedavidmeister10:07:53

you can break the state down into small chunks of read/write data cell and pure input/output functions cell=

thedavidmeister10:07:22

all the links between the cells and functions can get realllly complicated quite fast

thedavidmeister10:07:32

but you don't have to worry because javelin is doing the plumbing for you

thedavidmeister10:07:53

traditionally you have to bind events or something similar to keep your incoming data, processing and desired DOM mutations all in sync

thedavidmeister10:07:09

and hand wire them all together, it takes a long time to do that by hand and is very error prone

thedavidmeister10:07:44

the situation is even worse if you don't adopt 1-way data flow and treat the DOM you're mutating as a source of incoming data as the "outputs" and "inputs" get all tangled together and you end up with some pretty crazy bugs

thedavidmeister10:07:11

so that's the goal of javelin πŸ™‚

thedavidmeister10:07:38

the problems caused by writing functions that "reach out" to external scope will be the same no matter what framework you use unfortunately

thedavidmeister10:07:27

- no way to test/predict the behaviour of a function just by looking at it in isolation

thedavidmeister10:07:52

- functions are tied to the structure and original context of the app they were written in

thedavidmeister10:07:00

- can't compose functions together within the same app without refactoring their internals or arguments every time you change something

chromalchemy11:07:08

@thedavidmeister Thank you for the thoughtful (re)orientation. This is something I have wrestled with, especially as my app grows in complexity. I have started leaning on Specter functions to edit a "mega thing" map that can be trivially synced to Firebase. The Specter macros don't expand properly inside formula cells, which is why I have seperate "query" functions, that I call inside the formula. But I think this is maybe a hint of not pushing the Javelin dataflow model far enough, making the working state more atomic, and making the master state that I want to serialize, the final result of the javelin flow, reactively building back up the state that was split apart at the beginning of the flow. In any case, I will try to better adopt the best practices you outlined, to better leverage simplicity, composability, testability, etc.

thedavidmeister11:07:21

i did the same thing you did then refactored my whole app at some point...

thedavidmeister11:07:15

you should still be able to write query functions, i think

thedavidmeister11:07:19

just pass the data as an argument

thedavidmeister11:07:36

i have query functions for datascript and i save datoms to my backing db

thedavidmeister11:07:43

seems along the lines of what you described

chromalchemy11:07:04

Good to know that I'm not too nieve:grin: I guess I was concerned that If I enact both decomposition and re-composition of a data structure in a set of javelin cells, it would in effect double my working memory footprint and lead to performance hangups and radically constrain how much data I could work with in the client.. How does that intuition square?

thedavidmeister11:07:47

why do you have to do this recomposition?

thedavidmeister11:07:29

if you're using something to represent a database

thedavidmeister11:07:35

you can run queries against it with javelin cells

thedavidmeister11:07:00

and then write to the database more easily with something like do-watch! rather than cell=

thedavidmeister11:07:09

or just against event handlers like :input

chromalchemy11:07:39

Ex. I have a set of maps that I can sync to FB. That's the db. Even if I filter out some of the maps into a subset with a formula cell and a query function, I still "write" changes to the master set, with a "setter" function on a :click attr. Or maybe put that "setter function into the formula cell to create a lens.

thedavidmeister11:07:37

yeah either seems fine

thedavidmeister11:07:59

but a lens already has read and write in it

thedavidmeister11:07:29

why have two different sets of cells when you could just turn the read only cells into lenses

thedavidmeister11:07:28

as both read and write are just functions i don't see a huge impact on memory just from that?

chromalchemy11:07:33

I think originally I loved the Idea of splitting all state into simple reactive bits via javelin. But I couldn't get my head around how I could package the state up for serialization, then re-hydrate the dataflow from dumb deserialization. I think my thinking had not evolved to consider Javelin lenses as the obvious solution. Maybe cause I was wary of writing crafty "setter" functions, when working with the split-up javelin data is so simple and obvious (the lens functions typically have to go levels deeper...?). That's one reason I'm attracted to Specter. It rebuilds the original data structure for you, with transformations inlined.

thedavidmeister11:07:21

oh, well give it a shot then

thedavidmeister11:07:25

i haven't used Specter

thedavidmeister11:07:05

i tend to go with the simplest solution i can think of until i run into a performance issue

thedavidmeister11:07:11

then i profile it and see what i can do πŸ™‚

chromalchemy11:07:49

It works, I think I'm muddying the water a bit though. I think your right, getting a better working sense of the lens side of Javelin will streamline my code and avoid a lot of this state composition confusion.

thedavidmeister11:07:15

i use lenses a lot

thedavidmeister11:07:22

but i keep the write side of things very "dumb"

thedavidmeister11:07:36

usually just some simple data wrangling, like juggling strings and keywords or whatever

chromalchemy12:07:11

Is this statement accurate?: In a Javelin lens, the read (getter) function and the write (setter) functions, can have totally different working data and architectural focus. So when you define them both in the same formula-cell, you can be threading very different things into one function declaration. While this composition is valuable, promoting succinct expressions downstream, it can be a hurdle for readability and understanding how the dataflow is working, considering each lens can have this large conceptual duality.

thedavidmeister12:07:53

but it's no worse than a lot of other techniques that are considered "normal" that are floating around πŸ˜›

thedavidmeister12:07:59

i tend to use it in pretty obvious ways

thedavidmeister12:07:03

(let [c (j/cell false)]
 (j/cell= c #(reset! c (boolean %))))

thedavidmeister12:07:12

and even more so, i tend to wrap up the more re-usable lenses in functions

thedavidmeister12:07:26

so, while a boolean lens is pretty contrived, i'd write it more like

thedavidmeister12:07:11

(defn boolean-lens
 [c]
 (j/cell= c #(reset! c (boolean %))))

thedavidmeister12:07:10

here's a more realistic example

thedavidmeister12:07:13

(defn throttle-cell
 "Returns a javelin cell that will only update its value at most once per X ms"
 ([v ms] (throttle-cell v ms (j/cell false)))
 ([v ms locked?]
  {:pre [(number? ms) (j/cell? locked?)]}
  (let [c (j/cell v)
        lock! #(reset! locked? true)
        unlock! #(reset! locked? false)]
   ; Set the lock whenever c changes.
   (add-watch c (gensym) lock!)
   ; Unlock automatically after ms.
   (add-watch locked? (gensym) (fn [_ _ _ n] (when n (h/with-timeout ms (unlock!)))))
   ; Return the throttled cell.
   (j/cell= c #(when-not @locked? (reset! c %))))))

thedavidmeister12:07:40

what's happening to the data being dropped by the throttle is not clear, i suppose...

chromalchemy12:07:54

Cool. Thx for the examples, and filling out the context. I will go about upping my (composable) lens game! πŸ€‘

thedavidmeister12:07:32

yeah, i avoided it for a long time

thedavidmeister12:07:52

now i kind of wish i'd just bit the bullet earlier and forced myself to learn it

flyboarder23:07:42

@thedavidmeister so you know, I also added the history-cell to hoplon/brew there could also be an HTML5History version if we wanted