7 ms·
My shop specializes in multiplatform FRP applications via the reflex haskell library [0]. After many years of wrangling FRP my biggest learned lesson is certai
by danharaj 9y ago
My shop specializes in multiplatform FRP applications via the reflex haskell library [0].
After many years of wrangling FRP my biggest learned lesson is certainly this: not everything is a stream! You need to be able to mix streams (also called events), which embody push semantics and behaviors (things you can sample), which embody pull semantics. This is important for performance and for the structure of large applications.
Coming up with a set of primitives to efficiently combine events and behaviors in expressive ways is really difficult but worth it.
[0] https://github.com/reflex-frp/reflex-platform https://github.com/reflex-frp/reflex-platform
- com2kid 9y agoOne of the teams I was on solved the pull behavior by having an "immediately publish last known value on subscription" parameter we could pass in when subscribing. This solved most of our needs around pulling data. We weren't doing a full FRP system, no composable streams, just building a system in which the only way to communicate was through subscriptions between components. This worked really well in practice, for example to configure the rate at which the accelerator ran, a component would subscribe to the proper hz publisher, with different publishers existing for supported frequencies. A fancy bit of code handled doing the math needed to deliver all the different subscribers data at their requested rate.
- realPubkey 9y agoSo its like an rxjs BehaviorSubject?
- com2kid 9y agoNot quite sure, haven't looked into rxjs much. We were running on an embedded micro with 256KB of RAM, so just making everything async and callback based was already pushing the boundaries of the industry. Vendors we worked with got extremely confused when we said they needed to hand us async code, that they had a few milliseconds to run in before they relinquished control, and that busy loops were verboten. I really miss working in a code base where every component can be trivially subscribed to. On the flip side, I've seen subscriptions taken too far, as people make every single value on a form a separate subscription. Seriously, it is A-OK to give the First and Last names together when the user hits submit.
- madeuptempacct 9y agoThere is ZERO utility to using observables with a registration form. Zero.
- michaelsbradley 9y agoFlapjax had behaviors, as you describe, as a key facility in a JavaScript FRP implementation, dating back to 2008: https://github.com/brownplt/flapjax/blob/f92e99ea25a4533c4ab2c7730e82330764d59ebe/fx/flapjax.js#L482 https://github.com/brownplt/flapjax/blob/f92e99ea25a4533c4ab... I "discovered" FRP in 2011, when I stumbled upon Flapjax — spent a month pouring over the source code, learned a lot, my brain has never been the same since!
- danharaj 9y agoSounds about right, FRP was originally formulated around events and behaviors all the way back in 1997! Great credit to Conal Elliott and the late Paul Hudak for that.
- naasking 9y ago> After many years of wrangling FRP my biggest learned lesson is certainly this: not everything is a stream! You need to be able to mix streams (also called events), which embody push semantics and behaviors (things you can sample), which embody pull semantics. I have a creeping suspicion that after a few more years, "push" is just going to be viewed as codata, and "pull" will just be "data". I mean, that's kind of what they are, our languages just don't have codata as first-class citizens, and so we shoehorn it into stream abstractions.
- zachrose 9y agoCould you unpack this a bit for the relative layperson? I once heard a guy at a Haskell meetup say that “codata is the categorical dual of data” but I didn’t know what that means then and I still don’t know.
- Retra 9y agoData is finite data structures, while Codata is infinite data structures (like streams.) They are duals because you can generate and process data using induction, while you can use coinduction to generate and process codata. One is "built up" naturally, and the other is "torn down" naturally.
- naasking 9y agoTo elaborate a little on the other poster, the data types you're used to are finite values built incrementally, ie. build this, then build this, then build that. Codata is the inverse, in that you can see it as a finite set of observations of some already built infinite data structure, ie. unpack this value at the head, then unpack that, etc. For instance, the stream of values observed from a process that runs forever. We usually encode codata using inductive data types, but this is awkward for all of the reasons that come up in FRP and other reactive systems.
- isoos 9y agoI believe that the distinguishing property is not what the event/data/codata is, rather how you are accessing it and when you process it. In a "push" model you provide a callback that will receive events and it has no way of influencing their frequency or timing. There is no mandatory buffering here. In the "pull" model, you have a handle, that you can call to receive the next event, which is essentially an iteration over async events. It is up to you to decide the frequency or the timing, and it is up to the backing library to buffer events if needed to be. I'm linking to the Dart API pages here, but the same concept applies to every language/library out there. Stream.listen [0] is straightforward and similar to many other libs out there. The interesting part is that Dart has a StreamIterator[1], which allows you to "pull" events form the stream at your own pace. [0] https://api.dartlang.org/stable/1.24.3/dart-async/Stream-class.html#listen https://api.dartlang.org/stable/1.24.3/dart-async/Stream-cla... [1] https://api.dartlang.org/stable/1.24.3/dart-async/StreamIterator-class.html https://api.dartlang.org/stable/1.24.3/dart-async/StreamIter... It is also easy to use Stream Iterator to loop over the values unconditionally: Stream<Event> stream = whatever(); var iter = new StreamIterator<Event>(stream); while (await iter.moveNext()) { // process the iter.current value at whatever pace you'd like }