Brainstorm to turn stream/varying back into Signals

588 views
Skip to first unread message

Evan Czaplicki

unread,
Mar 18, 2015, 5:10:40 PM3/18/15
to elm-d...@googlegroups.com
Combining Richard's recent post about Varyings and Laszlo's idea about ports may point to a way to unify everything again.


Minimally invasive change to Signal library

What if a Port was defined like this:

type alias Port =
    { address : Address a
    , events : Events a
    }

subscribe : a -> Events a -> Signal a
 -- give a stream of events an initial value and it
 -- can become a signal


And then everything else in the signal library would be the same. Same merge and sample and everything else.


Events as a community library

If we have this concept of Events, it seems reasonable to expand that into a library of its own. This brings back the stream/varying issue with different names. When do I use a signal? When events?

Maybe the default in the standard library is "always use signals" but we could expose something like this function:

Signal.destructure : Signal a -> (a, Events a)

The idea is to allow conversion back and forth such that Signal.filterMap can be used to define Events.filterMap. This could be a community library so that you don't get smacked with this distinction until you come to desire it.

John Mayer

unread,
Mar 18, 2015, 5:45:46 PM3/18/15
to elm-d...@googlegroups.com

I like Signal + Events better. I'd argue to keep them both in the standard library. I often think that Event a = Signal (Maybe a). Previously, when dealing with user input in Elm, I'd unavoidably encounter Signals that start with a Nothing and then afterwards always be Just something. I think this will be only more common with the new release and how it handles inputs and outputs with Tasks/Promises. So, I lean towards making Event a first class pattern and keeping the standard library combinators following this pattern to force good architecture.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Evan Czaplicki

unread,
Mar 18, 2015, 6:00:34 PM3/18/15
to elm-d...@googlegroups.com
Here's a version of the Signal library that has both Events and Signals in there. That kind of thing?

Or split it into separate modules? I think the other version of this is to split it between Signal, Event, and Port modules.

Hassan Hayat

unread,
Mar 18, 2015, 7:22:29 PM3/18/15
to elm-d...@googlegroups.com
I'd split it into seperate modules. 

I assume that someday the module system would be improved such that all these libraries can be packaged into one big package called "Core" or something.

So, that you may then do:

import Core

and then do:

mySignal : Core.Signal a
 
myEvents
: Core.Events b
 
 
and so on...


I think that it makes sense to split them up (mainly because of map). It's also a sanity check cuz there probably are going to be tons of elm-events-extra and elm-signal-extra libraries and you may want to bring some of those functions in the standard library without worrying about function names colliding.

Richard Feldman

unread,
Mar 18, 2015, 7:28:10 PM3/18/15
to elm-d...@googlegroups.com
The type Events really rubs me the wrong way. The fact that it's a type with a plural name is kind of awkward, suggesting it's just a vague collection of Event values - like a list or a set, not something conceptually related to time like what Stream connotes.

I realize that we on this mailing list find Signals familiar, but I have yet to meet the programmer who had any idea what Signal meant until I walked them through an explanation. My experience with the term Stream has been the polar opposite: I just say the word "stream" and they know what I'm talking about. In fact, explaining signals in terms of streams - "it's basically a stream of values that has to have an initial value" - has been one of the quickest ways to get a programmer unfamiliar with Elm to understand Signals. (They nod at "a value varying over time," but then when I show them port code they ask why ports need initial values; that critical distinction doesn't seem to have clicked.) I guess this experience is why I keep advocating for terms like InitializedStream.

Is the objection to having both Stream and Signal that they have similar names and therefore would be easily confused? If so, Stream seems the more helpful inclusion.

Hassan Hayat

unread,
Mar 18, 2015, 7:34:01 PM3/18/15
to elm-d...@googlegroups.com
If the problem is Stream and Signal sounding alike, then Stream could be renamed to Flow, for example.

Richard Feldman

unread,
Mar 18, 2015, 8:33:41 PM3/18/15
to elm-d...@googlegroups.com
From my perspective, though, that has the opposite of the desired effect; what I like about Stream is that I mention it to programmers and they immediately know what I'm talking about. "Streaming" is already a pervasive mainstream concept.

Unfortunately, the same is true of neither Signal nor Flow.

Hassan Hayat

unread,
Mar 18, 2015, 8:37:24 PM3/18/15
to elm-d...@googlegroups.com
Dataflow? Feed? (Like RSS Feed?)

Sonny Michaud

unread,
Mar 18, 2015, 8:59:05 PM3/18/15
to elm-d...@googlegroups.com
That is a very interesting data point.  For me, it was very much the opposite.  I was learning FRP and the fact that signals were a foreign concept was actually extremely useful.  There was nothing to unlearn, there was simply a new (and better) abstraction I could deal with.

I haven't been following the whole Varying/Stream discussion too closely, but I was definitely a bit sad to see that Signals were going to be removed.  Then, I realized, there is probably a way to recreate Signals as something along the lines of:
type alias Signal = Stream (Maybe a) | Varying a

I think it would be possible to continue using the signals abstraction, but also have an explicit separation when it is necessary.  I am not sure this is beneficial on the code level, but I think signals are a really good abstraction that allows the programmer to think on a higher level and ignore a lot annoying details.

- Sonny

Hassan Hayat

unread,
Mar 18, 2015, 9:17:43 PM3/18/15
to elm-d...@googlegroups.com
I agree, Sonny, it was helpful to me too that FRP was just foreign to me cuz then I couldn't conflate it with anything.

The way I understood Signals at first is like a movie or an animation. Basically each state is like a frame and foldp cycles through each frame and from that you get an moving application like a moving picture (a.k.a movies). Maybe a better name for Signals could be harvested from this intuition? 

Richard Feldman

unread,
Mar 18, 2015, 10:29:09 PM3/18/15
to elm-d...@googlegroups.com
I think Feed is an improvement over Events, although Stream still seems better if we're just considering that one term in isolation.

Richard Feldman

unread,
Mar 18, 2015, 10:31:20 PM3/18/15
to elm-d...@googlegroups.com
...Or did you mean Feed as an alternate term for Signal?

Even if not, that's actually pretty interesting to consider:

subscribe : a -> Stream a -> Feed a
 -- give a stream of events an initial value and it
 -- can become a feed

Richard Feldman

