“Super-Strong” Typing, Dynamic Typing and Type Inference in F# 2.0

posted in: Language Features | 0

This article gives an overview how types are accessed in F# with a combination of features related to static typing, dynamic typing and type inference.

Static Typing

Static type checking ensures, at compile time, that the program is free from most kinds of type errors. The source code editor can highlight such errors as soon as they are written out. Static type checking in F# goes further than in many other strongly typed languages:

  • By default, types defined in F# cannot be null. This eradicates a whole category of painful and costly errors (Tony Hoare of Microsoft Research Cambridge, who introduced null references in ALGOL in 1965, later regarded this as his billion dollar mistake).
  • Implicit conversions are not allowed—not even between numbers, when the precision would be preserved. For instance, you cannot pass an integer, when a floating point value is expected, without explicit conversion. However, F# permits passing an argument of a derived type where a parameter of a base type is expected. It is also possible to utilize implicit operator overloads, which have been implemented in other languages, by calling the op_implicit member of the Common Intermediate Language(CIL) type:
    open System.Drawing
    let rect = Rectangle(23, 12, 17, 13)
    let rectF = RectangleF.op_Implicit rect
  • A new feature introduced by F#, units of measure, lets the compiler verify calculations associated with measurements. This safeguards against passing a number of meters when a number of kilograms is expected. On the other hand, when a number of meters with another number of meters is multiplied, the compiler correctly creates type-safe square meters:
    [<Measure>]
    type m
    let meters = 276<m>
    let squareMeters = meters * meters
    // Resolved by the compiler as:
    // val squareMeters : int<m ^ 2> = 76176
    In F# 3.0, the core library will contain the namespace Microsoft.FSharp.Data.UnitSystems.SI, with predefined units of measure types for the International System of Units (SI), such as ampere (A), hertz (Hz), kelvin (k), lux (lx), metre (m), newton (N), second (s), ... Units of measure were originally conceived by Andrew Kennedy of Microsoft Research Cambridge in his PhD thesis. He also wrote a great introduction about it in his blog.
  • F# 3.0 will feature another novelty: type providers. They let you access any kind of untyped data source, locally or remotely, in a strongly-typed way at compile time, with full autocompletion/IntelliSense support. There will be built-in type providers for database connections, OData services, web services, resource files, and more. You will also be able to create your own custom type providers.

Dynamic typing

With type providers, there is no more need for dynamic typing... almost! For instance, one may still require dynamic typing for calls to arbitrary, untyped data sources unknown at compile time. In F#, this can be achieved by implementing the predefined dynamic operators ? and ?<-. These operators permit to access precise parts of a type in a "weak" (but generic) way, while still keeping the "strong" access to the rest of the type's API intact.

// Implement dynamic operators for System.DataRow
// to allow getting/setting a generic value.
open System.Data
let (?) (row:DataRow) (columnName:string) = 
    unbox row.[columnName]
let (?<-) (row:DataRow) (columnName:string) value  = 
    row.[columnName] <- value

// Create a DataTable with two columns.
let table = new DataTable()
table.Columns.Add("Name", typeof<string>)
table.Columns.Add("Born", typeof<DateTime>)

// Initialize a DataRow, using the dynamic operator.
let row = table.NewRow()
row?Name <- "John Doe"
row?Born <- DateTime(1983, 7, 26)
table.Rows.Add row

// Read the row content into a strongly-typed record.
type Person = {Name: string; Born: DateTime}
let record = {Name = row?Name; Born = row?Born}

I find this approach preferable to C#, whose dynamic keyword is less granular, as it forces the programmer to access the complete API of a type in a weak way. It is also non-generic, as each access to a dynamic instance returns another dynamic instance.

Type inference

Type inference in F# automatically recognizes the intended types (including generic types) of local values, parameters (including return parameters), and let-bound fields. Is is based on the Hindley-Milner (HM) method. One can always declare a type manually, in various places in the source code. However, declaring a type is usually not necessary. As a consequence, you can write straightforward, simple code, even when the types and signatures involved are highly generic. The following example creates a helper class in F#, which may be used by any client written in a CIL-compatible language (F#, C#, VB.Net, ...).

open System.Linq
type SortHelper =
    static member GetTopN(values, keySelector, n) =
        Enumerable.OrderByDescending(values, keySelector) |> Seq.take n 

The above GetTopN method looks as follows when accessed from C#. Notice how the F# compiler has automatically inferred the types and genericity of the signature from the context:

public static IEnumerable<a> GetTopN<a, b>
    (IEnumerable<a> values, Func<a, b> keySelector, int n)

Conclusion

The combination of static typing, dynamic typing and type inference gives you the best of three worlds. Static typing with type inference has been known for many years in functional languages such as Haskell and functional/object-oriented languages such as OCaml. However, with units of measure and type providers, F# gives you a new kind of "super-strong typing", while still allowing weak typing in cases where it is unavoidable. In addition, F# gives you type-safe access to thousands of types predefined in the .NET Framework (or Mono), and—via CIL interoperability—to a potentially unlimited number of libraries implemented in other languages.

Comments are closed.