githubEdit

Syntax

Declaration

let f x y = x + y + 1

  • Binding made with let keyword

  • Binds both name (f) and parameters (x and y)

  • Optional type annotations for parameters and/or returned value

    • let f (x: int) (y: int) : int = ...

  • Otherwise, they will be inferred, eventually with an automatic generalization

  • The function body is an expression:

    • → no need for a return keyword here.

    • → possibility to define intermediary variables and functions inside the body

Generic function

  • In many cases, inference works with automatic generalization

    • let listOf x = [x](x: 'a) -> 'a list

  • Explicit inline annotation of generic parameters

    • let f (x: 'a) = ...

  • Explicit inline annotation with generic type inference

    • let f (list: list<_>) = ...

  • Full explicit annotation of generic parameters

    • let f<'a> (x: 'a) = ...

    • Pros: callers can specify the type parameter

Anonymous / Lambda function

Expression defining a function

Syntax: fun parameter1 parameter2 etc. -> expression

Note:

  • fun keyword is mandatory

  • Thin arrow -> ≠ Bold arrow => (C♯, JS)

Lambda: usage examples

1. As an argument to a high-order function

  • To avoid having to define a named function

  • Recommended for a short function, to keep it readable

circle-exclamation

2. In a let binding with inference

  • To make it explicit when the function returns a function

  • A kind of manual currying

  • Use sparingly ❗

3. In an annotated let binding

The function signature is pre-defined using a type alias

  • Force implementation to follow signature

  • 📗 Domain modelling made functional by Scott Wlaschin

circle-info

Parameter naming

  • We can't provide the names of the parameters in the type alias. ❌

  • But the parameters are named in the function signature. 👍

  • To get named parameters in the type itself, we can use a delegate type or an interface. 📍

Conversion to a Func

We can convert a lambda into a Func<> but explicitly by passing through its constructor:

Conversion to a LINQ Expression 🚀

chevron-rightConversion to a LINQ Expression 🚀hashtag

Contrary to C#, it's more complex in F♯ to write an Expression<Func<> from a lambda. You have to go through a quotation and then convert it into an expression using either the FSharp.Linq.RuntimeHelpers.LeafExpressionConverter for simple cases (see example below), or the NuGet package FSharp.Quotations.Evaluator for more complex cases.

function keyword

  • Define an anonymous function with an implicit match expression surrounding the body.

  • Short syntax equivalent to fun x -> match x with.

  • Takes 1 parameter which is implicit

circle-info

A matter of taste

  • For fans of point-free 📍

  • More generally, in a succession of pipes arg |> f1 |> function ... |> f2... → The idea is to pattern match a large expression without storing it in an intermediate variable.

Parameters deconstruction

  • As in JavaScript, you can deconstruct inline a parameter.

  • This is also a way of indicating the type of parameter.

  • The parameter appears without a name in the signature.

Example with a Record type 📍

This is also called pattern matching. → But I prefer to reserve this expression to match x with ...

Tuple parameter

  • As in C♯, you may want to group function parameters together.

  • They can be grouped together in a tuple and even deconstructed.

  • f (x, y, z) looks a lot like a C♯ method!

  • The signature indicates the change: (int * int * int) -> TResult.

    • The function now only has only 1 parameter instead of 3.

    • Loss of partial application of each element of the tuple.

circle-check

Conclusion

Recursive function

  • Function that calls itself

  • Special syntax with keyword rec otherwise error FS0039: ... is not defined

  • Very common in F♯ to replace for loops

    • Because it's often easier to design

    • And still performant thanks to tail recursion (details just after)

Example: find the number of steps to reach 1 in the Collatz conjecturearrow-up-right

Tail recursion

  • Type of recursion where the recursive call is the last instruction 🔗 Tail recursion | Wikipediaarrow-up-right

  • Detected by the compiler and rewritten into a simple loop → ✅ Better performance → ✅ Prevents stack overflow

💡 Accumulator argument

In general, we can transform a non-tail-recursive function into a tail-recursive one by adding an additional parameter, playing the role of "accumulator" like with fold/reduce functions - see Versatile aggregate functions 📍

→ Example: previous steps function rewritten as tail-recursive

circle-info

Notes

  1. Line 2: TailCall attribute was added in F♯ 8 to indicate (to the compiler and to the reader) that the function should be tail recursive.

  2. Line 3: loop is a name idiomatic for this type of recursive inner-function. It's "accumulator" parameter is usually named acc, unless there is an obvious better name like count here.

  3. Line 8: we start the "loop" with count = 0.

  4. Lines 5 and 6: the recursive calls to loop are really the last call, hence the tail recursion.

  5. We can verify that the function is compiled as a while loop in SharpLabarrow-up-right.

🚀 Continuation-passing style

Another common trick to write a tail-recursive function, although more advanced, is to accumulate values inside a series of continuation function calls.

→ Example: we can write List.append using this style:

circle-info

Notes

  • The loop inner function is recursively reading the list1, accessing the current item as the head and passing the tail to the next iteration of the loop.

  • Line 4: when we reach the end of list1, we call the continuation function, called here cont. Another usual name for the continuation function parameter is next.

  • Line 5: the head is captured/accumulated in the continuation function built with a lambda that takes a list called rest, put the head on top of it, and pass the resulting list to the previous version of the continuation function.

  • Let's unwrap the continuation functions: 0 ┬ fun rest0 -> id (list1[0] :: rest0) └ fun rest0 -> list1[0] :: rest0 1 ┬ fun rest1 -> (fun rest0 -> list1[0] :: rest0)(list1[1] :: rest1) └ fun rest1 -> list1[0] :: list1[1] :: rest1 ... N ┬ fun restN -> list1[0] :: list1[1] :: ... :: list1[N] :: restN └ list1[0] :: list1[1] :: ... :: list1[N] :: list2

  • The real implementation of List.append is different and is optimized using other techniques including mutation.

🔗 Continuation-passing style | Wikipediaarrow-up-right 🔗 Understanding continuations | fsharp for fun and profitarrow-up-right

Mutually recursive functions

  • Functions that call each other

  • Must be declared together:

    • 1st function indicated as recursive with rec.

    • other functions added to the declaration with and.

Function overload

circle-exclamation

Warning

Example:

  • LINQ Select method has an overload taking the current index as additional parameter

  • The equivalent in the List module are the 2 functions map and mapi: List.map (mapping: 'T -> 'U) (items: 'T list) : 'U list List.mapi (mapping: (index: int) -> 'T -> 'U) (items: 'T list) : 'U list → See map vs mapi 📍

Template function

Create specialized "overloads"

Example: helper wrapping String.Compare:

circle-exclamation

Parameter order

The additional parameter is placed at a different location in C♯ and F♯:

  • Comes last in C♯:

  • Comes first in F♯, to enable its partial application:

inline function

Principle

Inline extansion ou inlining : compiler optimization to replace a function call with the its body. 🔗 Inline expansion | Wikipediaarrow-up-right

  • Performance gain 👍

  • Longer compilation ⚠️

💡 Same as the refactoring Inline Methodarrow-up-right

inline keyword

  • Tells the compiler to "inline" the function

  • Typical usage: small "syntactic sugar" function/operator

Examples from FSharp.Corearrow-up-right:

The other uses of inline functions relate to SRTP 📍

Last updated