unread,
Mar 18, 2015, 10:33:04 PM3/18/15
to elm-d...@googlegroups.com
...especially if you consider that something like a News Feed or an RSS Feed always has some value.

You can always ask "what's my feed look like right now?" and get an answer, even if nothing has been posted to it yet. There might be some useful intuition there.

Hassan Hayat

unread,
Mar 18, 2015, 10:49:44 PM3/18/15
to elm-d...@googlegroups.com
I actually meant it for streams but now that you mention it... Feed seems to work as a replacement of Signal.

But I was thinking of Feed as a replacement for Stream because you can "feed a Feed into a port" and "subscribe to a Feed from a port". That was the logic. I think this is pretty easy to explain. And I remember in the mailing list you saying something about Signals being a pub/sub system. "Feed" fits that nicely.

I kinda also dig Sonny's idea of changing Signal to be some sort of glorified maybe.

So, suppose in the Feed library you have some "empty" Feed:

empty : Feed a

You could then use this as the initial value for "Signals". Then we could just remove Signals as a concept entirely and deal exclusively with "Feeds". It still works the same way (with a Signal graph and whatnot). Except that in all cases, the initial "Signal" is "empty" (or a constant "Feed" of your choice, if that makes sense). 

I don't know what the larger implications of this would be though.

Richard Feldman

unread,
Mar 18, 2015, 10:55:11 PM3/18/15
to elm-d...@googlegroups.com
Another option to consider: Timeline as a replacement for Signal.

I could see that being more intuitively understood as "a value that changes over time" than Feed or Signal, although it's more verbose than either.

Hassan Hayat

unread,
Mar 18, 2015, 11:02:13 PM3/18/15
to elm-d...@googlegroups.com
+1 for Timeline. It totally fits my intuition of Signals being some sort of Animation. Plus it really explicits the whole Time thing (time travelling debugger, etc...)

Brian Slesinsky

unread,
Mar 18, 2015, 11:22:36 PM3/18/15
to elm-d...@googlegroups.com
I like the notion of variables being, well, variable. As pseudocode:

main: Var Html 
main = vary startingFrame with streamOfFrames

Rintcius Blok

unread,
Mar 19, 2015, 8:43:35 AM3/19/15
to elm-d...@googlegroups.com
I'm quite happy with Stream and Signal, would this be something?

type alias Port =
    { address : Address a
    , stream : Stream a
    }

subscribe : a -> Stream a -> Signal a

Also, I'd prefer decompose or deconstruct over destructure

Evan Czaplicki

unread,
Mar 19, 2015, 10:44:58 AM3/19/15
to elm-d...@googlegroups.com
Let's try to organize all the different things people are saying:

Signal:
  • Pro: Keep existing terminology, minor changes needed in 0.15 with the ability to easily move towards having a Signal and Events library. Very easy to present this concept.
  • Con: a lot of the typical functions are nicer to use when you make a distinction.
From now on, all the versions have the con that "presenting becomes a two part process" which maybe will be confusing.

Events + Signal:
  • Pro: Keeps existing terminology as much as possible.
  • Con: It's weird to say "Events" when "Stream" is a commonly used term that works well. People don't know what a Signal is.
Stream + Varying:
  • Pro: We use the standard term "stream". Varying does not have an overloaded meaning, but it's still a very literal term for "values that vary over time".
  • Con: Some people dislike the name 'varying' for reasons that are not super clear to me
Stream + ???
  • Pro: We address the "I don't like the name Varying" concern
  • Con: I don't think it's ideal to be metaphorical in cutesy ways. If someone says, "the internet is like a series of tubes" that does not mean it's clearer to call the internet "a series of tubes" all the time because the metaphor will break down. So I don't think we have a good candidate for a straightforward descriptive name.

I think these are the viable options to choose from. Does seeing it like this help?

--

Rintcius Blok

unread,
Mar 19, 2015, 10:55:57 AM3/19/15
to elm-d...@googlegroups.com

On Thursday, March 19, 2015 at 3:44:58 PM UTC+1, Evan wrote:
Let's try to organize all the different things people are saying:

Signal:
  • Pro: Keep existing terminology, minor changes needed in 0.15 with the ability to easily move towards having a Signal and Events library. Very easy to present this concept.
  • Con: a lot of the typical functions are nicer to use when you make a distinction.
From now on, all the versions have the con that "presenting becomes a two part process" which maybe will be confusing.

Events + Signal:
  • Pro: Keeps existing terminology as much as possible.
  • Con: It's weird to say "Events" when "Stream" is a commonly used term that works well. People don't know what a Signal is.
Maybe I'm missing something but why not Stream + Signal instead of Events + Signal? I.e. use the term Stream instead of Events. It would get rid of the first "con".
 
Rintcius

Evan Czaplicki

unread,
Mar 19, 2015, 11:03:22 AM3/19/15
to elm-d...@googlegroups.com
Ooops, forgot that one!

Stream + Signal:
  • Pro: Keep existing terminology to some extent, get to use the typical "stream" term
  • Con: Is signal actually a better name than varying? Like, forget you know Elm already. Is it actually better? Second, to me stream and signal sound really similar, so I can imagine beginners having trouble keeping them separate in their minds "I want one that is always defined, which one is which again?"
Perhaps my editorials on these options does not match reality? I'll try to find new people to present it to when I'm back in SF and see what works.

--

Dobes Vandermeer

unread,
Mar 19, 2015, 11:57:35 AM3/19/15
to elm-d...@googlegroups.com

I think stream should be reserved for lazy lists. Other wise it's too far off from standard FP terminology.

Richard Feldman

unread,
Mar 19, 2015, 1:21:21 PM3/19/15
to elm-d...@googlegroups.com
This one just occurred to me...

Stream + Channel:
  • Pro:  Both Stream and Channel are fairly common terms in use around programming concepts like this. "A channel is a value that varies over time" has some interesting supporting metaphors like "you can tune in at any moment and see what's on at that exact moment." Gets the positive benefits of Stream without the concerns of Signal or Varying.
  • Con: "channel" is already a term in use as of 0.14
Here's what the OP looks like in this world:

type alias Port =
    { address : Address a
    , stream : Stream a
    }

subscribe : a -> Stream a -> Channel a
 -- give a stream of events an initial value and it
 -- can become a channel

