Skip to content

Instantly share code, notes, and snippets.

@Raynos
Forked from Gozala/Readme.md
Created April 4, 2013 21:09
Show Gist options
  • Save Raynos/5314377 to your computer and use it in GitHub Desktop.
Save Raynos/5314377 to your computer and use it in GitHub Desktop.

Signal

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)
  // ...
}

End of signal

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()
}

Valid signal behavior

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)
      }
    })
  }
}

Push & Pull

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.

Transformations

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)
  }
}
@Raynos
Copy link
Author

Raynos commented Apr 4, 2013

// map:: (A -> B) -> Signal<A> -> Signal<B>
function map(lambda) { return function duplex(source) {
    return function signal(next, end) {
        signal(function(value) {
            var sink = next(value)
            return sink ? rest(duplex, sink) : null
        }, end)
    }
} }

// rest :: (Signal<A> -> Signal<B>) -> Sink<B> -> Sink<A>
function rest(duplex, destination) {
    return function sink(signal) { 
        destination(duplex(signal))
    }
}

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment