Signal represents collection of values (over time) associated with a single identity. It can be expressed as follows:
function signal(next) {
next(1)
next(2)
next(3)
// ...
}
Signals in haskell and Elm have no notion of end that implies lot's of technical constraints at the implementation level. Also while this may work in pure languages it's very unnatural for language like JS and highly mutable environment it's running in. There for this definition of signal has notion of end. Finite signal can be expressed as follows:
function signal(next, end) {
next(1)
next(2)
next(3)
end()
}
Since signals may be used to represent IO it's quite possible to have race
conditions. If signal ends with an argument (different from null
and
undefined
) it's treated as an error:
function signal(next, end) {
next(1)
end(Error("oops!"))
}
Inovking next
or end
after end
was invoked is forbidden. Signals
that do so are considered broken, although runtime enforcement of this
is out of the scope of this definition. Preferably such enforcments can
and will be done as a second tire utilities:
function normalize(signal, strict) {
return function normalized(next, end) {
var ended = false
signal(function forwardNext(value) {
if (ended) {
if (strict) throw Error("Can not yield values from ended signal")
else console.warn("Can not yield values from ended signal", signal, value)
} else {
return next(value)
}
}, function forwardEnd(error) {
if (ended) {
if (strict) throw Error("Can not end signal more than once")
else console.warn("Can not end signal more than once", signal, value)
}
})
}
}
There is no single right choice when it comes to choosing between push & pull style streaming. They both have pros & cons and depending on problem scope diffirent choice makes it better fit. Push style streams can be a lot more efficent since they don't require chopping at each transformation. On the other hand reading just a part of stream, pausing & resuming than again is a lot harder with push style streams, specilly in pure functional style.
This definition of signal attempts to take hybrid approach. It favors push
style for the efficency, but lets consumer downgrade to pull style. Unlike
linked lists of head, tail
pairs, there are no predifined chunks. Signal
starts pushing values, but consumer can signal it to return rest in form
of other signal:
function range(from, to) {
return signal(next, end) {
var value = from
while (value < to) {
var continuation = next(value)
value = value + 1
// If consumer returns continuation function,
// rest of the signal should be passed to it
// and rest should be cleaned up.
if (typeof(continuation) === "function")
return continuation(range(value, to))
}
end()
}
}
In a way this is similar to pull streams with a difference that size of chunks is dictated by consumer instead of making chunk size of value.
function map(f, signal) {
return function mapped(next, end) {
function rest(continuation) {
continuation(map(f, signal))
}
signal(function(value) {
var continuation = next(value)
return typeof(continuation) ? rest(continuation) :
continuation
}, end)
}
}
The above is a correct implementation of map. However it's weird because
rest
takes the sinks in the wrong order which is messed up.