United Kingdom: +44 (0)208 088 8978

Implicit type conversions in F# 6

Taking a look at the new implicit type conversions in F# 6.

We're hiring Software Developers

Click here to find out more

F# has always been quite strict with its types, and that's broadly very useful. Implicit conversions and upcasting are often not allowed, helping to prevent certain kinds of bugs. However, in F# 6 some of these rules have been relaxed.

Implicit conversion of int to "wider" types

Note: 1 has type int and 1L has type int64

Before F# 6, if you provided any number type when a different number type was expected this would cause a compiler error. The motivation here was to avoid any loss of accuracy or integer overflow that could accidentally happen with an implicit conversion. F# wanted you to always explicitly convert that type to force you to think about the risk of doing so.

In F# 6, you are now allowed to provide an int when an int64 is expected. Consider this example in a list, where every type must match or be convertible to the type of the first element:

[ 1L; 2L; 3 ]
// returns [ 1L; 2L; 3L ]

The first two elements are int64 but the third element is an int. However, the code compiles and runs. The third element has been automatically converted to an int64.

There are two more similar conversions from int that are now allowed:

[ 1.1; 2 ] // int -> float/double
[ 1n; 1 ] // int -> nativeint

Note that these are only allowed because they are safe conversion that can't result in overflow or loss of accuracy. This is known as a "widening" of the type. Other type conversions such as float -> int are not done implicitly even though they might be more convenient in some situations, as they could accidentally result in bugs.

Implicit upcasts

Before F# 6, when multiple branches returned different types you would always get a compiler error, even if both types derived from a common type. Code like this would cause a compiler error:

let xs : int seq =
    if true then
        [ 1 ]
        [| 1 |]

// xs is now seq { 1 }

The first branch returns a list and the second an array, which both derive from seq. From F# 6, this code will compile successfully.

Note that in this particular case the code only compiles because the whole expression is known to be a seq, which is known because of the type annotation.

.NET implicit conversions on method calls

The JsonNode type from System.Text.Json has an implicit conversion from several types, which makes it convenient for building JSON, which is a flexible and dynamic data type.

open System.Text.Json.Nodes

// Define a function and a method that take a JsonNode
let takeNode (x:JsonNode) = x
type MyClass =
    static member TakeNode (x:JsonNode) = x

// a) Compiles before F# 6
takeNode (JsonNode.op_Implicit 1) // compiles before F# 6

// b) Compiles from F# 6 only but with a warning
takeNode 1 // Warning: implicit conversion

// c) Compiles from F# 6
MyClass.TakeNode 1 // no warning or error

These three examples above demonstrate the following:

a) Before F# 6 we needed to call JsonNode.op_Implicit to convert an int into a JSON node. All of the following examples cause a compiler error before F# 6.

b) From F# 6, we can pass an int directly to a function where JsonNode is expected, but it produces a warning.

c) From F# 6, we can pass an int directly to a method where JsonNode is expected, without any warning.

Implicit convenience

Although we trade off a little bit of type safety by using these implicit conversions, they have been designed to apply to cases that are least likely to cause any bugs while giving the most improvement in user experiences, especially when using common .NET APIs.