Replies: 6 comments 7 replies
-
|
Sure, these are great questions. I'll answer them in order 1. Signals can't be inspected at the REPLSignals can definitely be inspected at the REPL. Here's an example (def counter (flex/source 0))
(def counter-sq (flex/signal (* @counter @counter)))
;; Our signal hasn't computed anything yet because it's "disconnected"
@counter-sq ;; => :town.lilac.flex/init
;; now it's "connected" and will have a value, and recompute on every change to `counter`
(def fx (flex/effect [] @counter-sq))
@counter-sq ;; => 0
(counter 2)
@counter-sq ;; => 4
(flex/dispose! fx)
;; since `fx` is disposed, it will disconnect the signal, clearing its state and no longer recompute
@counter-sq ;; => :town.lilac.flex/initYou said
What it sounds like is happening is you're dereferencing a signal that hasn't been "connected." By default, signals do not recompute on change. Only if they are depended on by another connected signal or an effect do they "connect" do they compute their value and react to changes. This allows you to construct as many signals as you want, and only those that are referenced will react to updates and store their value, which is nice for letting letting the GC clean things up so we don't take up too much memory. Ultimately, the goal of flex is to trigger side effects based on changes to inputs. If you only want to inspect a value, you can do what I did in the example above and create a dummy effect that dereferences the signal. You can then dispose of the effect when you're done. But I'm curious, what do you want to happen as these values change? 2. Not super clear how to chain signals
I'm not sure what you mean here. "Chain my signals" means you want signals that depend on other signals, right? That is definitely supported. Here's a diamond dependency modified from the tests in test/town/lilac/flex_test.cljc (def A (f/source 2))
(def B (f/signal (* @A @A)))
(def C (f/signal (+ @A 2)))
(def D (f/signal (* @C @C)))
(def fx (f/effect [] (prn (+ @B @D))))In this diamond dependency, the propagation algorithm guarantees that each update to 3. signal wrapping everythingYou can define your own macro to help you with this. :) (defmacro defs
[sym body]
`(def ~sym (flex/signal ~@body)))4. No quick and easy way to subscribe to map values
I'm not sure that's a fair assumption, but it's easy to build a helper function for yourself if you like (defn sub
[s f]
(flex/signal (f @s)))
(def params
"The state atom"
(flex/source
{:input-dir "./data/gauge/"
:id-2-name {1645240 5
8373316 3
7598252 4
1683136 2
8371400 1
1641660 6
7598984 7}
:inside-d18o "irregular-cave-samples.csv"
:outside-d18o "manual-rain-samples.csv"
:plot-height 400
:plot-width 1000
:blah "blah"}))
(sub params :blah) ;; returns a signal that will track the value of `:blah` in the map |
Beta Was this translation helpful? Give feedback.
-
|
This is fantastic! Thank you for getting back so fast and helping me iron out my misunderstandings
Thank you again for helping me work through the speed bumps. I can definitely start my rewrite with this. I really like your pragmatic/flexible approach. And please don't take it wrong. I don't mean to come and just say "Your defaults are wrong and you should do things different". I will try to write some macros/helpers for myself as you suggest - I just am a bit apprehensive to make my own ball of mud before i really understand your design rationals. It's likely your way is well thoughtout since you've been thinking about this way longer than me |
Beta Was this translation helpful? Give feedback.
-
I would not take your posts this way! I assume that we are both learning. You, about my weird library, and me, about your weird use cases 😛 and each of us about our weird workflows. I'll respond to a few key points in your last post
The downside of auto connecting by default is that you then do not have a way to detect when you should dispose of them and let them be garbage collected. Take a simple example (def counter (flex/source 0))
(def counter-sq (flex/signal (* @counter @counter)))When You might also end up having a program with 10s or hundreds of signals defined, and (IMO) they shouldn't all be reacting unless they're actually being used to produce some output, otherwise it's a waste. You can also have programs that dynamically create signals in reaction to other signals, which could end up with thousands of signals being created over time, and I want those to be able to be as close 0-cost as possible.
I think my workflow so far has been to create an app using signals and inspecting them once they're hooked up. So for instance I'll create a web UI that has some sources and signals and inspect them as they change over time. I think that I have tended to do less focused development on a specific signal, as most of my complex logic I put elsewhere. I see the benefit of being able to test the logic of a signal without it being connected by derefing it outside of the context of a signal or effect. I think one of the things I wanted to communicate to the user is whether a signal in fact is connected or not. Perhaps a better developer experience would be to do something like @counter-sq ;; => 2
;; prints out "WARNING: Signal dereferenced outside of reactive context"What do you think?
I think the risk to creating a dummy connection is actually that it won't be garbage collected once out of scope, because signals and effects keep references to both their dependents and dependencies, so as long as the signal is in scope it will keep the effect in memory. The problem is that if you don't yourself store a reference to the effect, you can't dispose it and disconnect everything. It sounds like your need isn't necessarily to have a connection that will auto update, but to be able to test the logic of your signals at the REPL. Is that right? If so, I could think about how to create a "non-reactive" branch that will calculate everything but print a warning and not cache its result without a connection, like I suggested above.
Creating lots of little signals might not be a big deal, but you can also use (require '[town.lilac.flex.memo :as flex.memo])
(def sub
(flex.memo/memoize
(fn [s f]
(flex/signal (f @s))))
(def src (flex/source {:foo "bar" :baz 42}))
(def foo (sub src :foo))
(= foo (sub src :foo)) ;; => true |
Beta Was this translation helpful? Give feedback.
-
|
@kxygk I've migrated this to discussions since I think there might be multiple tasks/issues that come out of our back and forth |
Beta Was this translation helpful? Give feedback.
-
|
@kxygk making another threaded post because it might be easier to talk about these things without them being intermingled with the deref discussion, but I'm really curious now how you're hooking up cljfx and flex. Could you describe your approach? Do you have any public code you've written so far? |
Beta Was this translation helpful? Give feedback.
-
|
So I've been using flex in doing some machine learning homework assignments. It's not be a great fit or stress test for the library tbh :)) - just in terms of problem space. I think flex really comes in to play when you have a large complex problem with a lot of interdependence. In shorter homework style problems simpler/linear scenarios pure functions and transducers are usually enough - but it's still got me thinking a bit Every simple pure function that takes some static argument, ex: '(my-func arg)' can be refactored to take a signal/state and return a signal. This sort of makes life better..? You can arbitrarily generate new signals, and you can reevaluate by updating the input. More complex functions that maybe calculate some other other properties derived from the input state/signal can sort of do that quietly internally - and if the computation has already been done (memoized) and it can be reused then that occurs automatically So in the end you can.. In effect turn everything into signals! But in practice.. it's actually kinda tedious and annoying to write :S and it doesn't compose very nicely with idiomatic Clojure. If you want to 'map' over a seq and do some stuff on the items.. Well now what you need is an 'fn' and that's gunna call other 'fn' s and.. It's no longer using all the signals you've been writing.. So to get around this you either have to still write bare 'fn's and then write wrappers that generate signals (but that's also tedious) or maybe create an 'fn' signal before the map call.. and then in the map at every iteration update the input and then deref to get the result? But the code just seems to get kinda icky everywhere. It's still something I'm exploring and playing around with though I honestly don' t really have any concrete suggestions at this point yet. I guess at the end of the day what might make life easier is and ergonomic way to be able to write a function that when given a constant returns a constant - and when given a signal returns a signal? Coding with signal you the constant need to deference things everywhere has weird knockon effect - as you can't really use the signal blindly as a value. But this is a just a bit of a half baked thought at this point :)) Anyway, those are my first kind of disjoint thoughts. And I also just want you to know I'm still trying flex out and looking forward to figuring out how to integrate it into my workflow |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi,
Sorry I'm getting back to you so late. I appreciate the thoughts you shared with me here:
https://old.reddit.com/r/Clojure/comments/11723ea/is_there_a_reframecljfxlike/
I just tried to convert a little plotting program of mine to use flex and I've hit a few initial stumbling blocks. I only really mean this as to share an initial-impression/user-experience - and not criticism. It's very likely "i'm holding it wrong" and misunderstanding how to use flex.
Signals can't be inspected at the REPL
Just working off the example in the README. You hook up
counter-sqto yourcounteratom. Whencounterupdates,counter-squpdates under the hood. However as far as I can tell, there is no way to actually inspectcounter-sqat the REPL. It just returns a:town.lilac.flex/init. You can hook up a listener and print the value, but it's clunky and only prints a single time on change. tbh, I expected it to outwardly continue to behave transparently as a integer - but at the very least I'd want to be able to peak at its value at the REPL :)Not super clear how to chain signals
I obviously wants to go a step further from the basic example and chain my signals. Leverage diamond dependencies and so on.. Since I can't dereference them, I'm not super sure how to do that? I can listen to a signal and create a new signal - but that seems a bit clunky. I don't think it will do the diamond dependency magic that way as well?
signal wrapping everything..
Basically any variable that derefs a
flex/sourcewill in effect be a signal. So I'm envisioning you end up with a ton of this in your code:Maybe just aesthetic but it'd be a bit clearer with a macro
Because you don't exactly have a static variable nor really a
defnfunction. That said, I get the desire to avoid macros :))In the CLJFX model the equivalent of signals are split into
sub-valandsub-ctx. The latter is a memoized function call - a bit similar to your signal. The former is a convenience function that allows you to subscribe to map valuesSo as I started to massage my code to use flex, my
flex/sourceatom looked something like this:From the API it looks like in flex I need to create a signal for each an every key @_@ so that when a part of the map gets updated I get the appropriate update (that can propagate to all the signals). Otherwise my whole DAG gets recomputed on each map update (well I assume it'll terminate when identical values are returned). Ergonomically this is a bit of a mess. Basically if you have a .. simple? (finite/non-lazy) built in datatype as a source, you can be sure you'll automatically want signals for every single elements
in CLJFX
sub-valis limited to first level map keys - but ideally then should be able to generate a signal on deeper valuesAnyway, those are my first thoughts :))
Beta Was this translation helpful? Give feedback.
All reactions