Hackernoon logoAn Introduction to F# as Microsoft's / .NET's Answer to a Functional-First Programming Language by@samwalpole

An Introduction to F# as Microsoft's / .NET's Answer to a Functional-First Programming Language

Sam Walpole Hacker Noon profile picture

@samwalpoleSam Walpole

Fullstack .NET and JavaScript web developer. Coding teacher and advocate

Recently I've been learning how to write code in F#. For those who haven't heard of it, F# is Microsoft's/.NET's answer to a functional-first programming language.

My motivation was to learn a functional programming language that would make coding for scientific computing and data analysis more expressive, concise, and maintainable, all while fitting seamlessly into the .NET ecosystem that I already know and love. F# fits that bill perfectly.

Why Functional Programming?

For an object oriented programmer, the idea of functional programming can be a little foreign at first. We're used to applications in which everything is an object and the application works by changing the state of those objects (mutation) over time.

In contrast, functional programs consist of values and functions, and the application works by applying these functions to values in order to produce new values.

This might sound similar to methods in an object oriented style, but there are two key differences:

1. Values cannot be modified (they are said to be immutable). Instead, operations create and return new values.

2. Functions should be pure, meaning that a given input always produces the same output. This is in contrast to a class method, in which some internal state of the class might have an effect on the output of the method.

Let's have a look at this in a code example, in which we want to raise an employee's salary by a given factor:

// C# Object Oriented Example
public class Employee
{
	public string Name { get; set; }
	public float Salary { get; set; }
}

public class SalaryRaiser
{
	public float Factor { get; set; }
	
	public void RaiseSalary(Employee employee)
	{
        // function not pure - depends on Factor, which isn't a parameter
		var raise = employee.Salary * Factor;
		
        // employee object is mutated directly
		employee.Salary += raise;
	}
}

var employee = new Employee { Name = "Bob", Salary = 12345.67F };
var raiser = new SalaryRaiser { Factor = 0.05F };
raiser.RaiseSalary(employee);
// F# Functional Example
type Employee = { Name: string; Salary: float }

let factor = 0.05
let calculateSalary employee factor =
    // function is pure - factor is now a parameter
    let raise = employee.Salary * factor

    // new employee record returned
    { employee with  Salary = employee.Salary + raise }

let employee = { Name = "Bob"; Salary = 12345.67 }
let updatedEmployee = calculateSalary employee factor

The key benefits of these functional features (immutability and pure functions) is that they make your code much more maintainable and testable, since you can be sure that values aren't accidentally being mutated (also known as side-effects) and that a given function will always behave the same for a given input.

Data Types in F#

In F# data types are static and strongly typed, meaning that they are determined at compile time and a given value cannot change it's type. This will be familiar to C# programmers. However, there are some slight differences that may take some getting used to:

  • F# has a powerful type inference system meaning that you hardly ever have to explicitly state what the type is - it is inferred from context
  • // C#
    public int AddInt(int a, int b)
    {
        return a + b;
    }
    
    var value = AddInt(1, 2)
    // F#
    let addInt x y = x + y
    
    let value = addInt 1 2
  • F# expects explicit conversion between types
  • // C#
    public float AddFloat(float a, float b)
    {
        return a + b;
    }
    
    int a = 1;
    int b = 2;
    
    var value = AddFloat(a, b); // ints are implicitly converted to floats
    // F#
    let addFloat a:float b:float = a + b // for the purpose of the example, explictly state the a and b must be floats
    
    let value = addFloat 1 2 // will throw exception since 1 and 2 are ints
    
    // instead
    let a = float 1
    let b = float 2
    
    let value = addFloat a b

Other than this, F# contains all of the primitive data types that you will be used to in C# and many other languages.

Collections

Lists

Lists in F# are similar to those in C#, except that (because they are immutable) methods like Add/Remove do not exist. Instead, to perform the equivalent operations, new lists must be created from the existing list.

// 1. Declare a list
let list = [1;2;3] // creates a list with the elements 1, 2 and 3

// 2. Declare a list using range syntax
let list = [1..5] // create a list with the element 1, 2, 3, 4, and 5

// 3. Add an element to the start of a list
let list = [1;2;3]
let list2 = 4::list // create a list [4;1;2;3]

// 4. Remove elements from the list
let list = [1;2;3]
let list2 = list.[0..1] // returns a list containing the elements between index 0 and 1. Equivalent to removing the index 3

Sequences

Sequences are similar to lists, except they are lazily evaluated, meaning that the element values are not loaded in to memory until needed. This is useful for dealing with very large sequences. An equivalent very long list has to have all elements stored in memory simulataneously and may even crash your PC!

// large list
// WARNING: may crash your pc
let list = [1..1000000] // create a list containing 1 million elements

// large sequence
let sequence = seq {1..1000000}

Tuples

The collections we have looked at previously must contain data all with the same data type (i.e. a list of ints). A tuple is different in that it contain contain multiple values each with different data types.

// 1. declare a tuple
let myTuple = (1, "hello")

// 2. destructure a tuple
let myTuple = (1, "hello")
let (num, str) = myTuple // assigns num the value 1, and str the value "hello"

Records

