mstade / funkis

Functional JavaScript
MIT License
6 stars 0 forks source link

Channels for communicating sequential processes #10

Open mstade opened 10 years ago

mstade commented 10 years ago

Starting this PR as a WIP on channels. This work will likely also include – or depend on – protocols, multiple/predicate dispatch, and futures.

Channels are a nice way to provide an abstraction for processes communicating with one another. They act a lot like sequences, with the exception that values may not be immediately available. As such, there's a need to find a useful abstraction for future values. Promises as they are specified in ES6 are an over engineered mess, and while they may well be supported as return values for non-blocking channels, they probably shouldn't be the recommended mechanism.

One thing I'd like to accomplish with channels is to make them act largely the same as sequences – that is, they are a view on to a data structure. The main difference however, is that sequences are blocking and immediate, so when realizing a value (i.e. calling rest or first) that value is immediately available. Of course, the value may be a future, meaning the consumer will have to add a function to be called once the future value is available. (Which may be immediately, in which case the future is effectively the same as constantly.) The benefit of this is that function such as take, map, etc. works regardless of what, where, or when the structure is available. To make for nice syntax, something similar to Clojure's go blocks make sense. Here's a contrived mockup:

const tick = timer(5000)
    , log  = console()

go(function *() {
   yield take(1, tick)
   put(log, "Five seconds later, I feel refreshed.")
})

In this example, tick and log are channels that are used in go block which takes care of the boilerplate of dealing with futures returned from yield take(1, tick). Using sequences like this implies that the sequence is fully realized before the execution is resumed. This may be magical, and some care might be required to deal with it. Here's another example, where an infinite channel is used:

const brush = mouse()

go(function *() {
  while(brush.open) {
    const points = yield take(4, brush)
    draw(canvas, bezier(points))
  }
})

The imaginary draw function is used to output pixels on to a canvas (this, as it were, could also be a channel, but I'm choosing to not go the shiny-hammer route just yet) by virtue of taking 4 points and making a Bézier curve out of them.

In both of these mockups, generators are used to suspend execution. Since funkis is largely a research project, I don't think there's much need to limit it to ES5 environments. Should that be the case however, it may be worth looking into something like degenerate.

A benefit to using something like go blocks is that routines can be made into values to pass around and compose. Here's an expansion of the drawing mockup above:

const brush = mouse()

go(comp(curve, noise)(brush))

function * curve(brush) {
  while(brush.open) {
    const points = yield take(4, brush)
    draw(canvas, bezier(points))
  }
}

function * noise(brush) {
  const perturbed = chan()
  yield perturbed

  while (brush.open) {
    var co = yield take(1, brush)
    put(map(co, perturb))
  }

  function perturb(d) {
    return d + random(0, 1)
  }
}

This is a mockup, so there are a bunch of assumptions made (like, what's random, draw etc.?) but it ought to show the general direction in which I hope this work will go. The idea is to have a simple abstraction of a sequence of values, where not only is the underlying data structure not important but neither is the point in time when values are available.

Work in progress, so things are bound to change, but this marks a start.

mstade commented 10 years ago

I feel this needs some expansion:

Using sequences like this implies that the sequence is fully realized before the execution is resumed. This may be magical, and some care might be required to deal with it.

What I mean by this, is that something like yield take(4, chan) (no pun intended) will immediately return a seq. Grabbing first in this case would then yield a future, since chan is a channel. Presuming this is in a go block, the go function would then realize the sequence, and for any future it finds attach a callback to retrieve the value. Once all futures have been realized, the go block would call back to the generator, giving back a new seq with all values that were futures being realized into actual values.

Essentially this means the go block would only resume the generator once all values that were yielded have been realized. In the case a yielded value is a seq, it means realizing the whole seq (bummer if its infinite.) I think this is the only way to do this, while still having the nice syntax of suspending execution.

mstade commented 10 years ago

It might be tricky to implement take/put/filter/map etc in such a way that they are seq/chan agnostic (which is to say, they are sync/async agnostic) but it's worth researching how that can be done.

mstade commented 10 years ago

@sammyt what should happen when you do this?

Seq = defprotocol({
  first : []
, rest : []
})

const nums = [1, 2, 3]

Seq(nums, { // Notice the first argument is the `nums` array, not a "type"
  first: function(arr) { return arr[0] }
, rest: function(arr) { return arr.slice(1) }
})

Seq.first(nums) // 1
Seq.rest(nums) // [2, 3]

Seq.first([3, 2, 1]) // ???

Should defprotocol pick the "type" of the object, (i.e. getPrototypeOf) and associate the protocol implementation to that, or associate the protocol implementation with the actual object itself? I'm inclined to say the latter, because it's less magical, but I don't know.

mstade commented 10 years ago

Or should defprotocol be "clever" enough to recognize non-type objects (can it?) and simply balk at them? A danger with the whole getPrototypeOf story is that if of course that you may just end up defining implementations for things you didn't intent, such as Object meaning now everything will probably match (since Object is the type of everything, mostly.)

sammyt commented 10 years ago

Should defprotocol pick the "type" of the object, (i.e. getPrototypeOf) ... or associate the protocol implementation with the actual object itself?

I also would say the object itself, else to much magic

sammyt commented 10 years ago

How would I define the protocol implementation for the following "data structure"

var Node = function(data, left, right){
  this.data = data
  this.left = left
  this.right = right
}

Such that any new Node(...) had a defined Seq implementation?

mstade commented 10 years ago

Presumably, it'd be something like this:

Seq(Node, {
  first: function(n) { return n.data }
, rest: function(n) { return n.right }
})

var someNode = new Node(1)

Seq.first(someNode) // 1
Seq.rest(someNode) // undefined

When invoking, we'd do an is check on the first argument, which would then see that someNode is an instance of Node and thus match that. If we just use is it'll work nicely with both instances and "types", since it favors strict equality over anything else.

Not sure about using the protocol itself as the implementation mechanism...

If you wanted a tree view out of your node, you could have some syntax like this:

var tree = Seq(someNode)

tree.first() // 1
tree.rest() // undefined

Basically, any protocol given two arguments means implementation; given just the one argument would mean a wrapped instance which is just sugar for Protocol.method(instance, ...) – make sense?

mstade commented 10 years ago

That's a nice and presumptuous implementation that'll let you traverse any tree in any direction, so long as it's right. :o)

coveralls commented 10 years ago

Coverage Status

Coverage decreased (-6.82%) when pulling 4709d455fc58450691f075cbbe30a570f1b05cdc on chan into 2dc3288fca2c67207dcca0690ec7ea460bf58a31 on master.

coveralls commented 9 years ago

Coverage Status

Coverage decreased (-0.44%) to 99.56% when pulling 48b97b1ed4ec68c1290fcfaa4890cd09fbbc78a7 on chan into db9c055546bc9cb00b0d1ff6087daec91970fc52 on master.

mstade commented 9 years ago

Build fails because travis-ci/travis-ci#3108. Change config to use iojs whenever that gets sorted, or change to Wercker or something.

coveralls commented 9 years ago

Coverage Status

Coverage decreased (-0.44%) to 99.56% when pulling 71016355322b28df39543af74c6e2ec8c13a5fa6 on chan into db9c055546bc9cb00b0d1ff6087daec91970fc52 on master.

mstade commented 9 years ago

Travis added support for iojs finally, and so all is suddenly well. :o)