Type Constraints for Data Constructors

So we know how to use a type parameter to statically constrain the length of a list.  Let’s continue our exploration of type constraints and see if we can figure out how to constrain the return type of a data constructor.

 

Data Constructor Constraints in Haskell

 

The paper “Fun with Phantom Types” by R Hinze has an example of a simple expression language in Haskell.

 

data Term t = Zero

            | Succ(Term Int)

            | Pred(Term Int)

            | IsZero(Term Int)

            | If (Term Bool) (Term t) (Term t)

 

What we’d like to be able to do with this language is have the interpreter statically catch any type errors.  For instance,

 

Main>let one = Succ(Zero)

Main>eval IsZero(one)

 

is a type safe declaration because IsZero can take a Term Int, whereas

 

Main>IsZero(IsZero one)

 

is not type safe because IsZero does not take a Term Bool.  Hinze suggest the following ‘mild’ extension for Haskell.

 

data Term t = Zero                             with t = Int

            | Succ(Term Int)                   with t = Int

            | Pred(Term Int)                   with t = Int

            | IsZero(Term Int)                 with t = Bool

            | If (Term Bool) (Term a) (Term a) with t = a

 

What the extension does is constrain each type constructor to returning a Term of the type specified after the with keyword.  Let’s see how we could do this in F#

 

Data Constructor Constraints in F#

 

We’ll start by building a discriminated union to construct our Term.

 

type Term<’t> = Zero

              | Succ of Term<int>

              | Pred of Term<int>

              | IsZero of Term<bool>

              | If of Term<bool> * Term<’t> * Term<’t>

 

And let’s examine the types when we use our union.

 

// val one : Term<’a>

let one = Succ(Zero)

// val is_false : Term<’a>

let is_false = IsZero(one)

// val is_problem : Term<’a>

let is_problem = IsZero(IsZero(one))

 

Notice how each of our bound variables gets assigned the type Term<a>?  What we would prefer is for the F# compiler to constrain the return type of the constructor to a designated type. This would allow the compiler to catch the type error on line 3.  An IsZero can only accept Term<int> and here we’re passing it a Term<bool>. 

 

So if we can’t fool the compiler at link time then let’s at least force a runtime exception.

 

let rec eval (t : Term<’a>) =

    match t with

    | Zero         -> 0

    //this code causes the construct to be less generic than the annotation

    | Succ e       -> eval e + 1

    | Pred e       -> eval e – 1

    //the expression has type bool but here is used with type int

    | IsZero e     -> eval e = 0

    | If(e1,e2,e3) -> if eval e1 then eval e2 else eval e3         

 

No such luck.  The match on Succ constrains t to Term<int> which prevents us from passing e to the eval statement at the IsZero match.  The type inference algorithm does not allow for polymorphic inputs or outputs to functions.  Why not?  Remember from this blog entry that “match” functions get compiled down into standard .Net methods.  So what language does allow for polymorphic return types?

 

Data Constructor Constraints in VB

 

Let’s dust off our old standby and see if it can’t teach the Haskell and F# prodigies a lesson on problem solving.

 

Public MustInherit Class Term(Of TEval)

    Public MustOverride Function Eval() As TEval

End Class

 

Public Class Succ

    Inherits Term(Of Integer)

    Private _e As Term(Of Integer)

    Public Sub New(ByVal e As Term(Of Integer))

        _e = e

    End Sub

    Public Overrides Function Eval() As Integer

        Return _e.Eval + 1

    End Function

End Class

 

Public Class IsZero

    Inherits Term(Of Boolean)

    Private _e As Term(Of Integer)

    Public Sub New(ByVal e As Term(Of Integer))

        _e = e

    End Sub

    Public Overrides Function Eval() As Boolean

        Return _e.Eval = 0

    End Function

End Class

 

The constructor for each of our terms does allow us to constrain the type of term that can be used to construct the term.  The TEval type constraint further lets us constrain the type that gets returned when the type is evaluated.

 

All we’ve done is break our constructor and eval function out over each term.  Because each term has its own slot in the virtual method table our “broken out” function can return a different type depending upon the match.  It seems odd that F# does something very similar to the above when building discriminated unions but chooses to combine the eval function into a single function instead of making the match code local to each term.

 

//why is the code to right of the -> not encapsulated in each term?

let rec eval (t : Term<’a>) =

    match t with

    | Zero         -> 0

    | Succ e       -> eval e + 1

    | Pred e       -> eval e – 1

    | IsZero e     -> eval e = 0

    | If(e1,e2,e3) -> if eval e1 then eval e2 else eval e3         

 