Records group data into distinct objects, similar to classes. Records can even have method. However, as with most things in F#, records are immutable, so 'editing' a record requires you to create a new record from the existing one.

// 1. Declare a record type with a method
type Person = {
    Name: string; 
    Age: int;
} with member this.IsAdult = this.Age >= 18

// 2. Create a record
// Note that we don't need to tell the compiler that this is a Person record
// It can infer it from the fields we provide
let sam = { Name = "Sam"; Age = 27 }

// 3. Create a record based on an existing one
let olderSam = { sam with Age = 28 }

Discriminated Unions

Discriminated unions are a powerful feature in F# and may not be familiar to a lot of programmers - they certainly weren't to me! Discriminated unions allow values to be one of a number of named cases, each with potentially different types and values. This is probably shown that explained:

// 1. Basic discriminated union
// this declares a Status type, that can have the value Active or Inactive
type Status = Active | Inactive
let myStatus = Status.Active

// 2. A more complex example
type Shape = 
    | Rectangle of width:float * length:float // takes a tuple of two floats
    | Circle of radius:float // takes a float

// both have the Shape type
let cir = Circle 5.
let rect = Rectangle(5., 6.)

Pattern Matching

Pattern matching is very common in F# and is a way of controlling the flow of the application based on the match input value. It can be likened to using if..else statements in this manner, and if statements do exist in F#, but pattern matching is far more idiomatic (and hopefully you'll agree, more powerful).

// 1. Pattern matching a discriminated union
type Status = Active | Inactive

// return true if Active, return false if inactive
let isActive status = 
    match status with
    | Active -> true
    | Inactive -> false

// 2. Pattern matching to handle errors
type Result =
    | Error of string
    | Data of int

// handle printing data or error cases to the console
let handle result = 
    match result with
    | Data data -> printfn "My result: %i" data
    | Error error -> printfn "An error ocurred: %s" error

//3. Pattern matching tuple values
let matchPoint point = 
    match point with
    | (0, 0) -> printfn "This is the origin"
    | (x, 0) -> printfn "X is %i, while Y is 0" x
    | (0, y) -> printfn "Y is %i, while X is 0" y
    | _ -> printfn "Both values are non-zero" // _ is a catch all case

Some Functional Concepts

Currying and Partial Application

A pure function can only have one input and one output, and this is true for functions in F# as well. However, we've already seen some functions with apparently multiple parameters. For example:

let addInt x y = x + y

addInt 5 6 // evaluates to 11

Well, behind the scenes, F# is actually taking this declaration and splitting it into two separate functions, each with only one parameter. Something along the lines of:

let addInt x  =     
    let subFunction y = x + y
    subFunction 

let intermediate = addInt 5 // evaluates to a function that adds five to its parameter

intermediate 6 // evaluates to 11

This process of taking a multi-parameter function and splitting it into a pipeline of single parameter functions is called currying. This is all well and good, but what is the practical benefit for us? Well one of the interesting consequences is something called partial application.

Going back to our AddInt function in C#. It has two parameters, and you can only ever call it with two parameters. Calling it with one parameter will just lead to a compiler error.

However, if we call our addInt function in F# with just one parameter, something interesting happens. It returns a function that has our first parameter baked in! This is known as partial application, and is a highly useful concept for creating reusable pieces of code.

let addInt x y = x + y

let add5 = addInt 5 // partially apply addInt to create a new function that adds 5 to the input

add5 6 // evaluates to 11
add5 7 //evaluates to 12

Piping

Piping is a special type of operator that allows you to put the parameter to the function before the function. For example:

let addInt x y = x + y
let add5 = 5 |> addInt // pipe 5 into the addInt function

Its use may not seem immediately obvious , but it is highly useful for creating pipelines of functions, in which the output of one function feeds directly into the input of another.

[1; 2; 3] // create a list
|> List.map (fun x -> x + 1) // add 1 to each element of the list
|> List.filter (fun x -> x > 2) // only return elements that have a value greater than 2 (returns [3; 4])

Note that the expressions prefixed by `fun` above are simply lambda expressions (or anonymous functions) as you would see in many other languages.

Composition

Composition is the act of joining multiple functions together to create a single new function. This can seem pretty much the same as piping at first glance, but it is subtly different: a pipe has to start with a value and is immediately evaluated to give an output; function composition joins multiple functions together and returns that new function (i.e. nothing is evaluated at this stage).

// Example comparing piping and composition

let add5 = (+) 5
let double = (*) 2

// piping
5
|> add5
|> double // evaluates to 20

// composition
let add5ThenDouble = add5 >> double // compose into a new function

add5ThenDouble 5 // evaluates to 20

Conclusion

This has been my brief introduction into F# for Object Oriented Programmers. The topics I have covered are by no means exhaustive, but hopefully it is enough to whet your appetite for F# and functional programming.

For more resources F# For Fun and Profit is an excellent website and will help you dive deeper into the language.

I post mostly about full stack .NET and Vue web development (and soon maybe some more F# content!). To make sure that you don't miss out on any posts, please follow this blog and subscribe to my newsletter. If you found this post helpful, please like it and share it. You can also find me on Twitter.

Previously published here.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.