Lecture 25 — 2015-12-07

π Calculus

This lecture is written in literate Haskell; you can download the raw source.

The π calculus is, like the λ calculus, a foundational model of computation. We briefly looked at its syntax and semantics. I write “bar x” for x with a bar over it, as in class.

P,Q,R ::=
  x(y) . P        receive y on x
  bar x <y> . P   send y over x
  P | Q           run concurrently
  (nu x) P        fresh channel
  !P              repeat infinitely
  0               terminate

First, we defined a set of equational rules, just like the λ calculus.

We had a correlate of α equivalence:

(nu x) P = (nu y) P[y/x] for fresh y

Commutativity, associativity, and the 0 identity for concurrency:

P|Q = Q|P

(P|Q)|R = P|(Q|R)

P|0 = P

We interpret the “repeat infinitely” rule via the following rule:

!P = P | !P

Note that it can apply infinitely often:

!P = P | !P = P | P | !P = P | P | P | ... | !P

A binding collection rule, the 0 identity for binding:

(nu x)(nu y) P = (nu y)(nu x) P

(nu x)0 = 0

The following slightly strange rule allows us to change the scope of fresh channels. These rearrangements can be vital for applying reduction rules.

(nu x)(P|Q) = (nu x) P | Q   if x not in Q

With these equational rules out of the way, we can define the rewriting rule --> in Term x Term. The central, most important rule is the following one, where a process that wants to send meets one that wants to receive, and a message is actually sent:

bar x <z>.P | x(y).Q --> P | Q[z/y]

This rule is synchronous: one process must be waiting to write while the other waits to read. There are asynchronous π calculi, but we won’t look at them.

There are also three congruence rules. The first two are straightforward:

P --> Q
P|R --> Q|R

P --> Q
(nu x)P --> (nu x) Q

The last rule is a slightly odd one, making a formal connection between the equalities we defined above and the rewrite system.

P = P'    Q = Q'    P' --> Q'
P --> Q

Why bother with this rule? It lets us use the equational theory to, for example, expose two processes to the send/receive rule aboe.

We briefly saw some encodings into the π calculus: bools and natural numbers.

Our first cut at bools looked like:

true b = b(t).b(f). bar t <t>
false b = b(t).b(f). bar f <f>

test b = (nu t)(nu f). bar b<t>. bar b<f>. | 
          t(t). print "true" | 
          f(t). print "false" 

Note that I leave .0 off the end. The idea here was just like the Church booleans: given a choice of two things, true ‘means’ doing one while false ‘means’ doing the other. Note that it doesn’t really matter what we send, but just that we merely sent something. If we then run:

(nu b). false b | test b

This code will printf false. What happens if we run the following?

(nu b). false b | test b | test b

We might expect that false gets printed twice, but it will only print once: false b only sends one value down its false channel, not two. We can therefore refactor our bools into:

true b = b(t).b(f). !(bar t <t>)
false b = b(t).b(f). !(bar f <f>)

Now, true and false send on the appropriate channel infinitely, allowing for more than one test.

Numbers are less standard, but we can write our own version. We’ll follow the general pattern of the Church numerals: a number n sends on a given channel n times. The π calculus presents a difficulty here: it’s a little tricky to observe something sending 0 times. So we’ll model numbers as processes that respond to two messages: one asking whether or not the given number is zero, the other sending n messages.

Here’s zero:

zero c = c(iZ). c(r). !(iZ(x). x true) | !(r(x). 0)

The number zero on the channel c first gets two channels, iZ and r, over which the “are you zero?” and “what’s your number?” messages are sent, respectively. We respond with true to the first and do nothing in response to the latter.

What does the number one look like? Well, it should respond false to “Are you zero?” and when given a channel to send on, it should send just the once.

one c = c(iZ). c(r). !(iZ(x). x false) | !(r(x). bar x<x>)

What about two?

two c = c(iZ). c(r). !(iZ(x). x false) | !(r(x). bar x<x>. bar x<x>)

Okay, let’s write successor:

succ c n = 
  c(iZ). c(r). 
    !(iZ(x). x false) | 
    !(r(x). bar x<x>. 
            (nu d).(nu iZ').(nu r'). 
              (n d | bar d <iZ'>.bar d <r'>. bar r' <x>))

First, we can always return false when asked if we’re zero. Second, we can respond to the message “what’s your number?” by sending one value on the given channel x and then telling n to say its number on the same channel.