Channel.destructure : Channel a -> (a, Stream a)

Richard Feldman

unread,
Mar 19, 2015, 1:55:52 PM3/19/15
to elm-d...@googlegroups.com
I'm curious to explore this option further:

Signal:
  • Pro: Keep existing terminology, minor changes needed in 0.15 with the ability to easily move towards having a Signal and Events library. Very easy to present this concept.
  • Con: a lot of the typical functions are nicer to use when you make a distinction.
From now on, all the versions have the con that "presenting becomes a two part process" which maybe will be confusing.

All else being equal, having only one term to think about instead of two is objectively simpler.

I'm curious: which functions are nicer to use with the distinction? And are there any cases where it would be inconvenient to be required to provide an initial value even though that value would never be used?

Richard Feldman

unread,
Mar 19, 2015, 2:31:57 PM3/19/15
to elm-d...@googlegroups.com
To take a step back, here are some observations I've made about working with Elm APIs.
  1. Consider Haskell for a moment. As I understand the history, when it was discovered that Applicatives were a superclass of Monads, the language's design path became to make that relationship explicit, and to start using Applicatives wherever possible because they were more general. This seems logical for Haskell, given the language's design and that it's already paid the complexity cost of HKP.
  2. In contrast, Elm has paid no such price. The type system is substantially simpler for it (a fantastic benefit!) but that means whenever you introduce a new type with a subclass or superclass relationship to another type, you're also necessarily introducing a conversion step to go between them.
  3. One of the things I really appreciate about Elm is that its design philosophy is closer to "a better experience is better" than Haskell's "more abstraction is better." In practice, the path to a better experience often involves sacrificing some degree of flexibility.
  4. As such, the barrier for splitting out any given type into two types should be higher in Elm than it is in Haskell - regardless of their relationship. There's an intrinsically higher price to be paid, and it's far from given that the benefit of separation will outweigh that.
There seem to be similarities between the Signal/Stream split in Elm and Monad/Applicative split in Haskell, but it's not clear that Elm should go the same route Haskell did.

petar...@googlemail.com

unread,
Mar 19, 2015, 3:45:10 PM3/19/15
to elm-d...@googlegroups.com
I don't think my objection is to Varying as a term (although I think for non-English speakers, it may be a bit harder to grasp than Signal which is pretty universal in Indo-Eurpean languages). It is more the split of Signal into multiple concepts that I am not sure about. Richard explained it much better than I could.

P.

Alex Neslusan

unread,
Mar 19, 2015, 4:12:17 PM3/19/15
to elm-d...@googlegroups.com
For what it's worth, It wasn't until I read Sonny's post in this thread that I realized the kind of type alias Signal = Stream (Maybe a) | Varying a relationship. Until then I was still sort of confused as to the actual distinction between Stream and Varying.

I'm also not really convinced that tying a word like "Stream" vs "Signal" into someone's existing knowledge is necessarily going to be a big step toward learning Elm. I've only tried teaching Elm to two people, but learning ML-style syntax and how Signals actually fit together into a program larger than a Hello World seemed like the two most difficult pieces.

Thomas Weiser

unread,
Mar 19, 2015, 6:09:53 PM3/19/15
to elm-d...@googlegroups.com

TL;DR: Varying without an implicit dropRepeats may be contraintuitive.

Some musings about having only one type Signal, vs. two, Stream and Varying (or whatever they are termed): How would a newcomer to Elm think about these concepts?

When I was a newcomer I needed some time to realize that a signal has these two aspects: being a time-varying value and having some “pulse”, which potential updates even if the value doesn’t change. Like a unified representation of state and events.

I have read a post from Evan stating that modern research in FRP prefers the unified concept of signals over the distinction of state and events. Ok I thought, nice, a nifty choice to keep the language lean.

Then there is this problem with obligatory start values for signals. IIRC this was the initial motivation for the split into Stream and Varying. A Stream is a Signal without a start value, and a Varying is a Signal with a start value.

But does this operational model really meet the mental model of state and events? No, at least not in one specific point: As proposed a Varying can still update even if the value stays the same (not varying, so to say), and it tell its child-nodes about this non-change. This seems contraintuitive and hard to explain.

We could come around with implicit dropRepeats in those API functions that return a Varying result, e.g. map, toVarying, etc. Is this feasible? Would there be any serious consequences in efficiency or semantics?

Honestly, I’m feeling a bit confused about the best way through operational and intuitive models and their trade-offs. I hope my writing makes some sense anyway.

Dobes Vandermeer

unread,
Mar 19, 2015, 7:33:41 PM3/19/15
to elm-d...@googlegroups.com

Personally I'd rather have a signal of maybe for events, rather than two concepts. It wouldn't be hard to have a separate set of signal functions that map over the maybe for you, but you still have the option to work with the maybe directly as well.

Events can actually be defined as nothing except in the update on which the event occurred.  But there might be other signals that come and go as well; a modal application might turn off a subset of signals to nothing when they don't apply.  Like if you are on the game menu the in-game related signals can flat line.

Brian Slesinsky

unread,
Mar 20, 2015, 1:45:17 AM3/20/15
to elm-d...@googlegroups.com
Good point! Yes, automatic debouncing seems like a good thing to have for a value that varies, but doesn't make sense for an event stream. It seems like that's additional justification for having two concepts, though maybe not enough.

Kaspar Emanuel

unread,
Mar 20, 2015, 10:43:44 AM3/20/15
to elm-d...@googlegroups.com

Excuse me if I am missing something fundamental but could we define Signal in terms of Stream like so?

type Stream a = Stream a

type Event a = Changed a | Unchanged a

type Signal = Stream (Event a)

Stream, Event and hence Signal would all be “applicative”. An alternative to Event could be Sample though I think I like that name because I have done some signal processing. I wrote a similar abstraction for Helm: Signal and Sample.

Kaspar Emanuel

unread,
Mar 20, 2015, 10:46:00 AM3/20/15
to elm-d...@googlegroups.com

Signal should have been a type alias for valid Elm code or:

type Signal = Signal (Stream (Event a))

Kaspar Emanuel

unread,
Mar 20, 2015, 11:35:29 AM3/20/15
to elm-d...@googlegroups.com

… and I keep forgetting the type variable. For clarity:

