let link = new Link("link", config, ...)

// We want to write:

link.Send(data)

// But that does not compile. Instead, we must write:

(link :> ILinkOutput).Send(data)

The bad news are that there is no work around. The good news is that this situation does not happen too often in practice, as the creator of a link is in general not the user. New instances are often forwarded to other objects or functions, which will see them as ILinkOutput, thus not requiring a cast:

let register (p : ISender) (l : ILinkOutput) =

p.AddOutputLink(l) // OK: p is an ISender, no need for explicit cast

l.ConnectInputTo(p) // OK: l is an ILinkOutput, no need for explici cast

let link = new Link(...)

let process = new Process(...)

register process link // OK: process and link automatically upcasted

Now, what happens if we have a function taking a process and which needs to use two of its interfaces? There are two obvious solutions, which are not so pretty.

Ugly-ish:

let register (p1 : ISender) (l : Link) (p2 : IReceiver) =

p1.AddOutputLink(l) // Nice

p2.AddInputLink(l) // Nice

(l :> ILinkOutput).ConnectInputTo(p1) // Not so nice

(l :> ILinkInput).ConnectOutputTo(p2) // Not so nice

Uglier:

let register (p1 : ISender) (l1 : ILinkOutput) (l2 : ILinkOutput) (p2 : IReceiver) =

p1.AddOutputLink(l) // Nice

p2.AddInputLink(l) // Nice

l1.ConnectInputTo(p1) // Nice (is it?)

l2.ConnectOutputTo(p2) // Nice (is it?)

// Set up two processes communicating with each other.

// As links are uni-directional, use two links.

let p1 = new Process(...)

let p2 = new Process(...)

let link_p1_p2 = new Link(...)

let link_p2_p1 = new Link(...)

// p1 -> p2

register p1 link_p1_p2 link_p2_p1 p2 // Oops!!!

// p2 -> p1

register p2 link_p2_p1 link_p1_p2 p1 // Oops!!!

So many 1s and 2s... We really meant to write:

// p1 -> p2

register p1 link_p1_p2 link_p1_p2 p2

// p2 -> p1

register p2 link_p2_p1 link_p2_p1 p1

The correct solution uses generics and type constraints:

let register

(p1 : ISender)

(l : 'L

when 'L :> ILinkInput

and 'L :> ILinkOutput)

(p2 : IReceiver) =

p1.AddOutputLink(l)

p2.AddInputLink(l)

l.ConnectInputTo(p1)

l.ConnectOutputTo(p2)

// Set up two processes communicating with each other.

// As links are uni-directional, use two links.

let p1 = new Process(...)

let p2 = new Process(...)

let link_p1_p2 = new Link(...)

let link_p2_p1 = new Link(...)

// p1 -> p2

register p1 link_p1_p2 p2

// p2 -> p1

register p2 link_p2_p1 p1

Another use of type constraints is for cases when you want to pass lists of ISenders to a function.

Problem:

let sendAll (ps : ISender list) data =

ps

|> List.iter (fun p -> p.Send(data))

let p1 = new Process(...)

let p2 = new Process(...)

sendAll [ (p1 :> ISender) ; (p2 :> ISender) ] data // Blah!

Solution:

let sendAll (ps : 'S list when 'S :> ISender) data =

ps

|> List.iter (fun p -> p.Send(data))

let p1 = new Process(...)

let p2 = new Process(...)

sendAll [ p1; p2 ] data

Which can also be written:

let sendAll (ps : #ISender list) data =

ps

|> List.iter (fun p -> p.Send(data))

let p1 = new Process(...)

let p2 = new Process(...)

sendAll [ p1; p2 ] data

## 1 comment:

Nice!

Post a Comment