Wednesday, July 8, 2009

Interfaces made convenient by type constraints

If my previous entry convinced you to make extensive use of interfaces, you may have encountered a problem, namely that calling methods implementing interfaces requires casting:

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