官术网_书友最值得收藏!

  • Testing with F#
  • Mikael Lundin
  • 2320字
  • 2021-07-23 20:46:13

Immutability

The default behavior of F# when using the let keyword is an immutable value. The difference between this and a variable is that the value is not possible to change. With this, there are the following benefits:

  • It encourages a more declarative coding style
  • It discourages side effects
  • It enables parallel computation

The following is an example implementation of the String.Join function using a mutable state:

// F# implementation of String.Join
let join (separator : string) list = 
    // create a mutable state
    let mutable result = new System.Text.StringBuilder()
    let mutable hasValues = false
        
    // iterate over the incoming list
    for s in list do
        hasValues <- true
        result
            .Append(s.ToString())
            .Append(separator)
            |> ignore
            
    // if list hasValues remove last separator
    if hasValues then
        result.Remove(result.Length - separator.Length, separator.Length) |> ignore

    // get result
    result.ToString() 

In this example, we used a string builder as a state where values were added together with a separator string. Once the list was iterated through, there was one trailing separator that needed to be removed before the result could be returned. This is of course true only if the sequence was nonempty.

This is how it could be written in an immutable way:

// F# implementation of String.Join
let rec join separator = function
    | [] -> ""
    | hd :: [] -> hd.ToString()
    | hd :: tl -> hd.ToString() + separator + (join separator tl) 

This implementation is quite stupid, but it is proving the point of immutability. Instead of having a state that is mutated, we used recursion to apply the same method once again with different parameters. This is much easier to test, as there are no moving parts within the function. There is nothing to debug, as there is no state that is changing.

If one were to create an expressive solution to this problem, it would rather look like this:

// F# implementation of String.Join
let join separator = 
    // use separator to separate two strings
    let _separate s1 s2 = s1 + separator + s2
    // reduce list using the separator helper function
    List.reduce _separate 

In this implementation, we add a few constraints by narrowing down the specification. Only a list of strings are now allowed, and the implementation will throw a System.ArgumentException exception when it encounters an empty list. This is OK if it's a part of the specification.

The problem itself is a reduce problem, so it is natural to use the higher order reduce function to solve it. All of this was, of course, a matter of exercise. You should always use the built-in String.Join function and never roll your own.

This is where you can see the functional programming excel. We moved from a 20 Lines of Code (LOC) mutable code example to a 3 LOC immutable code example.

Less code makes it easier to test, and less code may also reduce the need for testing. Each line of code brings complexity, and if we can bring down the number of lines of code by writing terser code, we also reduce the need for testing.

Immutable data structures

As we have seen, the immutable value is default in F# compared to the mutable variable in other languages. But the statement that F# makes on immutability doesn't end here. The default way of creating types in F# makes for immutable types.

An immutable type is where you set values upon creation of an instance, and each attempt to modify the state of the instance will result in a new instance. The most comprehensive example in .NET is DateTime.

This makes it possible for us to use function chaining like this:

// Now + 1 year + 1 month + 1 day
> System.DateTime.Today.AddYears(1).AddMonths(1).AddDays(1.) 

In F#, we define a new type like this:

> type Customer = { FirstName : string; Surname : string; CreditCardNumber: string }
> let me = { FirstName = "Mikael"; Surname = "Lundin"; CreditCardNumber = "1234567890" }

Now if I update the credit card number, it generates an new instance.

let meNext = { me with CreditCardNumber = "2345678901" } 

There are many benefits of doing this:

  • If you are processing the me parameter, it will not change its state, making async operations safe.
  • All data that belongs to the me parameter is accurate at the point in time when the me parameter was created. If we change me, we lose this consistency.

Fewer moving parts also make it easier to test code, as we don't have to care about the state and can focus on the input and output of functions. When dealing with systems such as trading and online ordering, immutability has become such a major player that now there are immutable databases. Take a look at Datomic and validate how an immutable database fits into immutable code.

Built-in immutable types

In order to support immutability throughout the language, there are some immutable types that come with the F# framework. These support pretty much any kind of functional computation that you would need, and with them, you can compose your own immutable type.

