fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

Type inference exception for simple functions

Open fxdmhtt opened this issue 7 months ago • 2 comments

I am a beginner in F#. I am imitating ramda to write some functions for learning and testing.

When imitating ramda's addIndex function, I found that the type inference was abnormal.

The code is as follows:

module Ramda
    let addIndex fn =
        fun f (xs: 'a list) ->
            let mutable i = 0
            let origFn = f
            let newFn x =
                let result = origFn x i xs
                i <- i + 1
                result
            fn newFn xs

    [1..5] |>                   addIndex List.map (fun x i _ -> x -        i ) |> printfn "%A"
    [1..5] |> List.map float |> addIndex List.map (fun x i _ -> x - (float i)) |> printfn "%A"

The type inference I expect is:

let addIndex (fn: ('a -> 'b) -> 'a list -> 'c) =
    fun (f: 'a -> int -> 'a list -> 'b) (xs: 'a list) ->
        // ......

But the actual type inference is:

let addIndex (fn: ('a -> 'b) -> 'a0 list -> 'c) =
    fun (f: 'a -> int -> 'a0 list -> 'b) (xs: 'a0 list) ->
        // ......

And the problem is in the line

fn newFn xs

Because the type inference of the three is:

// fn : ('a -> 'b) -> 'a0 list -> 'c
// newFn : 'a -> 'b
// xs : 'a list

F# should be able to infer that 'a and 'a0 are actually the same type.

It can be stably reproduced in the following environments:

  1. macOS + VSCode + .NET 9.0 SDK + Ionide for F# v7.25.10
  2. Windows 10 + Visual Studio 2022 17.14.2 + .NET 9.0
  3. Windows 10 + VSCode + .NET 9.0 SDK + Ionide for F# v7.25.10

I know that addIndex is not a pure design, it's just a toy. But even so, F# shouldn't be a surprise on this issue.

Additional information:

If the code is as follows:

let mapIndexed = addIndex List.map
[1..5] |>                   mapIndexed (fun x i _ -> x -        i ) |> printfn "%A"  // Either this
[1..5] |> List.map float |> mapIndexed (fun x i _ -> x - (float i)) |> printfn "%A"  //  Or this

It will not compile because mapIndexed is a normal function rather than a generic function, so only one of these two lines can compile.

However, if I write the code as follows:

let mapIndexed f xs = addIndex List.map f xs
[1..5] |>                   mapIndexed (fun x i _ -> x -        i ) |> printfn "%A"
[1..5] |> List.map float |> mapIndexed (fun x i _ -> x - (float i)) |> printfn "%A"

Then mapIndexed not only maintains the generic function, but also correctly performs type inference. That is:

mapIndexed  // ('a -> int -> 'a list -> 'b) -> 'a list -> 'b list

However, the type inference of the addIndex function is still abnormal.

fxdmhtt avatar May 29 '25 04:05 fxdmhtt

It is not a bug. It's a difference between "value" and "function".

Your first example assigns a "function object / delegate" to a field (I'm assuming you're doing this inside a module) called "mapIndex". F# then infers it's type from first usage and roots that type because in .Net values cannot be generic. Only methods and types can be.

Your second example declares a real function/method to "mapIndex". Thats why it works - you have inference working at each callsite because it is a real function with type parsmeters, not just just a value of a concrete type.

If you're coming from C#, try creating a Func of T field inside a non-generic class for example. Compiler won't let you do that because "there is no T" coming from anywhere

I understand that it requires some .Net knowledge and maybe it is slightly counter-intuitive from F# perspective but don't forget that F# is .Net first language so it has to play by the rules of .Net type system

En3Tho avatar May 29 '25 09:05 En3Tho

It is not a bug. It's a difference between "value" and "function".

Your first example assigns a "function object / delegate" to a field (I'm assuming you're doing this inside a module) called "mapIndex". F# then infers it's type from first usage and roots that type because in .Net values cannot be generic. Only methods and types can be.

Your second example declares a real function/method to "mapIndex". Thats why it works - you have inference working at each callsite because it is a real function with type parsmeters, not just just a value of a concrete type.

If you're coming from C#, try creating a Func of T field inside a non-generic class for example. Compiler won't let you do that because "there is no T" coming from anywhere

I understand that it requires some .Net knowledge and maybe it is slightly counter-intuitive from F# perspective but don't forget that F# is .Net first language so it has to play by the rules of .Net type system

Thank you so much~

Well, that might explain the "Additional information" part. But my core issue is the type inference of 'a0 vs 'a, which is the "bug" I'm concerned about.

fxdmhtt avatar May 29 '25 11:05 fxdmhtt

Closing as being by design

T-Gro avatar Oct 15 '25 10:10 T-Gro