Fsharp: Type Inference Could be better with List.groupBy or sumBy

Created on 15 Nov 2019  路  4Comments  路  Source: dotnet/fsharp

When a type declaration is made in the function header, either the groupBy or sumBy function incorrectly inferences another non related type.
This is more of a nuisance than anything else, but in my admittedly uneducated view the compiler should have enough information to correctly infer the type

Repro steps
The code below should reproduce the error

Expected behavior
Compiler correctly interprets the type

Actual behavior
The type OrderHistoryItem does not match the type CartItem
The error occurs for itms in the sumBy function - highlighted under _itms_

type CartItem = {
    ItemID: string
    ItemDescription: string
    ItemPrice: double
    Qty: int
}

type OrderHistoryItem = {
    SlipNumber: string
    OrderDate: DateTime
    MealTimeID: string
    PickupLocation: string
    ItemID: string
    ItemDescription: string
    ItemPrice: decimal
    Qty: int
}

let subtotalItems (items:CartItem list) =
    items
        |> List.groupBy (fun x -> (x.ItemID, x.ItemDescription, x.ItemPrice) )
        |> List.map ( fun ((id,desc,price),itms)  -> {
                            ItemID= id
                            ItemDescription= desc
                            ItemPrice= price
                            Qty= List.sumBy (fun x -> x.Qty) itms
                    } )

Known workarounds
Reorder the type declarations of CartItem and OrderHistoryItem

Add redundant type declaration to sumBy function as below:

let subtotalItems (items:CartItem list) =
    items
        |> List.groupBy (fun x -> (x.ItemID, x.ItemDescription, x.ItemPrice) )
        |> List.map ( fun ((id,desc,price),itms)  -> {
                            ItemID= id
                            ItemDescription= desc
                            ItemPrice= price
                            Qty= List.sumBy (fun (x:CartItem) -> x.Qty) itms
                    } )

System Info
VsCode 1.40.1

dotnet --info

.NET Core SDK (reflecting any global.json):
 Version:   2.1.802
 Commit:    177d0b2525

Runtime Environment:
 OS Name:     ubuntu
 OS Version:  16.04
 OS Platform: Linux
 RID:         ubuntu.16.04-x64
 Base Path:   /usr/share/dotnet/sdk/2.1.802/

Host (useful for support):
  Version: 2.1.13
  Commit:  1a165a1588

.NET Core SDKs installed:
  2.1.4 [/usr/share/dotnet/sdk]
  2.1.802 [/usr/share/dotnet/sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.13 [/usr/share/dotnet/shared/Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.13 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.0.5 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.13 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

Most helpful comment

This is currently by design as per what @abelbraaksma explained, so I'll close this on that basis. Closing this in favor of a comment here: https://github.com/fsharp/fslang-suggestions/issues/594#issuecomment-554424789

As it stands, this is not a bug even if it kind of feels that way.

All 4 comments

@tangledupinblue as a workaround, if you use itms |> List.sumBy (fun x -> x.Qty) the initial sample works without extra annotation.

@smoothdeveloper - interesting, tried it in my project and it does work. Thanks

@tangledupinblue the key rule to remember about type inference is: if the variable is evaluated (that is: seen by the compiler) before the call to the function, the compiler knows its type.

I.e., it would have also worked if you had something like let x = itms prior to the List.sumBy function. That is, typically _any_ prior usage will usually have the compiler infer the type correctly. That's why so much F# code is written with |> piping.

The reason behind this is simple: to prevent multiple passes over the source to do type inference, the compiler cannot look-ahead too much. Hence, the moment it "sees" List.sumBy, it does not yet know which generic version of that function to choose, and therefor, what the types of its arguments are.

If you hadn't had two times the same name for the record field Qty the problem wouldn't have arisen either. For the same reasons of "single pass", the compiler remembers the last record that has that field.

This is currently by design as per what @abelbraaksma explained, so I'll close this on that basis. Closing this in favor of a comment here: https://github.com/fsharp/fslang-suggestions/issues/594#issuecomment-554424789

As it stands, this is not a bug even if it kind of feels that way.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vasily-kirichenko picture vasily-kirichenko  路  3Comments

Tarmil picture Tarmil  路  4Comments

jackfoxy picture jackfoxy  路  4Comments

AaronEshbach picture AaronEshbach  路  3Comments

mrakgr picture mrakgr  路  3Comments