Why are two bidirectional channels needed to implement the server protocol for a cell?

59 views Asked by At

It may seem odd that two bidirectional channels are needed to implement the protocol. Couldn't we use just a single channel and change the server loop to give and take on that single channel. Note that this is allowed in Hopac and poses no problem. A job cannot send itself a message using a channel in a single synchronous operation. Explain what would go wrong if there was only one channel instead of separate getCh and putCh channels. Hint: Consider a situation with multiple clients.

The above is from the Hopac doc.

Are the new users really expected to understand the finer points of library implementation at this point in the journey? I prefer it when the documentation gives answers rather than asks questions. What is the reason why using a single channel for both take and get is a bad idea?

module HopacExample

open System
open Hopac
open Hopac.Infixes

type Cell<'a> = {
    takeCh : Ch<'a>
    putCh : Ch<'a>
    }

let get c = Ch.take c.takeCh
let put c (x: 'a) = Ch.give c.putCh x

let cell x = Job.delay <| fun () ->
    let c = {takeCh = Ch (); putCh = Ch ()}
    let server x = 
        Alt.choose [
            Ch.take c.putCh
            Ch.give c.takeCh x ^->. x]

    Job.iterateServer x server >>-. c

run <| job {
    let! c = cell 1
    let print () = Job.start (get c >>- fun i -> printf "%i\n" i)
    let put i = Job.start (put c i)
    do! print()
    do! put 2
    do! print()
    do! put 3
    do! print()
    do! put 4
    do! print()
    do! put 5
    do! print()
    do! put 6
    do! print()
    }

Console.ReadKey()
2
3
4
4
5
6
module HopacExample2

open System
open Hopac
open Hopac.Infixes

let get c = Ch.take c
let put c (x: 'a) = Ch.give c x

let cell x = Job.delay <| fun () ->
    let c = Ch ()
    let server x = 
        Alt.choose [
            Ch.take c
            Ch.give c x ^->. x]

    Job.iterateServer x server >>-. c

run <| job {
    let! c = cell 1
    let print () = Job.start (get c >>- fun i -> printf "%i\n" i)
    let put i = Job.start (put c i)
    do! print()
    do! put 2
    do! print()
    do! put 3
    do! print()
    do! put 4
    do! print()
    do! put 5
    do! print()
    do! put 6
    do! print()
    }

Console.ReadKey()
5
5
5
5
5
6

There are some differences, but nothing I would not attribute to vagaries of concurrency.

1

There are 1 answers

0
Marko Grdinić On

The reason why you need two channels is to force the users to go through the server. You'd be able to tell what the problem with using just a single channel is if you removed the server entirely. With just a single channel, the clients might bypass the server and communicate to each other directly.

module HopacExample2

open System
open Hopac
open Hopac.Infixes

let get c = Ch.take c
let put c (x: 'a) = Ch.give c x

let cell x = 
    let c = Ch ()
    Job.start (put c x) >>-. c

run <| job {
    let! c = cell 1
    let print () = Job.start (get c >>- fun i -> printf "%i\n" i)
    let put i = Job.start (put c i)
    for i=2 to 6 do
        do! print()
        do! put i
    }

Console.ReadKey()