Skip to content
This repository has been archived by the owner on Oct 24, 2023. It is now read-only.

Channels for communicating sequential processes #10

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open

Conversation

mstade
Copy link
Owner

@mstade mstade commented Apr 22, 2014

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
Copy link
Owner Author

mstade commented Apr 22, 2014

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.

@@ -0,0 +1,4 @@
module.exports = chan

function chan() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking forward to this one :)

@mstade
Copy link
Owner Author

mstade commented Jul 26, 2014

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
Copy link
Owner Author

mstade commented Jul 27, 2014

@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
Copy link
Owner Author

mstade commented Jul 27, 2014

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
Copy link
Collaborator

sammyt commented Jul 27, 2014

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
Copy link
Collaborator

sammyt commented Jul 27, 2014

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
Copy link
Owner Author

mstade commented Jul 27, 2014

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
Copy link
Owner Author

mstade commented Jul 27, 2014

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

@mstade mstade force-pushed the chan branch 3 times, most recently from a182851 to ff805e8 Compare October 13, 2014 07:50
@mstade mstade mentioned this pull request Oct 13, 2014
@mstade mstade force-pushed the chan branch 2 times, most recently from d68c398 to 4709d45 Compare October 15, 2014 00:52
@coveralls
Copy link

Coverage Status

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

mstade and others added 14 commits January 31, 2015 14:41
`defprop` is used to define properties on objects, and is the king of
mutability. It's useful though for low level mechanics in implementing
abstractions, and provides a cleaner API than `Object.defineProperties`.
`is(x, x)` could return false if `x` is an object
N.b.: If you do `Object.create(function() {}, {})`, the resulting object
will have a constructor set, but not to the prototype function and I have
no idea what indeed it gets set to. Might be worth investigating.
Hint: there is no coverage of `defprotocol`. Write some tests, man.
Damn gamification makes me want to achieve 100% coverage. I'm so easy..
This should probably change to something better, such as a **/*.api.js or
something such that it's possible to have files explicitly marked public
and others that would be considered private.
@coveralls
Copy link

Coverage Status

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

@mstade
Copy link
Owner Author

mstade commented Jan 31, 2015

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

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.44%) to 99.56% when pulling 7101635 on chan into db9c055 on master.

@mstade
Copy link
Owner Author

mstade commented Feb 7, 2015

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants