Subscribe to get more articles about programming languages
Donate

F# gotchas for C# developers

sharp cactus

F# (F-Sharp) is a .NET language developed by Microsoft. It brings the world of ML languages like OCaml, Standard ML or Elm closer to the .NET world. Jumping from an imperative programming language like C# to a declarative functional programming can be mind-bending. Here are some head scratchers that could come up at the beginner and intermediate level.

Untyped or typed arguments?

One of the first things that appear different is that type declarations are not required. Coming from a scripting languages background one might think that it’s the same as there, but it’s not. Consider an example:

// define a function "repeat"
// that repeats a string 5 times
let repeat s =
    String.replicate 5 s
let main =
    let result = repeat '*' // char
    printfn "%A" result

This code won’t compile because of:

error FS0001: This expression was expected to have type
    string
but here has type
    char

In C# you have to declare argument types in the method declaration like:

String repeat(String s) { ... } 

An F# declaration in C# syntax would look more like this:

var repeat(var s) { ... } 

and it still remains type-safe, because the argument type is automatically inferred. In this example the compiler scans the body of the function and finds out that it expects a string, because String.replicate expects a string.

Spacing and grouping parameters

Most of the F# functions take arguments one by one separated with spaces (as seen in the previous example). However in some cases the arguments are grouped. For example:

let a_plus_b_1_1 = String.Join("+", ["a";"b"])
let a_plus_b_1_2 = String.Join ("+", ["a";"b"])
let a_plus_b_2 = String.concat "+" ["a";"b"]

All 3 cases produce the same result “a+b”, and the first 2 are equivalent. As a rule of thumb, calling .NET standard library functions will expect tuples (in parentheses) and it looks more like C# code, while F# library functions will expect no grouping. Many functions’ names in the F# library start with a lowercase letter, but it’s not granted, so it’s usually necessary to consult the documentation.

To bring more confusion in case of a single argument you can omit the parentheses:

"xxxaxxx".IndexOf('a')
"xxxaxxx".IndexOf ('a') 
"xxxaxxx".IndexOf 'a'

All 3 cases are equivalent and produce a result 3.

Generics

Sometimes a function is generic. It’s not possible to determine the parameter types form its body. For example:

let add5times a =
    a + a + a + a + a

In such case F# is smart enough to determine the types from the function calls. So if a function is called like so:

let my5stars = add5times "*"

then the argument and the result type will be inferred to be string. Of course add5times 10 infers int and produces a result 50.

It gets confusing when you can’t call this generic method on different types in the same program:

let result1 = repeat5 "*"
let result2 = repeat5 10

this won’t compile. It yells that after the first line F# decided that “repeat5” function takes a string and so you can’t pass an int. Although documentation speaks about “Automatic Generalization”, it doesn’t help in this case, and you need to provide explicit type information in one way or another.

This problem can come up in code working with collections like lists or arrays. To fix it rely on explicit specific types, generic type parameters, type constraints or type casts.

Upcasts

Upcasting is automatic in some languages like C#, Java, C++. In F# it is automatic as well… in some cases. It works for passing arguments in, but doesn’t work for return values, for example:

let makeTestSequence () : seq<int> =
    [1;2;3] :> seq<int>

Here an upcast (using an angle nose smiley operator :>) is required although the list is a subtype of seq. Some reasoning behind this is explained in this question, but it mostly excuses that it would be a hard thing to implement in the compiler. Consider this example, which works fine in C#:

var list1 = new List<int>() { 5, 5, 5 };
IEnumerable<int> list2 = new int[] { 1, 2, 3 };
var list3 = true ? list1 : list2;

The C# compiler is able to identify that it’s possible to upcast list1 from List to IEnumerable, and so list3 type is inferred to be IEnumerable. Similar code in F# requires an upcast:

let list1 = [ 5; 5; 5 ]
let list2 = seq { for i in 1 .. 3 -> i }
let list3 = if true then (list1 :> seq<int>) else list2

Discriminated unions and pattern matching

On the surface a discriminated union might look like an “enum” to a C# developer: you can define a set of exclusive constant values and use “switch cases” (match) on a variable:

type Color = RED | GREEN | BLUE

let color2rgb c =
    match c with
        | RED -> (255, 0, 0)
        | GREEN -> (0, 255, 0)
        | BLUE -> (0, 0, 255)

Discriminated unions can also have some data attached to each value. In C# you can only attach an integer value to a constant, but in some other languages (like Java and Swift) you can attach arbitrary values to each constant. In F# not only you can do that, but also match (switch) on that additional data with patterns and conditions:

type ColorSpace = 
    | RGB of red:int * green:int * blue:int
    | Greyscale of brightness:int
    | BlackAndWhite of isWhite:bool

let color2bw c =
    match c with
        | Greyscale b when b > 127 -> BlackAndWhite(isWhite=true)
        | Greyscale b -> BlackAndWhite(isWhite=false)
        | RGB (r, g, b) -> BlackAndWhite (r + g + b > 127)
        | BlackAndWhite w -> c

This example demonstrates that the data attached to each union value can be of different shape and types. RGB value has a triplet of integers while BlackAndWhite has a single boolean. Pattern matching can decompose those values to get individual components and restrict cases with “when” conditions.

One additional note, which is not obvious, is that each union value can be treated and used as a function that accepts the attached data as arguments. This behaviour can be utilized in some creative ways. In the code above BlackAndWhite was used as a function that takes a boolean argument. If it’s not clear, consider this:

let colorConstructor = BlackAndWhite
let whiteColor = colorConstructor true

Discriminated unions are even more powerful than any conventional “enum” types as they support recursive definition of values. This lets it easy to define tree-like structures which can be matched and traversed recursively, for example:

type Employee =
    | Worker
    | Manager of list<Employee>

The matching expressions are very powerful and support a variety of matching strategies. Many data decomposition patterns are there, and each case can be refined further by a “when” condition. Matching with active patterns gives even more opportunities for advanced matching strategies.

Struct vs record

In short “record” is an ML equivalent of .NET structs. Some key differences are that records are:

  • immutable by default (although it’s possible to opt-in for mutability)
  • support decomposition in pattern matching
  • participate in type inference by their field names

It feels possible for the “record” behaviour to be implemented on top of .NET structs in order to not bring any new concepts to the language. Record and struct concepts are not so much different from a programmer’s perspective. It is easy to imagine though that structs are always memcopied when passed around (as a .NET value type) while records might need copy-on-write behaviour to save memory in some typical usage scenarios.

Namespaces vs modules

This confusion comes again from the language differences and implementation specifics. Conceptually modules are very similar to namespaces. A problem is that in the .NET VM it’s not possible to create global variables and global functions. Variables and functions should always be put inside of some class (like “Main” inside of a “Program”). F# programs can have variable and function declarations in the file scope, so the implementation has to create a special “module” class to hold it. By default it has the same name as the file name. This can be thought of as a static .NET class. It’s possible to group code by defining custom modules, but such modules can’t span across multiple files as namespaces.

Additional information

Subscribe to get more articles about programming languages
Donate