Constraints in FSharp

FSharp   .NET  

Constraints in F# is a really powerful feature, but it is also an area that I think is missing some clear documentation about how to use it. This will be a short write up for future me when I need it, and I also it will help some lost souls out there.

Inline functions

F# will most of the time handle the types for you, also generics when it can do so. A simple example:

let add x y = x + y  

This will when it stands all by itself resolve to the have the type:

val add : x:int -> y:int -> int

That all the compiler can do when it is not provided any more information. If you instead write

let add x y = x + y  
add "Hello " "world"  

add will get the type

val add : x:string -> y:string -> string  

since you on line two specify that you will use it with strings. What should you do if you want to add it with any arguments that supports the + operator? This is where the inline keyword is useful (it can also be used in some optimization scenarios). So let us define the function again and adding the inline keyword:

let inline add x y = x + y  

The type for this version of add is a little bit more complicated:

val inline add :  
  x: ^a -> y: ^b ->  ^c
    when ( ^a or  ^b) : (static member ( + ) :  ^a *  ^b ->  ^c)

What does this mean?

  • x: ^a -> y: ^b -> ^c specify that the function takes two arguments and returns something. The argument has types ^a and ^b and the result have type ^c
  • when ( ^a or ^b) : (static member ( + ) : ^a * ^b -> ^c) is adding a constraint on the types ^a, ^b and ^c. The constraint says that it must exist a static operator + that takes a tuple of type ^a * ^b and returns type ^c.

We got all that by adding the keyword inline, but why? When adding the inline keyword you basically say that whenever you see the function name somewhere in the code replace that name with this function definition. This basically gives you one new implementation of the function every time you use it. If you don't use inline you will only have one implementation and that is why you don't get it as generic as you would like. This is my simplified explanation, someone on the compiler team can probably explain it a lot more in detail.

Constraining on interfaces

I won't go on with you can constrain the types on interfaces because it quite simple and there is more interesting constraint in the next section. I'll just throw the code at you

type ISimpleInterface =  
    abstract member Add: int -> int -> int

type SimpleClassA() =  
    interface ISimpleInterface with
        member this.Add x y = x + y

type SimpleClassB() =  
    interface ISimpleInterface with
        member this.Add x y = x + y

let doSimple<'T when 'T :> ISimpleInterface>  (x: 'T) = x.Add 5 5  
let doSimple2 (x: ISimpleInterface) = x.Add 5 5

let a = new SimpleClassA()  
let b = new SimpleClassB()  
doSimple a  
doSimple b  

Here first define an interface and then two implementations of that interface. After that I defines two functions doSimple and doSimple2, which are basically identical. I prefer to use the second variant when possible.

If it walks like a duck and quack like a duck

Duck typing can be achieved in F# by using constraint. This is really powerful since you can write functions that take in arguments and as long as the types of the arguments support doesn't violate the constraint you can use them without the need of an interface. Why would you want to do this you might ask? The reason I started to look into this was actually because I needed it at work. I am using the excellent SqlClient type provider, and wanted to use the SqlProgrammabilityProvider to read and update multiple tables using the pipe operator. The problem is that the Update method is defined on the generated table type and not as a static Update method. A simplified version of what I had looks somewhat like this:

type SomeClass1() =  
    member
       this.Add(a:int, b:int) = a + b

type SomeClass2() =  
    member
        this.Add(a:int, b:int) = a + b

and I wanted an add method that could be applied to either SomeClass1 or SomeClass2 so I could write someClassInstance |> add 2 3. To achieve that you use member constraints like this:

type SomeClass1() =  
    member
        this.Add(a:int, b:int) = a + b

type SomeClass2() =  
    member
        this.Add(a:int, b:int) = a + b

let inline add (y:int) (z:int) (x: ^T when ^T : (member Add : int*int->int)) =  
    (^T : (member Add : int*int->int) (x,y,z))

SomeClass1() |> add 2 3  
SomeClass2() |> add 2 3  

In the function definition I define that I the argument x must have an operator called Add that has type int*int->int. In the body of the function I specify that I will call the Add function on variable x with the input of y and z. It looks complicated until you get your head around it. It is also important to specify the method as inline. One thing that is good to know is that it doesn't work for curried functions even though you don't get a compile time error.

The case I had was a slightly more complicated since the Update method I wanted to use had some optional arguments. If you know that optional arguments is represented as Option types in F# the code isn't that surprising:

type SomeClass1Option() =  
    member
        this.Add(?a:int, ?b:int) = 
            match a,b with
            | Some x, Some y -> x + y
            | _, _ -> 0

type SomeClass2Option() =  
    member
        this.Add(?a:int, ?b:int) = 
            match a,b with
            | Some x, Some y -> x + y
            | _, _ -> 0

let inline add (y:int) (z:int) (x: ^T when ^T : (member Add : int option*int option->int)) =  
    (^T : (member Add : int option*int option->int) (x,Some y,Some z))

SomeClass1Option() |> add 2 3  
SomeClass2Option() |> add 2 3  

Given all this I ended up writing an update method that looks like this:

let inline updateTable(table: ^T when ^T : (member Update : SqlConnection option*SqlTransaction option*int option -> int)) =  
    (^T : (member Update : SqlConnection option*SqlTransaction option*int option-> int) (table, None, None, None))

That method can be used with piping to call the Update method on any table generated with SqlProgrammabilityProvider with default arguments.

You can also write constraint on static operators, but I won't cover that. The documentation for constraints is found here: https://msdn.microsoft.com/en-us/library/dd233203.aspx.


Comments powered by Disqus