Search code examples
listf#inlineinference

Why F# fails to have type inference for list elements?


I was trying to put both int and float into a list construction:

> let l2=[1;2.0];;

  let l2=[1;2.0];;
  ----------^^^

stdin(50,11): error FS0001: This expression was expected to have type
    int
but here has type
    float
>

Well, if I write let l3=[1,2.0] in Haskell it works to construct the list to have all "fractional" elements. Why F# doesn't support automatic type inference? F# is based on .net and both Int and Float are Object type, right? Why Int and Float couldn't be put into one list, while some elements are having type promotion or conversion automatically? Seems the type system of F# makes it very difficult to write more generic programs.

Any hints?


Solution

  • F# doesn't have typeclasses like Haskell. While it's based on .NET, the only type in common between int and float is obj, and obj offers no operators that enable you to perform arithmetic.

    Still, you can define a list that contains both int and float values:

    let l : obj list = [1; 2.0]
    

    This is close to useless, though, because you can't do anything with these values unless you attempt to downcast them. It's also unsafe, because such a list can easily contain values that aren't numbers:

    let l : obj list = [1; 2.0; "foo"]
    

    All is not lost, however, because you can use the inline keyword to let the compiler figure out what operators are needed for various input:

    type Direction = Left = -1 | Straight = 0 | Right = 1
    
    let inline turn (x1, y1) (x2, y2) (x3, y3) =
        let prod = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)
        if prod > LanguagePrimitives.GenericZero then Direction.Left
        elif prod < LanguagePrimitives.GenericZero then Direction.Right
        else Direction.Straight
    

    This function has the following type:

    val inline turn :
      x1: ^a * y1: ^f -> x2: ^b * y2: ^g -> x3: ^j * y3: ^e -> Direction
        when ( ^b or  ^a) : (static member ( - ) :  ^b *  ^a ->  ^c) and
             ( ^j or  ^a) : (static member ( - ) :  ^j *  ^a ->  ^i) and
             ( ^c or  ^d) : (static member ( * ) :  ^c *  ^d ->  ^l) and
             ( ^e or  ^f) : (static member ( - ) :  ^e *  ^f ->  ^d) and
             ( ^g or  ^f) : (static member ( - ) :  ^g *  ^f ->  ^h) and
             ( ^h or  ^i) : (static member ( * ) :  ^h *  ^i ->  ^k) and
             ( ^l or  ^k) : (static member ( - ) :  ^l *  ^k ->  ^m) and
              ^m : (static member get_Zero : ->  ^m) and  ^m : comparison
    

    It states that a ^a and ^b are types that support the static operator - that, when used, returns the type ^c, which supports the static operator *, and so on.

    This example is taken from my convex hull example.

    It can be used with both int and float lists, because both types support the required operators:

    > turn (1,2) (3,3) (2,1);;
    val it : Direction = Right
    > turn (1.0,2.0) (3.0,3.0) (2.0,1.0);;
    val it : Direction = Right
    

    This is hardly as elegant as Haskell, but it can be used in a pinch. In practice, though, I don't miss it much. I rarely miss generic arithmetic in my professional life; I find it telling that some of the most commercially successful programming languages (Java, C#) don't support generic arithmetic.