type Stream a = Stream a

type Event a = Changed a | Unchanged a

type alias Signal a = Stream (Event a)

-- or

type Signal a = Signal (Stream (Event a))

Brian Slesinsky

unread,
Mar 20, 2015, 1:12:14 PM3/20/15
to elm-d...@googlegroups.com
More generally:

type Update a = Create a | Diff a a | Unchanged a | Delete a | Missing

This are all the possible changes for a value that comes and goes.

This seems like two perspectives on a value that varies. A Signal of
values can be converted to or from a Signal of updates.

Events are more general. There are events that can't be thought of as
diffs on a value. But you can have a value that's "the most recently
seen event", and at the beginning of the program it's Missing.

- Brian
> --
> You received this message because you are subscribed to a topic in the
> Google Groups "Elm Discuss" group.
> To unsubscribe from this topic, visit
> https://groups.google.com/d/topic/elm-discuss/w2Rmim4IUn4/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to

Kaspar Emanuel

unread,
Mar 20, 2015, 3:54:35 PM3/20/15
to elm-d...@googlegroups.com

More generally:

type Update a = Create a | Diff a a | Unchanged a | Delete a | Missing

Yes, that is a more general explanation but let’s talk about what to expose to the user.

The user doesn’t care about the initial Missing/Create/Delete values. Similarily the value “Diffed” against doesn’t matter.

This simplified Update/Event/Sample type I had is actually useful in illustrating the difference betweens Streams and Varying/Signal (I’ll just stick to using Varying henceforth).

Like Thomas said:

When I was a newcomer I needed some time to realize that a signal has these two aspects: being a time-varying value and having some “pulse”, which potential updates even if the value doesn’t change.

So I would propose something like this API:

type Update a = Changed a | Unchanged a

valueOf : Update a -> a
valueOf update = case update of
Changed x -> x
Unchanged x -> x

type alias Varying a = Stream (Update a)

-- instead of destructure
streamValues : Varying a -> Stream a
streamValues = Stream.map valueOf

-- a more general fold
fold : (a -> b -> b) -> b -> Stream a -> Stream b

-- instead of subscribe
dropRepeats : Stream a -> a -> Varying a
dropRepeats stream initial =
fold
(\x z -> if x == (valueOf z) then Unchanged (valueOf z) else Changed x)
(Unchanged initial)
stream


This way you can write functions lifted to Stream and convert them to Varying if you have something sensible to do with the Update

Kaspar Emanuel

unread,
Mar 21, 2015, 12:04:54 PM3/21/15
to elm-d...@googlegroups.com
Am I missing something fundamental in proposing something like this?

Brian Slesinsky

unread,
Mar 22, 2015, 5:23:41 PM3/22/15
to elm-d...@googlegroups.com
I think you have it about right, and I've been thinking about this
problem the wrong way.

It seems like there are two ways of thinking about it:

- A Stream is a Signal whose initial value may be Nothing.
- A Signal is a Stream that receives an event when the Elm program starts up.

The second approach seems conceptually cleaner to me. Let's say we
define a special Stream called "onStartup" that only receives an event
on startup. We can use the type system to make sure that all Signals
also receive an event at startup, because they depend on the
"onStartup" stream in a way that causes them to emit an event at
startup.

More formally:

- A Stream is not defined before its first event happens. It's defined
all times after that.
- A Stream is a Signal if its first event happens at startup.

Therefore, a Signal is defined at startup and at all times after that.

- onStartup is a Stream
- onStartup's first event happens at startup, by definition.

Therefore, onStartup is a Signal.

- "map" is a function whose inputs and outputs are Streams
- The first event on the output stream happens when all of its inputs
have emitted their first events.

Therefore, if all the inputs to "map" are Signals then the result is a Signal.

- "merge" is a function whose inputs and outputs are Streams
- The first event on the output stream happens when any of its inputs
emit their first event.

Therefore, if any input to "merge" is a Signal then the result is a Signal.

I think we can derive everything else from onStartup, map, and merge:

- To define a constant Signal, you can map onStartup using a function
that always returns the same value.

- To convert a Stream to a Signal, merge with a constant Signal.

If we take this perspective, we don't say that a Stream contains a
Maybe. There is no value for a Stream before it gets its first event,
in the same way that a function parameter has no value before the
function is evaluated. Streams come into existence when they emit
their first events, recursively, and before then they haven't been
calculated yet.

We can also generalize this to times other than startup. The same
derivation applies, replacing "startup" with a different timestamp or
set of timestamps.

For example, perhaps some tasks should be allowed to happen before the
Signals are all defined? We could define a Ready timestamp which is
somewhat after program startup. This would allow some events and tasks
to happen before the program is Ready.

Or perhaps we could say that an AnimationStream is a stream that emits
an event on every animation frame.

This seems pretty neat, but I'm not sure how much of it makes sense to
embed into the type system. We could just have Streams and informally
talk about signals as the subset of streams that are defined at
startup. For the program's display, Elm could automatically display a
"Loading..." placeholder until the corresponding stream is defined. If
you don't like that, merge in your own placeholder screen.

- Brian

Jeff Smits

unread,
Mar 23, 2015, 8:24:40 AM3/23/15
to elm-discuss
In representation of a Signal might considered be a special case Stream that has an initial value at program start, but the semantics of a Signal are quite different. It's not (just*) a stream of events, it's a value that changes over time in a discrete manner, therefore there is always a value, therefore higher arity maps are possible. This is not the case for a normal Stream and makes it worthwhile to distinguish between them in the type system IMO.

* The Signal we're familiar with keeps the event information of it's value changes, so it's both an event stream and a value varying over time. I suspect that's the idea of Stream/Varying, Stream is just the events, Varying is just the value over time where you cannot have an observable event as long as the value doesn't change, whereas with Signal you can.

You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

Brian Slesinsky

unread,
Mar 23, 2015, 10:15:26 AM3/23/15
to elm-d...@googlegroups.com

What do you mean by "higher arity maps are possible"?

Jeff Smits

unread,
Mar 23, 2015, 10:19:58 AM3/23/15
to elm-discuss
map2, map3 etc.

Streams have just events. Most events in Elm don't happen at the same time (which is why merge usually doesn't drop events). So map2 on Streams makes no sense. But map2 makes sense for Varying/Signal, because it's a (time-varying) value. The resulting Varying/Signal changes whenever one of the "parents" changes, and because of the constraint of an initial value this all works.

Brian Slesinsky

unread,
Mar 23, 2015, 1:15:49 PM3/23/15
to elm-d...@googlegroups.com
I think we could redefine it in a way that makes sense:

A stream has a value as soon as it receives its first event. Its value
is always the most recently received event.

The output of map2 has a value as soon as both its inputs have
received an event.

The key to making this coherent is that streams can only be observed
via events. So, while it's true that some streams are defined later
than others, you can't observe them before their first event.
Therefore the Maybe type is unnecessary.

There is an issue for I/O, since you want the display to have a value
as soon as the program starts. but this can be done by merging the
stream with an onStartup stream.

In mathematical terms, this is taking a constructivist philosophy.
Instead of having a platonic ideal of a value that exists but cannot
be observed, we limit ourselves to what can be calculated. I think
this fits well with programming. For example, the value of a function
call doesn't really exist in a program until it's evaluated. But there
is no trouble substituting a function call with its value because it
will always be consistent, once evaluated.

From an implementation point of view, streams defined this way will
require "push" rather than "pull". We don't even create the stream
until there is an event for it. It is lazy but in a different way from
Haskell which is pull-based.

I think this might even allow for dynamically created streams. For
instance we can define a function that defines a stream, given an id,
but only creates it on demand. Reducing startup time by creating
streams dynamically seems like a good implementation technique?

- Brian

Richard Feldman

unread,
Mar 23, 2015, 1:29:02 PM3/23/15
to elm-d...@googlegroups.com, br...@slesinsky.org
One more that's been mentioned elsewhere but hasn't been done up in this format is Timeline. Personally this is my frontrunner.

Stream + Timeline:
  • Pro: Timeline has the word "time" right there in it, and explaining it as "a value that changes over time" is a good fit for the word Timeline. Saying "merging the timelines" or "mapping over several timelines" or even "folding over a timeline|" sounds like a good way to talk about what's happening in those cases.
  • Con: Timeline is longer than both Signal and Varying.
Here's what the OP looks like in this world:

type alias Port =
    { address : Address a
    , stream : Stream a
    }

subscribe : a -> Stream a -> Timeline a
 -- give a stream of events an initial value and it
 -- can become a timeline

Timeline.destructure : Timeline a -> (a, Stream a)

Hassan Hayat

unread,
Mar 23, 2015, 2:02:50 PM3/23/15
to elm-d...@googlegroups.com, br...@slesinsky.org
I already said that I like Timeline but I'd like to precise why I like it.

Varying implies that the value is changing over time, which is slightly odd to say that value changes.

Timeline implies that we're dealing with a list of discrete values that span across time. Thinking of it as a list makes it more natural to work with functions like "map" and "fold", etc...

I mean, this is the whole point of FRP, really (recognizing the equivalence of signals and lists), but this explicits it. It's like you're pushed to thinking of it as a list without requiring an explanatory paragraph about how you need to rethink stuff or whatever. This say, it's just a list. Nothing to it. The only difference is that you can't flatten it cuz... how can you have a discrete event that itself stretches across time and then have a list that stretches across time of events that stretch across time??? Maybe it would make sense in the Dr. Who world or the Marty McFly world, but not in this one.

Brian Slesinsky

unread,
Mar 23, 2015, 2:51:05 PM3/23/15
to elm-d...@googlegroups.com
I like the name as well. It also implies a connection to time-series
databases, which are essentially a way to record a timeline to stable
storage.

I don't see any fundamental reason why Timelines all have to start at
the same time, particularly when we move beyond dealing with the
larger world where there is more than a single program at a time.
Startup time doesn't seem particularly special. So perhaps there
should *just* be Timelines, they replace both Signals and Streams, and
they each have a start time, which may be in the future?

Each Elm program can have a timeline with major events, such as when
it's first invoked, when the display is first rendered, and when it's
destroyed. This timeline is defined while the program is running by
definition (and perhaps afterwards, from another program's point of
view.) Tasks should have timelines as well.

A big difference between lists and Timelines is push versus pull. We
can have an infinite list and evaluate it whenever needed using lazy
evaluation. With a timeline this doesn't work since its tail is in the
future. Instead we react to new items as they arrive.

Perhaps the simplest operation for a Timeline would be taking an
existing Timeline and combining it with a new value at present. You
can't define the future or the past, but you can define the present.
This is basically reacting to an event by emitting another one.

(I probably should have read more about the theory of FRP before
getting into this.)

- Brian

Hassan Hayat

unread,
Mar 23, 2015, 3:18:38 PM3/23/15
to elm-d...@googlegroups.com, br...@slesinsky.org
A big difference between lists and Timelines is push versus pull. We 
can have an infinite list and evaluate it whenever needed using lazy 
evaluation. With a timeline this doesn't work since its tail is in the 
future. Instead we react to new items as they arrive.

That did give me a thought. I think where we're trying to get at with the whole Signal vs Streams thing is that there's this need for an initial value (or not if you deal in maybes).

But this problem doesn't arise with lists. You don't need to have a full list to have a usable list. Why is that? Cuz Lists (as linked lists) have the concept of an end of list (Nil if you like). 

What if instead we thought of a timeline/signal/stram as like a backwards list? Like, it has a beginning of the list and then more elements. I don't know that you can pen down such a data structure using algebraic data types, but we could certainly think of it in the abstract. We could attach a timestamp to that beginning of list thing if we wished to know when it was created. But, there would be no end of list. Like, there's a beginning of time but there's no end of time (that would be depressing).



(I probably should have read more about the theory of FRP before 
getting into this.) 

Yeah, I think we all should've. ;)

Brian Slesinsky

unread,
Mar 23, 2015, 4:14:30 PM3/23/15
to elm-d...@googlegroups.com
On Mon, Mar 23, 2015 at 12:18 PM, Hassan Hayat <hassan...@gmail.com> wrote:
> What if instead we thought of a timeline/signal/stram as like a backwards
> list? Like, it has a beginning of the list and then more elements. I don't
> know that you can pen down such a data structure using algebraic data types,
> but we could certainly think of it in the abstract. We could attach a
> timestamp to that beginning of list thing if we wished to know when it was
> created. But, there would be no end of list. Like, there's a beginning of
> time but there's no end of time (that would be depressing).

