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.
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 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.
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)
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 : []
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.
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]
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]
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).
- One may
put
something on a channel - One may
take
something from a channel puts
blocktakes
andtakes
blockputs
- 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
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