Most of the already defined types and data structures within the .NET framework are mutable, and you should avoid them where you can and sometimes create your own immutable versions of the functionality. It is important to find a balance here between cost versus value.

Tuple

Tuple is one of the most common data types with the F# language and is simply a way of storing two or several values in one container.

The following code is an example of a tuple:

// pattern matching
let tuple = (1, 2)
let a, b = tuple
printfn "%d + %d = %d" a b (a + b) 

A tuple is immutable because once it's created, you cannot change the values without creating a new tuple.

Another important aspect of the tuple is how it maps to the out keyword of C#. Where the .NET framework supports out variables, it will be translated into a tuple in F#. The most common usage of the out keyword is the Try pattern, as follows:

// instead of bool.TryParse
// s -> bool choice
let parseBool s =
    match bool.TryParse(s) with
    // success, value
    | true, b  -> Some(b)
    | false, _ -> None 

The code maps the bool.TryParse functionality into the choice type, which becomes less prone to error as it forces you to handle the None case, and it only has three combinations of values instead of four, as with the tuple result.

This can be tested as follows:

[<Test>]
let ``should parse "true" as true`` () =
    parseBool "true" |> should equal (Some true)

[<Test>]
let ``should parse "false" as false`` () =
    parseBool "false" |> should equal (Some false)
    
[<Test>]
let ``cannot parse string gives none`` () =
    parseBool "FileNotFound" |> should equal None 

List

The list type is a very used collection type throughout F#, and it has very little in common with its mutable cousin: the System.Collections.Generic.List<T> type. Instead, it is more like a linked list with a closer resemblance to Lisp.

The following image shows the working of lists:

List

The immutable list of F# has the following properties:

  • Head
  • IsEmpty
  • Item
  • Length
  • Tail

This is enough to perform most computations that require collections. Here is an example of a common way to build lists with recursion:

// not very optimized way of getting factors of n
let factors n = 
    let rec _factors = function
    | 1 -> [1]
    | k when n % k = 0 -> k :: _factors (k - 1)
    | k -> _factors (k - 1)

    _factors (n / 2)

One strength of the list type is the built-in language features:

// create a list
> [1..5];;
val it : int list = [1; 2; 3; 4; 5]

// pattern matching
let a :: b :: _ = [1..5];;
val b : int = 2
val a : int = 1

// generate list
> [for i in 1..5 -> i * i];;
val it : int list = [1; 4; 9; 16; 25]

Another strength of the list type is the list module and its higher order functions. I will give an example here of a few higher order functions that are very powerful to use together with the list type.

The map is a higher order function that will let you apply a function to each element in the list:

// map example
let double = (*) 2
let numbers = [1..5] |> List.map double

// val numbers : int list = [2; 4; 6; 8; 10] 

The fold is a higher order function that will aggregate the list items with an accumulator value:

// fold example
let separateWithSpace = sprintf "%O %d"
let joined = [1..5] |> List.fold separateWithSpace "Joined:"

// val joined : string = "Joined: 1 2 3 4 5" 

The nice thing about the fold function is that you can apply it to two lists as well. This is nice when you want to compute a value between two lists.

The following image is an example of this, using two lists of numbers. The algorithm described is called Luhn and is used to validate Swedish social security numbers. The result of this calculation should always be 0 for the Social Security Number (SSN) to be valid:

List

Here is a perfect situation where you want to compute a value between two lists:

// fold2 example
let multiplier = [for i in [1..12] -> (i % 2) + 1] // 2; 1; 2; 1...
let multiply acc a b = acc + (a * b)
let luhn ssn = (List.fold2 multiply 0 ssn multiplier) % 10

let result = luhn [1; 9; 3; 8; 0; 8; 2; 0; 9; 0; 0; 5]

// val result : int = 0

The partition is an excellent higher order function to use when you need to split values in a list into separate lists:

// partition example
let overSixteen = fun x -> x > 16
let youthPartition = [10..23] |> List.partition overSixteen 

// val youthPartition : int list * int list =
//   ([17; 18; 19; 20; 21; 22; 23], [10; 11; 12; 13; 14; 15; 16])

The reduce is a higher order function that will not use an accumulator value through the aggregation like the fold function, but uses the computation of the first two values as a seed.

The following code shows the reduce function:

// reduce example
let lesser a b = if a < b then a else b
let min = [6; 34; 2; 75; 23] |> List.reduce lesser 

There are many more useful higher order functions in the List module that are free for you to explore.

Sequence

Sequence in F# is the implementation of the .NET framework's IEnumerable interface. It lets you get one element at a time without any other information about the sequence. There is no knowledge about the size of the sequence.

The F# sequence is strongly typed and quite powerful when combined with the seq computational expression. It will let us create unlimited length lists just like the yield keyword in C#:

// For multiples of three print "Fizz" instead of the number
// and for multiples of five print "Buzz"
// for multiples of both write "Fizzbuzz"
let fizzbuzz =
    let rec _fizzbuzz n =
        seq {
            match n with
            | n when n % 15 = 0 -> yield "Fizzbuzz"
            | n when n % 3 = 0  -> yield "Fizz"
            | n when n % 5 = 0 -> yield "Buzz"
            | n -> yield n.ToString()
                
            yield! _fizzbuzz (n + 1)
        }

    _fizzbuzz 1 

I'm sure you recognize the classic recruitment test. This code will generate an infinite sequence of fizzbuzz output for as long as a new value is requested.

The test for this algorithm clearly shows the usage of sequences:

[<Test>]
let ``should verify the first 15 computations of the fizzbuzz sequence`` () =
    fizzbuzz 
        |> Seq.take 15 
        |> Seq.reduce (sprintf "%s %s") 
        |> should equal "1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 Fizzbuzz"

Creating an immutable type

When you set out to create your own types, you have great tools that will help you start out with immutable versions. The most basic type declaration is the discriminated union:

// xml representation
type Node = 
    | Attribute of string * string
    | Element of Node list
    | Value of string 

A basic type is that of a record, which is in its basic setting an immutable type:

let quote = 
    Element
        [
            Attribute ("cite", "Alan Turing")
            Value "Machines take me by surprise with great frequency"
        ]

In order to change the value of an immutable type, you need to create a new copy of it with the value changed, a true immutable type:

// <blockquote cite="Alan Turing">Machines take me by surprise with great frequency</blockquote>

Once in a while, you need to create a class, and this is when you need to be careful to create an immutable type and not a mutable one. This can be identified by the following properties:

  • State can only be set upon creation
  • Change of state returns a copy of the instance
  • The class can only have references to other immutable types

This is an example of a good immutable class type:

type Vector = | X | Y | Z

type Point(x : int, y : int, z : int) =
        
    // read-only properties
    member __.X = x
    member __.Y = y
    member __.Z = z

    // update
    member this.Update value = function
    | X -> Point(value, y, z)
    | Y -> Point(x, value, z)
    | Z -> Point(x, y, value)

    override this.Equals (obj) =
        match obj with
        | :? Point as p -> p.X = this.X && p.Y = this.Y && p.Z = this.Z
        | _ -> false

    override this.ToString () = sprintf "{%d, %d, %d}" x y z 

When updating any value, it will generate a new instance of Point instead of mutating the existing one, which makes for a good immutable class.

主站蜘蛛池模板: 湖南省| 安多县| 定安县| 布尔津县| 内黄县| 上犹县| 兴宁市| 福鼎市| 柳河县| 莫力| 若羌县| 瓮安县| 井陉县| 兰州市| 柘城县| 邢台市| 江油市| 旌德县| 苏尼特左旗| 金昌市| 菏泽市| 同心县| 彩票| 凉山| 保康县| 尚义县| 瓦房店市| 扶绥县| 承德县| 嘉定区| 松潘县| 静宁县| 乌兰浩特市| 宿松县| 虞城县| 兴宁市| 什邡市| 杭州市| 虎林市| 广饶县| 随州市|