Well, it's more like we don't know where the end is, since we haven't
observed it yet. But I don't think there's anything wrong with having
an "end of Timeline" marker so that you know that no more data is
coming, and the subscriptions to the Timeline can be collected.

Really, all you have is the present. If you want to save the past then
you need to store it in a state machine.

And we can go further than that: even the present cannot be observed
unless there is an event. A timeline where no events happen is
unobservable unless we combine it with events from some other source.

When we write code using Timelines, it's really about subscriptions to
future events; it's metadata about what we've prepared to observe.

- Brian

Hassan Hayat

unread,
Mar 23, 2015, 6:24:10 PM3/23/15
to elm-d...@googlegroups.com, br...@slesinsky.org
But I don't think there's anything wrong with having 
an "end of Timeline" marker so that you know that no more data is 
coming, and the subscriptions to the Timeline can be collected. 

If my understanding of FRP is right, an "end of Timeline" would be problematic. It's that thing about "higher order FRP" that Evan talks about. If you can "end" a signal, then you can "restart" a signal afterwards. So, you get a fresh signal, and that breaks referential transparency. (As, in, a counter that ends at 5, and then when it restarts, it restarts at 0. Unless you wish to restart it at 5, in which case you'd need to keep track of all those signals) This means you could define flatten on signals and that's weird. 

Again, this is my limited understanding of FRP, so, take it with a bucketful of salt :D



Really, all you have is the present.

Ah, this is so true :D

With all this FRP stuff, it's hard not to bask in philosophy. Does time start? Does time end? Is time discrete or continuous? Does time require and initial value? Can discrete events themselves be timelines?
 

Kaspar Emanuel

unread,
Mar 23, 2015, 9:40:59 PM3/23/15
to elm-d...@googlegroups.com

On 23 March 2015 at 12:24, Jeff Smits <jeff....@gmail.com> wrote:

In representation of a Signal might considered be a special case Stream that has an initial value at program start, but the semantics of a Signal are quite different. It's not (just*) a stream of events, it's a value that changes over time in a discrete manner, therefore there is always a value, therefore higher arity maps are possible. This is not the case for a normal Stream and makes it worthwhile to distinguish between them in the type system IMO.

* The Signal we're familiar with keeps the event information of it's value changes, so it's both an event stream and a value varying over time. I suspect that's the idea of Stream/Varying, Stream is just the events, Varying is just the value over time where you cannot have an observable event as long as the value doesn't change, whereas with Signal you can.

Ah thanks, with this explanation and Evan’s new diagrams posted in another thread I can see where my thinking was wrong in a way. My Stream type was more of base of Signal/Varying that didn’t know anything about changes, just a constantly updating sample so to speak.

So what about going the other way and actually encoding type alias Stream a = Varying (Maybe a). Would it be useful?

Re: Timeline, I don’t like it because it suggests a fixed number of events somehow. But I am totally over the naming discussion anyway. Call it whatever you like, I can adapt. :)

Brian Slesinsky

unread,
Mar 23, 2015, 10:23:15 PM3/23/15
to elm-d...@googlegroups.com
On Mon, Mar 23, 2015 at 3:24 PM, Hassan Hayat <hassan...@gmail.com> wrote:
> If my understanding of FRP is right, an "end of Timeline" would be
> problematic. It's that thing about "higher order FRP" that Evan talks about.
> If you can "end" a signal, then you can "restart" a signal afterwards. So,
> you get a fresh signal, and that breaks referential transparency. (As, in, a
> counter that ends at 5, and then when it restarts, it restarts at 0. Unless
> you wish to restart it at 5, in which case you'd need to keep track of all
> those signals) This means you could define flatten on signals and that's
> weird.

There was something about modeling things that have lifecycles; i'll
have to re-read.

But I was thinking of something simpler. It seems coherent to define a
clock that strikes at noon today and never again. Its value is just
the most recent event. Knowing that no further events are coming seems
like an optimization that helps garbage collection. Like GC it
shouldn't have an observable effect, but the runtime can clean up
after proving that the timeline will never be observed again.

- Brian

Jeff Smits

unread,
Mar 24, 2015, 3:12:17 AM3/24/15
to elm-discuss
The way I imagine Stream and Varying to be*, expressing one in the other is difficult. It's more like Signal a = (Stream (), Varying a) where the stream gives an event whenever an "update" to the Signal occurs and the varying is just the value of the signal, where the updates without a value change are invisible. 

* This is my interpretation, it's never been clearly defined to be so. But it's the only way I can imagine that Varying differs from Signal. But the current implementation of varyingToStream seems to indicate Varying is just a plain old Signal
If this is really supposed to be the case, then yes
type alias Stream a = Signal (Maybe a)
can work as the signal will be Nothing until the first event from the stream and then have updates of Just (value of the stream event)

--

Brian Slesinsky

unread,
Mar 24, 2015, 11:59:47 AM3/24/15
to elm-d...@googlegroups.com
On Mon, Mar 23, 2015 at 7:22 PM, Brian Slesinsky <br...@slesinsky.org> wrote:
> On Mon, Mar 23, 2015 at 3:24 PM, Hassan Hayat <hassan...@gmail.com> wrote:
>> If my understanding of FRP is right, an "end of Timeline" would be
>> problematic. It's that thing about "higher order FRP" that Evan talks about.
>> If you can "end" a signal, then you can "restart" a signal afterwards. So,
>> you get a fresh signal, and that breaks referential transparency. (As, in, a
>> counter that ends at 5, and then when it restarts, it restarts at 0. Unless
>> you wish to restart it at 5, in which case you'd need to keep track of all
>> those signals) This means you could define flatten on signals and that's
>> weird.
>
> There was something about modeling things that have lifecycles; i'll
> have to re-read.

I haven't re-read yet but I remember what the issue was. You can't
dynamically create a stream that depends on the past. A stream based
on a state machines is a way of remembering history. Since all events
must be received in the present, if you want to remember history, you
need to arrange for it to be remembered in advance. Also, if you close
a stateful stream then you've forgotten it forever; this can only be
done if we're sure it's unobserved.

