Fork me on GitHub
#re-frame
<
2020-04-16
>
Quentin Le Guennec15:04:42

What is the proper way of composing signals? Intuitively, I would like something like Monad in Haskell. @(compose-signal (subscribe {:a}) #(subscribe [:b %))

Quentin Le Guennec15:04:10

Something that works like >>= in Haskell

Quentin Le Guennec15:04:05

I feel like neither reg-sub or reg-sub-raw does that, it rather "aggregates them"

p-himik15:04:56

For the people not familiar with Haskell, what does >>= do? Do you mean something like

(let [a @(subscribe [:a])
      b @(subscribe [:b a])]
  ...)
?

Quentin Le Guennec15:04:04

yes, something like that, but I think re-frame doesn't allow that, because a would need to be deferred?

p-himik15:04:35

Uhm. Why do you think that?

p-himik15:04:51

The code that I wrote is perfectly valid if used within a form-1 component.

p-himik15:04:00

Or within the render function of any other form.

Quentin Le Guennec15:04:10

oh nvm, you deferred it, it wasn't paying attention

p-himik15:04:37

Ah. Not "deferred". deref-ed. :)

Quentin Le Guennec15:04:26

Your code works indeed but it removes the potential for laziness. b cannot be computed if a is not deref-ed.

Quentin Le Guennec15:04:04

How I imagined it is the signal chain can be composed until the final value is needed, without any intermediate deref

p-himik15:04:18

Because of how Reagent works, you cannot possibly compute b without computing a first.

p-himik15:04:45

And you must not call subscribe without ever deref-ing it. It creates a memory leak.

Quentin Le Guennec15:04:55

But I compose b with a just how I can compose two functions without having a value

p-himik15:04:22

So if you call subscribe, you must use the @-ed value at some point.

p-himik15:04:48

The subs are not functions, far from it. The only common thing is that there's input and output, that's it.

p-himik15:04:02

With functions, it's the caller who's responsible to call the function again if the input changes. With subs, they do it themselves - changes to any of the subs derefed in a view will result in view rerendering.

Quentin Le Guennec15:04:35

Indeed, but I'm still not sure how signals cannot be composed. Surely if signal b depends of signal a, I don't need to know the value of a to manipulate b

Quentin Le Guennec15:04:09

I will need to know @a whenever I need to use @b

hindol15:04:28

You can compose subscriptions together and can deref the composite subscription. Look at the bottom of this file: https://github.com/day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs Is this what you want? (I have no idea what a Monad is)

p-himik15:04:04

@U0111PVCS8P That's the thing - you will need to use @b. Maybe not right where you subscribed to it. But every call to subscribe must be followed by a call to deref at some point. > Surely if signal b depends of signal a, I don't need to know the value of a to manipulate b You - don't need to, right. Reagent - does. b cannot exist without a.

p-himik15:04:06

@UJRDALZA5 The subscriptions signals and the :<- sugar might solve the problem for the simplest cases (and I admit - the case in the OP is one of them). But it doesn't solve the problem in the general case.

Quentin Le Guennec15:04:41

@U2FRKM4TW but @b will trigger the deref of a

p-himik15:04:44

No, I retract that - the OP case is not the simplest since :b doesn't even know about :a.

Quentin Le Guennec15:04:53

No because re-frame doesn't allow it or because it's computationally impossible?

Quentin Le Guennec15:04:14

I could write the compose-signal function myself, right?

p-himik15:04:33

Do you know how Reagent reactions work?

p-himik15:04:18

Damn, apparently I don't know it myself - I totally mixed it up with run!, sorry about that. Yes, you're correct. That is, if I understood you correctly. If b is a reaction that depends on a, then @b will trigger @a.

Quentin Le Guennec15:04:35

But there won't be any magic, right? I still need to define compose-signal ?

p-himik15:04:55

Yep. It can even be a macro. Either way, you will end up creating an extra reaction on top of a and b.

p-himik15:04:31

So if I'm imagining it correctly (and as you can surely already tell, it's very likely that I'm not 🙂), you will trade the requirement of writing extra @(subscribe) characters for the (compose-signal) characters and maybe an extra reaction. Right now, I don't see the appeal of such an exchange.

p-himik16:04:13

If you come up with something that you find satisfactory, please share if you can - I'm curious.

Quentin Le Guennec16:04:42

Well the idea is to come up with some way of introducing laziness to the equation, but I'm not sure it's possible nor derisable

Quentin Le Guennec16:04:55

Apart from reducing code size, ofc

p-himik16:04:34

