DEV Community

Rob Mulpeter
Rob Mulpeter

Posted on

F# : using List.exists on record types may cause breaking changes

Below are two examples of how you can determine if an expression returns true for a given list.

Example 1 uses List.exists as per the Microsoft documentation and passes the expression and list as the second parameter

Example 2 uses a pipe to pass List.exist an explicit type of list as per the documentation

type Person = { name: string }
type Fox = { name: string }

let itemExists =

    let people : Person list = [{ name = "Robert" }]

    //This won't compile: Type mismatch
    let example1 : bool = List.exists (fun i -> = "X") people

    //This will compile
    let example2 : bool = people |> List.exists (fun i -> = "X")
Enter fullscreen mode Exit fullscreen mode

The Problem

The problem here is that for example1 the order of the record types matter and in its above state will not compile with a type mismatch.

The lambda is implicitly determining the type of i based on name. But name is shared by both type Person and type Fox. It therefore takes the first matching type in the file or namespace from the BOTTOM. In this case i is of type Fox and is incompatible with the passed person list.

Where this becomes dangerous is when your types are spread out through the application or even in a separate library. If a new record type is added with the same property name and type it will result in your consuming application breaking if it were to take the update.

Top comments (1)

armousness profile image
Sean Williams

I think this is a—not exactly a bug, but an inadequacy of the type inference system. Other things that would fix your problem are,

let example1 = List.exists (fun (i: Person) -> = "X") people

let inline list_exists c f = List.exists f c
let example2 = list_exists people (fun i -> = "X")
Enter fullscreen mode Exit fullscreen mode

It seems that F# type inference works from left-to-right. I appreciate that type inference is more ambiguous in an object-oriented language for precisely the reason you laid out, that classes and records are namespaces so properties and methods don't uniquely identify types. But the big mistake of F# is, when you're dealing in combinators like map and filter and exists, the type inference works if you put the collection first (since it's unambiguously typed in almost all cases). So why did they put the functional argument first?