It seems like there should be no trouble creating new streams that
don't depend on the past, though? In particular, a clock doesn't
depend on history, or a stateful stream that depends only on present
and future events. Perhaps this distinction would have to be baked
into the system somehow, with a way to "slice" a stream at the present
and create another stream that depends only on its present and future
events.

Evan Czaplicki

unread,
Mar 24, 2015, 1:04:07 PM3/24/15
to elm-d...@googlegroups.com
Jeff, you are correct that (Signal a == Varying a) and that (Signal (Maybe a) == Stream a) where it starts Nothing and then is Just on every update.

Nothing is really changing here, we're just making APIs a bit more precise about how they should be used and making it so we can leave off default values sometimes.

For discussions of starting and stopping signals, see this presentation. That's a thing, there's research on it, it's not so simple and it's not a slam dunk kind of feature.

Alexey Zlobin

unread,
Mar 24, 2015, 4:27:00 PM3/24/15
to elm-d...@googlegroups.com
May be I'm missing something. But for Signals we have proper milti-argument maps and no multi-argument merge, while for Streams the situation is opposite. (Signal (Maybe a) == Stream a) is a clever representation trick which only confuses users?

PS If we want a new word for Signal why don't pick "Behavior"? It consistent at least with something.

Kaspar Emanuel

unread,
Mar 24, 2015, 5:54:05 PM3/24/15
to elm-d...@googlegroups.com

On 24 March 2015 at 07:11, Jeff Smits <jeff....@gmail.com> wrote:

This is my interpretation, it's never been clearly defined to be so. But it's the only way I can imagine that Varying differs from Signal. But the current implementation of varyingToStream seems to indicate Varying is just a plain old Signal
If this is really supposed to be the case, then yes
type alias Stream a = Signal (Maybe a)
can work as the signal will be Nothing until the first event from the stream and then have updates of Just (value of the stream event).

I think this is where the implementation differs from the concept in a way and may be what had led me on the wrong path as well. I think the value of Stream is conceptually Nothing after it has passed through the graph until it receives another value. The implementation holds on to the value as it is likely read several times while being processed and there is no point in discarding it (because it cannot be read after that). It is quite confusing I must say. Please do correct me if I am wrong about any of this.

On 24 March 2015 at 17:03, Evan Czaplicki <eva...@gmail.com> wrote:

Jeff, you are correct that (Signal a == Varying a) and that (Signal (Maybe a) == Stream a) where it starts Nothing and then is Just on every update.

But is it Nothing just after an update? Otherwise I don't see the problem with the map arity. I thought the problem was the result of a map2 would only be Just if events on both Streams happen to coincide (basically never).

Either way, what do you think about defining it as (Varying (Maybe a)) on the type level in Elm? Wouldn’t it make the from/to conversions clearer? I kind of like it as an explanation but I have no idea if it’s useful in practice and have yet to use Stream/Varying in anger.

John Mayer

unread,
Mar 24, 2015, 6:45:37 PM3/24/15
to elm-d...@googlegroups.com
I'm always afraid of replying to a threadnaught, but here goes. I think we're all set but I'd like to drive the nail into this coffin. Sorry, this is super long. I'll probably copy this to a gist.

There's real value in having separate concepts. Varying and Stream play distinct roles, despite their technical similarity. Within a well-architected program, in any given scenario there should be no ambiguity or guessing in the programmer's choice between a Varying or a Stream; put another way, the use-cases of Varying and Stream fall into two clearly delineated buckets.

Main (the display) and Model are great examples of things that are clearly Varying. In a UI, you always display something, including program start. And what is on your screen changes over time.

In my view, anything that can be described as the dynamic state of the program should be a Varying. But why? Remember, it is very nice and convenient to be able to forget that it's all events under the hood. Philosophically, that's what FRP is all about - control flow for events. Don't lose sight of a major rasion d'être of Elm: callbacks suck, so we choose instead to reason about values that vary of time. That's what state is. The program always has a state, and I think it's healthy to think of the state of the program as equivalent to the combined set of all of the Varying values of the program.

Another good example of Varying is the mouse pointer; it's always somewhere. It is damn convenient to reason about the position of the mouse as something that always has a value which varies, rather than a bunch of "updates".

But speaking of the mouse, clicks is the perfect example of why Stream is important. It makes no freaking sense to call mouse clicks a "value that changes over time". Honestly, it's just something we put up with this whole time, content with Signals' cool duality, but hand waving away the fact that Signal () makes no sense. "It's unit, that varies over time, but only ever has this one value...." Nope. Nonsense design. (Ok it was pretty handy and allowed some neat tricks, but it was definitely smelly! And it was always used as if it were a Stream, because the starting value has always been ignored in foldp. More on foldp later)

Lots of stuff just doesn't fit the old Signal (and the new Varying) model - events that keeps happening over time. Stream is appropriate when it is very inconvenient to forget that it's all events under the hood. If a concept is not naturally representing some concept in your program that "always is", then it's going to be difficult to reason about it as a value as that varies over time, and you've probably got a stream on your hands.

A well-architected program will have a backbone of Varying values, transformed and combined from top to bottom using single- and multi-arity maps. Along the way you incorporate events by creating new varyings with Stream and foldp and combining it into the backbone (To experience the essence, as an exercise to the reader, convert a Stream and an initial value into a Varying using foldp)

As a side note, this explains why we don't have a merge for Varying, and why we don't have higher-arity maps for Stream. All values that are Varying always exist, and we're abstracting and forgetting that they are events under the hood, so merging them doesn't really make sense, semantically. On the other hand, all Streams are probably triggered by distinct external sources, so they should never fire simultaneously (if they do, bad architecture!), thus combining them with a function doesn't make much sense, semantically, either.

Now here's the crazy part: I think it's an anti-pattern to convert a Varying back into a Stream (with the single exception of sampleOn). I'd be really interested to see those examples where folks had trouble, constantly converting back-and-forth, because it's probably a sign that the whole program could be organized in a more elegant way. This is just a hunch and not backed by data, so I'd like to explore this more.

Anyway, that's how I've been thinking about all this :-)

Raoul Duke

unread,
Mar 24, 2015, 6:47:50 PM3/24/15
to elm-discuss
> Don't lose sight of a major rasion d'être of Elm: callbacks suck, so we
> choose instead to reason about values that vary of time. That's what state
> is. The program always has a state, and I think it's healthy to think of the
> state of the program as equivalent to the combined set of all of the Varying
> values of the program.

