4

I don't understand the F# type inference system for nested functions. It seems particularly broken when I use types outside simple types such as int, string, ...

here is a small example of some code printing some reflection info

let inferenceTest () =
    let t = int.GetType()
    let methods = t.GetMethods() |> Seq.map(fun m -> m.Name)
    printfn "%s" <| String.concat ", " methods

This works fine! No need for casting etc. Now assume that the printing is much more involved, hence we want to separate it into a nested function

let inferenceTestFailsToCompile () =
    let printType t =
        let methods = t.GetMethods() |> Seq.map(fun m -> m.Name)
        printfn "%s" <| String.concat ", " methods

    let t = int.GetType()
    printType t    

This fails with "lookup on object of indeterminate type based on information prior to this program point. A type annotation may be needed..."

Why is there suddenly less information for the type system? Perhaps I could understand the problem had my printType() function been in the same scope as my inferenceTestFailsToCompile()

When I instead create a lambda that takes t as its closure, the typing issue goes away

let inferenceTestLambda () =
    let t = int.GetType()
    let printType =
        let methods = t.GetMethods() |> Seq.map(fun m -> m.Name)
        printfn "%s" <| String.concat ", " methods
    printType
Carlo V. Dango
  • 13,322
  • 16
  • 71
  • 114
  • See: [Understanding type inference](http://fsharpforfunandprofit.com/posts/type-inference/), [Type Inference (F#)](https://msdn.microsoft.com/en-us/library/dd233180.aspx), and [F# Ways to help type inference?](http://stackoverflow.com/q/13821811/1243762) – Guy Coder Feb 11 '16 at 20:48
  • As a side note: `printType` doesn't need that additional `unit` argument. It has one "real" argument, and that sufficiently qualifies it as a function. – TeaDrivenDev Feb 12 '16 at 01:37

1 Answers1

5

Basically type inference is done top to bottom left to right. There are lots of exceptions but I won't go into them.

In the first example the inferencing engine had enough information to infer the types correctly.

let inferenceTest () =
    let t = int.GetType()
    let methods = t.GetMethods() |> Seq.map(fun m -> m.Name)
    printfn "%s" <| String.concat ", " methods

became

let inferenceTest () =
    let (t : type) = int.GetType()
    let methods = t.GetMethods() |> Seq.map(fun m -> m.Name)
    printfn "%s" <| String.concat ", " methods

became

let inferenceTest () =
    let (t : type) = int.GetType()
    let methods = (t.GetMethods() : System.Reflection.MethodInfo []) |> Seq.map(fun m -> m.Name)
    printfn "%s" <| String.concat ", " methods

became

let inferenceTest () =
    let (t : type) = int.GetType()
    let (methods : seq<string>) = (t.GetMethods() : System.Reflection.MethodInfo []) |> Seq.map(fun m -> m.Name)
    printfn "%s" <| String.concat ", " methods

which was helped by the use of |> and fails as

let methods = Seq.map (fun m -> m.Name) (t.GetMethods())

In the second example this line is inferred as .

let printType (t : 'a) () = 

which causes the error on

let methods = t.GetMethods() |> Seq.map(fun m -> m.Name)

because the type for t is generic and does not give enough info for use with t.GetMethods().

To solve these problems, I use Visual Studio and move the mouse over the variables to look at the type. Then if I find a type that is not right I start adding the type definitions. This usually leads to fixing the error or uncovers a bug in my code.

EDIT:

This is part of an answer from Why is F#'s type inference so fickle? by Robert Harvey

F# uses one pass compilation such that you can only reference types or functions which have been defined either earlier in the file you're currently in or appear in a file which is specified earlier in the compilation order.

I recently asked Don Syme about making multiple source passes to improve the type inference process. His reply was

"Yes, it’s possible to do multi-pass type inference. There are also single-pass variations that generate a finite set of constraints.

However these approaches tend to give bad error messages and poor intellisense results in a visual editor."

Community
  • 1
  • 1
Guy Coder
  • 24,501
  • 8
  • 71
  • 136
  • Thanks for the type break-down. I'm assuming the only one that can call my function `printType()` is the code within the scope it is defined. Hence I don't understand that it cannot infer that the only usage is the `MethodInfo[]` – Carlo V. Dango Feb 11 '16 at 20:50
  • `let t = int.GetType()` and `printType t` come after `let methods = t.GetMethods() |> Seq.map(fun m -> m.Name)` so the inferencing engine does not know about them when it gets to `t.GetMethods`. There are lots of papers on [Type Inference](http://citeseer.ist.psu.edu/search?q=type+inference&sort=rlv&t=doc&submit.x=16&submit.y=20) and I would not be surprised if one talks about what you seek, but F# chose the implementation of [Hindley-Milner](https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system) for specific reasons. – Guy Coder Feb 11 '16 at 21:01
  • 1
    @CarloV.Dango It has nothing to do if is a nested function or who can call it. F# tries to be as generic as possible and `printType` is a function that expects just an argument `t`. The only thing you do is call a method on it, and you cannot safely infer a type by just having a method call. Because every class can implement that method. So you get an error that at this point the type cannot be determined. This is a general restriction, and would also happen if you extract `getType` into its own function non-nested function. – David Raab Feb 12 '16 at 04:16
  • @SidBurn the type system infers the types based on the usage. Hence it makes perfectly sense to look at inner functions and see their call sites and infer the types from what was passed into them as arguments. Just as it does when the code of the inner function is in-lined as it is in the first example – Carlo V. Dango Feb 12 '16 at 15:10
  • @CarloV.Dango The type system don't infer object types on its usage, because it is not possible to infer anything here. Because i already explained that you cannot infer a class based on a method call, because there can exists millions of classes with such a method. It is impossible to infer types for objects. The only thing that comes close would be something like (statically) typed duck-typing. So every class that implements the called method would be accepted. But F# don't do something like that automatically. – David Raab Feb 12 '16 at 17:31
  • @SidBurn I'm just puzzled that the compiler does not ralise that there can ever only be one caller. I've updated the example with yet another variation that compiles fine, when the clousure includes `t` – Carlo V. Dango Feb 15 '16 at 20:30
  • @CarloV.Dango When `t` is included you have `let t = int.GetType()` at that point `GetType` is called on an already known type `int`. So that example works fine. But if you extract it in another function you just call `GetType` on some object and the compiler cannot know the type of that object. If a function is just called from one place is not important. Type-inference always tries to get the exact type through inferring, not guessing. Otherwise as soon as you would call your function from two places, you would get errors on places where no error was before. That would be a bad behaviour. – David Raab Feb 16 '16 at 08:00