Because if it was then we’d realize the benefits of static checking for our domain specific expression languages just like the terms we constructed in VB.

 

// val one : Succ

let one = Succ(Zero())

// val is_false : IsZero

let is_false = IsZero(one)

// the type IsZero is not compatible with the type int

let is_problem = IsZero(IsZero(one))

 

Thanks for figuring that out for me compiler!  Kind of nice to see our trusty, Von Neumann, hacker language for Morts teach our lambda friends a lesson in how to do type safe domain specific languages.

 

Full Source Code

 

Public MustInherit Class Term(Of TEval)

    Public MustOverride Function Eval() As TEval

End Class

 

Public Class Zero

    Inherits Term(Of Integer)

    Public Sub New()

    End Sub

    Public Overrides Function Eval() As Integer

        Return 0

    End Function

End Class

 

Public Class Succ

    Inherits Term(Of Integer)

    Private _e As Term(Of Integer)

    Public Sub New(ByVal e As Term(Of Integer))

        _e = e

    End Sub

    Public Overrides Function Eval() As Integer

        Return _e.Eval + 1

    End Function

End Class

 

Public Class Pred

    Inherits Term(Of Integer)

    Private _e As Term(Of Integer)

    Public Sub New(ByVal e As Term(Of Integer))

        _e = e

    End Sub

    Public Overrides Function Eval() As Integer

        Return _e.Eval – 1

    End Function

End Class

 

Public Class IsZero

    Inherits Term(Of Boolean)

    Private _e As Term(Of Integer)

    Public Sub New(ByVal e As Term(Of Integer))

        _e = e

    End Sub

    Public Overrides Function Eval() As Boolean

        Return _e.Eval = 0

    End Function

End Class

 

Public Class [If](Of TEval)

    Inherits Term(Of TEval)

    Private _e1 As Term(Of Boolean)

    Private _e2 As Term(Of TEval)

    Private _e3 As Term(Of TEval)

    Public Sub New(ByVal e1 As Term(Of Boolean), ByVal e2 As Term(Of TEval), ByVal e3 As Term(Of TEval))

        _e1 = e1

        _e2 = e2

        _e3 = e3

    End Sub

    Public Overrides Function Eval() As TEval

        If _e1.Eval Then

            Return _e2.Eval

        Else

            Return _e3.Eval()

        End If

    End Function

End Class 

 

#light

 

open TermVB

 

let one = Succ(Zero())

let i = one.Eval()

printfn “%A” i

 

let i1 = IsZero(one).Eval()

printfn “%A” i1

 

// the type IsZero is not compatible with type Term<int>

//let i2 = IsZero(IsZero(one))

 

let i3 = If(IsZero(one),Zero(),one).Eval()

printfn “%A” i3

 

let True = IsZero(Zero())

let False = IsZero(one)

let t1 = If(True,True,False).Eval()

printfn “%A” t1

 

System.Console.ReadLine()

4 comments to Type Constraints for Data Constructors

  • Keith

    It’s not quite as simple as you make it sound… what if you wanted to match against some special cases instead of just matching against constructors? For instance, in the F# code, you could match against Succ(Zero) and yield 1, but where could you put that logic in your VB code?

  • hholtmann

    The mentioned Haskell-extension ist already implemented in GHC. It goes under the name
    Generalized Algebraic Data Types (GADTs) and works like this:

    {-# LANGUAGE GADTs -#}
    data Term t where
    Zero :: Term Int
    Succ (Term Int) :: Term Int
    Pred (Term Int) :: Term Int
    IsZero (Term Int) :: Term Bool
    If (Term Bool) (Term t) (Term t) :: Term t

  • mattdoig

    Thanks for the heads up hholtmann, I’ve been learning Haskell though the book “Programming in Haskell” which was probably writtin before GADT support. I can work through the rest of the paper now!

  • mattdoig

    Keith, I believe the following code illustrates your point

    let one = Succ(Zero)
    let two = Succ(Succ(Zero))
    let three = Succ(Succ(Succ(Zero)))

    let rec is_three (t : Term) =
    match t with
    | Succ(Succ(Succ(Zero))) -> true
    | _ -> false

    Where would we put everything to the left of the -> since there is no class module for Succ(Succ(Succ(Zero))). I guess the compiler would have to create a class definition for every recursive match as well, or a potentially simpler approach would be to utilize the CLR’s ability to return different types from functions. Why none of the languagues utilize this feature of the runtime nobody knows.

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>