I agree. Lazyness is useful to spread out the work and to avoid doing the unnecessary work. And when you render a component, both fly out the window because you need all of the data, right now.

p-himik16:04:28

Code side reduction is nice only if sacrifices, whatever they may be, are worth it. In this particular case, you probably sacrifice flexibility because the interface of compose-signal will likely be much more limiting compared to what you can do with two plain vectors and a value.

Quentin Le Guennec16:04:51

Well the same could be said for threading macros or comp, it still exists because it's practical in some cases.

p-himik16:04:17

I've been thinking and experimenting a lot with re-frame code bases that I have. And after about 3 years, I've made only 3 main things: 1. Create a macro that automatically creates calls to reg-sub for trivial cases of (get-in db [:a :b :c key]) or something similar 2. Use my own reg-event-* functions that all have the same base interceptors 3. Stop splitting events, subs, and views in different files. One component - one file, roughly. None of those events.cljs and co.

1
Quentin Le Guennec16:04:28

About laziness increasing the data lag, I'm not sure this is true

p-himik16:04:24

comp is still being debated by the users of Clojure. -> and co are useful in a vast number of cases simply because of how Clojure functions tend to position their arguments.

p-himik16:04:55

What do you mean by increasing the data lag?

Quentin Le Guennec16:04:47

You said, laziness is less useful in a render scenario, because "you need the data right now". I don't think eagerness is any better. In practice, I don't see how it should affect performance.

p-himik16:04:53

Lazyness is not able to improve the performance in the Reagent views. Lazyness can potentially improve performance in some other cases.

p-himik16:04:52

To be clear, I'm talking about the case when you eventually consume all the data. Because Reagent will consume all the data right away anyway.

p-himik16:04:22

Of course one could maybe do something incredible with setTimeout. But let's not dwell on it too much. :)

Quentin Le Guennec16:04:56

Reagent will consume the data whenever I call @, I think

p-himik16:04:17

Right, and if you have multiple @ in a single component tree, they will all be executed. So not using @ is akin to just not calling a function that produces some not lazy data.

Quentin Le Guennec16:04:43

Not sure I am following you

p-himik16:04:20

tl;dr: it's not possible, to the best of my knowledge, partially consume lazy data with Reagent. I've seen people much more experienced than I am in these matters advise to abstain from any lazy collections at all in Reagent because in come cases it makes it incredibly hard to debug, IIRC.

p-himik16:04:40

And if you do see or create such an example - again, please share if you can. :)

Quentin Le Guennec17:04:20

@U2FRKM4TW fyi, wrote this:

(defn chain-reaction [f r]
  (r/reaction
   (let [v @r]
     (if (and v (not (and (map? v) (:status v))))
       @(f v)
       v))))

Quentin Le Guennec17:04:44

the if condition is just a particular need

p-himik17:04:20

Ah, so it ended up not being related to re-frame at all. Can you give a couple usage examples?

Quentin Le Guennec18:04:43

yeah, (->> sig-a (chain-reaction f) (chain-reaction g)

Quentin Le Guennec18:04:00

will return the composed signal of f applied to g applied to a

p-himik18:04:16

Sorry, I meant a more practical one, especially if you use that function in re-frame with subscribe.

Quentin Le Guennec18:04:49

well let's say you have to chain queries, you could do somthing like this: (->> (subscribe [:a}) (chain-reaction #(subscribe [:b %})))

Quentin Le Guennec18:04:55

hate typing clojure anywhere else than in my editor err

Quentin Le Guennec18:04:21

(ofc the code I need to write depends on more than 2 signals)

Quentin Le Guennec18:04:55

reduce could be used with chain-reaction, also

p-himik18:04:55

I think I almost never see such a pattern have more than, say, 2 levels in my code. Probably because I tend to use small UI components, or maybe because your code has some particular requirements, dunno. In any case, it feels a bit strange that for N subs you create N - 1 reactions on top of them.

p-himik18:04:11

But if that works for you, then :thumbsup: :)

Quentin Le Guennec18:04:10

Not sure I'll end up using it, we'll see

Quentin Le Guennec18:04:47

But it was an useful exercise in understanding how reactions work for sure

lwhorton20:04:23

i have a conceptual question i hope someone can answer pretty easily: technically, could i call @(rf/subscribe [:foo]) anywhere in the codebase? in a component, in a handler, or just in any plain fn? there’s nothing special about the sub except that calling it inside a component means it will be cleaned up, and calling it anywhere else means it will dangle/leak memory over time? i.e. i want to make sure there’s no fancy introspection going on inside a component that means a subscription deref wont trigger if the subscribed value changes, if i deref it in some random function.

👍 2