http://intendtogether.pressible.org/intendtogether/why-not-events-awelon-blue

Raoul Duke

unread,
Mar 24, 2015, 6:49:59 PM3/24/15
to elm-discuss

Evan Czaplicki

unread,
Mar 24, 2015, 7:06:20 PM3/24/15
to elm-d...@googlegroups.com
Very strongly agree with John's points! I had a few use-cases in mind where you might want to produce streams:
  • Maybe your state depends on Window.dimensions. So you want to put the initial dimensions in your initial state, and pipe the resizes in as an event stream.
  • The type held in a foldp may be something like (Varying (Model, Maybe Effect)) so you are not only updating your application state, but also producing maybe some effect you want to run. I had imagined splitting this off into (Varying Model) and (Stream Effect) and piping them to the appropriate top-level declaration. I guess the pattern is "there's something that is not always defined"
Maybe it's weird that these cases exist? Maybe there are other ways to think of them?

Raoul, I don't think that link is super relevant, but I could be wrong. The author seems to be talking about systems that allow arbitrary reconfiguration (actors, message-passing concurrency, event listeners, etc.) which is not what we are dealing with here. I agree that those cases are difficult to reason about. I think it's best to respond to John's point more directly.

Raoul Duke

unread,
Mar 24, 2015, 7:08:59 PM3/24/15
to elm-discuss
> Raoul, I don't think that link is super relevant, but I could be wrong.

Sorry to be obtuse. I was attempting to agree that when possible it is
nice to get away from dealing with events and callbacks, and that
state is important.

Dobes Vandermeer

unread,
Mar 24, 2015, 7:12:14 PM3/24/15
to elm-d...@googlegroups.com
I think it's interesting to come back to this, since it was a fundamental distinction in the original FRP that Evan seemingly deliberately eliminated in Elm, for better or worse.

I think although conceptually they are distinct, I thought the merging of the two bought something.  Or at least I thought so, I'm a bit fuzzy on exactly what.

In Conal's push-pull FRP paper and some later ones they distinguish between three types of signals:
  1. Continuous signals that change all the time (e.g. mathematical functions of time)
  2. Continuous step-function signals (signals that change in response to events, or using some logic)
  3. Events
The reason being that continuous signals should always be sampled in a "pull" fashion - (i.e. polling using something like sampleOn). whereas the second can benefit from memoization/caching and can "push" when a change is detected.  And the final kind of signal only changes discretely in response to events.

This breakdown between push and pull mainly has to do with getting acceptable performance out of FRP programs.  Every variation of FRP that isn't "run the whole program every tick with the full list of events since launch" is basically trying to improve performance over that original concept.

Brian Slesinsky

unread,
Mar 24, 2015, 7:19:57 PM3/24/15
to elm-d...@googlegroups.com
This is a great explanation, and probably should be added to the documentation.
> You received this message because you are subscribed to a topic in the
> Google Groups "Elm Discuss" group.
> To unsubscribe from this topic, visit
> https://groups.google.com/d/topic/elm-discuss/w2Rmim4IUn4/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to

Evan Czaplicki

unread,
Mar 24, 2015, 7:20:28 PM3/24/15
to elm-d...@googlegroups.com
I can address the second example I mentioned like this:

fancyFold : (a -> s -> (s, Maybe b)) -> s -> Stream a -> (Varying s, Stream b)

This is essentially hiding the conversion from Varying to Stream, but maybe it lets us tell a better story about what is going on with Stream vs Varying.

John Mayer

unread,
Mar 24, 2015, 7:40:42 PM3/24/15
to elm-d...@googlegroups.com
Or perhaps:

sampleOnWith : (a -> b -> c) -> Varying a -> Stream b -> Stream c

sometimesSampleOnWith: (a -> b -> Maybe c) -> Varying a -> Stream b -> Stream c

--

Hassan Hayat

unread,
Mar 24, 2015, 7:43:33 PM3/24/15
to elm-d...@googlegroups.com
I think although conceptually they are distinct, I thought the merging of the two bought something.  Or at least I thought so, I'm a bit fuzzy on exactly what.

I think this is a point John brings out nicely.  

There's real value in having separate concepts. Varying and Stream play distinct roles, despite their technical similarity.

True, there's no such thing as a pure pulsating signal. It's all events. Some signals "update" whenever a timer fires, others whenever a button is pressed, and others respond to some random event like an Http request that was completed.  

I think that Streams make most sense when it comes to Tasks. Tasks are basically events/callbacks dressed in monadic attire. If instead we were to make tasks send messages to Varyings/Signals, it would be weird. I mean, I imagine Varyings as like this train that's just going from one stop to the other and I can't really stop it. For me, it's a thing that just updates and I don't want to be trying to control those values as they come. I want to react to them (oddly enough). 

On the other hand, Tasks (and by association, Streams), I do want to have granular control. These are the actual commands that will do stuff in the system (connect to the database, send http requests, etc...). It makes sense to say that "I've requested data from a database and I have received a stream of data". I don't want to say that I got a "Varying" of data from a database. I hope that data does not "vary" on it's way here. The same applies to Signal. 

Basically, the last thing you want is to model one-off actions and requests as Signals. It doesn't fit in the explanation with the graph of the keyboard values that change with time. In a sense, these one-off tasks return a stream of data. That makes sense (to me at least).

So, I think John makes a strong case when he says that we should keep the distinction between Stream and Varying. I think there's value to being able to convert back and forth but let's put a warning like "use sparingly". 

I think we might be going nowhere if we just continue to discuss exact terminology and details for too long. Let's try to focus on how we would explain this to people and communicate these ideas. Maybe in doing so we'll come up with better words than "Varying" or "Stream" or even a simpler way of describing the two concepts. Cuz, to a certain extent we're kinda discussing these things blindly otherwise. 



To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Kaspar Emanuel

unread,
Mar 24, 2015, 8:11:55 PM3/24/15
to elm-d...@googlegroups.com
That is some very fascinating analysis and discussion and a lot to think about.

Evan, I am still genuinely curious about my guesses/questions regarding the implementation details if you do find some time to skim this thread again.


Reply all
Reply to author
Forward
0 new messages