Skip to content

Instantly share code, notes, and snippets.

@TheSeamau5
Created January 31, 2015 02:35
Show Gist options
  • Save TheSeamau5/3d22923fe729a2385f42 to your computer and use it in GitHub Desktop.
Save TheSeamau5/3d22923fe729a2385f42 to your computer and use it in GitHub Desktop.
Investigating CSP for Elm

Communicating Sequential Processes (CSP)

CSP is one of the many ways of reasoning about concurrent systems. It emphasizes on the idea of a channel which enables communication between producers and consumers and where the channel itself is fundamentally decouple from either producers or consumers.

Queues

The easiest way of thinking about a channel is to think about a queue. A queue is very simple, it is a data structure where one may only push data to the end of a queue and may only read data from the front of the queue.

In pseudo-code:

queue = new Queue --empty queue

push 4 queue -- queue now has [4]

push 5 queue -- queue now has [4,5]

read queue -- returns 4, queue now has [5]

push 6 queue -- queue now has [5,6]

Channels are very much like queues. The basic operations on channels are put and take.

In pseudo-code:

channel = new Channel

put 5 channel -- channel has [5]

put 8 channel -- channel has [5,8]

take channel -- returns 5, channel has [8]

put 3 channel -- channel has [8,3]

So, as we can see, channels and queues behave more or less identically. An important part to note is that consumers block producers and producers block consumers. If one writes onto a channel, no one may read from the channel until the write is done. If one reads from a channel, no one may write to a channel until the read is done.

The Decoupling of Channels

The cool thing about channels is that it can be seen as merely a conveyor belt of data. There is no coordination needed here and there is no need for producers or consumers to have any knowledge about where the data came from, where it has been.

All a consumer can do is take a value from a channel and all a consumer can do is put a value into a channel.

The consumers need know nothing about the producers and the consumers. Consumers may be producers and producers may be consumers. Consumers and producers may even be the same objects.

Channels and mutability

In most all implementations of channels, channels are a fundamentally mutable construct. Usually, this is okay because this is not mutability of the bad kind. This is a completely isolated and decoupled version of mutability. Furthermore, the channels do not mutate the data they hold. The only thing mutating are the channels themselves. I include this detail early in the discussion as this is an important topic vis-a-vis Elm, which prides itself in its immutable guarantees.

Furthermore, this makes sense. A conveyor belt gets added luggage and then removed luggage. So, it never has the same items at different points in time.

(Note: as I wrote this last sentence, a lightbulb hit over my head and screeched the word: SIGNAL)

Closing Channels

A channel may be closed. A closed channel is a channel such that one may not put any more values. Picture a checkout desk in a supermarket and the worker is about to leave for the day. Typically, the worker will only accept the clients who are presently waiting at the checkout and will reject any new clients (usually by asking them kindly to checkout at the neighboring desk). This means that one may take from a closed channel but one may not put anything into a closed channel.

channel = new Channel

put 1 channel -- channel : [1]

put 2 channel -- channel : [1,2]

take channel -- 1, channel : [2]

put 3 channel -- channel : [2,3]

close channel -- channel : [2,3] is closed

take channel -- 2, channel : [3]

put 4 channel -- channel : [3]

take channel -- 3, channel : []

take channel -- Nothing, channel : []

Buffered Channels

A channel may optionally have a fixed-size buffer. This means that a channel may only accept a maximum number objects to be put on that channel.

channel = new Channel(5) -- can contain up to 5 objects

This immediately opens the question, what happens one tries to put an object on a full buffer. There are two main strategies, the sliding buffer, and the dropping buffer.

Sliding Buffer

The sliding buffer will drop the old values when newer values are put

channel = new Channel(2, slidingBuffer)

put 4 channel -- [4]

put 5 channel -- [4,5]

put 6 channel -- [5,6]

take channel -- 5, channel : [6]

Dropping Buffer

The dropping buffer will simply not accept (or drop) any newer values.

channel = new Channel(2, droppingBuffer)

put 4 channel -- [4]

put 5 channel -- [4,5]

put 6 channel -- [4,5]

take channel -- 4, channel : [5]

Channels beyond Queues

A channel is actually more than just a queue. One may define a choice operator on channels, which is not possible on queues.

Say that we have want to get an image from flickr, but if it takes too long (say, 1 second) you want to print that the request has timed out.

We may consider the following useful channel.

timeout = new TimeoutChannel(1000)
-- timeout is a channel that just returns after 1000 ms (1 second)

Given this useful channel, one may do the following :

choose [
  timeout,
  put (getImageFromFlickr imageURL) channel
]

choose is a function that takes a bunch of channels and will return only one. choose will return the first channel it can return.

If getting an image from Flickr takes too long, then choose will return timeout, else it will return the channel with the image.

One can then use this as a channel and do all the usual put and take operations on it.

choose may be defined various ways. One may define it to return the result of the "successful" operation from a list of operations, or to return the "successful" async channel from a list of channels.

The main idea of this operator is the idea of choice. One can choose between processes and drop all remaining processes (as if these processes were cancelled).

Summary of the rules of Channels

  • One may put something on a channel
  • One may take something from a channel
  • puts block takes and takes block puts
  • A channel may or may not have a buffer
  • The buffer may be either a sliding buffer or a dropping buffer
  • One may close a channel
  • One may take from a closed channel
  • One may not put onto a closed channel
  • One may choose between operations

Idea on how to implement this is Elm

The problem with implementing this in Elm is the inherent mutability in Channels. While it's a "safe" kind of mutability, it still is mutability and breaks the immutability guarantees of Elm.

As such, one may do something as follows:

create : Channel a

createWithBufferSize : Int -> Channel a

type Buffer = SlidingBuffer | DroppingBuffer

createWithBuffer : Int -> Buffer -> Channel a

put : a -> Signal (Channel a) -> Signal (Channel a)

take : Signal (Channel a) -> Signal (Maybe a, Channel a)

choose : List (Promise (Channel a)) -> Promise (Channel a)

close : Channel a -> Channel a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment