Syntax
Declaration
let f x y = x + y + 1
Binding made with
let
keywordBinds both name (
f
) and parameters (x
andy
)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 mandatoryThin 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
[1..10] |> List.map (fun i -> i + 1) // 👈 () around the lambda
// Alternative with a named function
let add1 i = i + 1
[1..10] |> List.map add1
Beware of useless lambdas:
List.map (fun x -> f x)
❌
List.map f
✅
2. In a let
binding with inference
let
binding with inferenceTo make it explicit when the function returns a function
A kind of manual currying
Use sparingly ❗
let add x y = x + y // Regular version: auto curried
let add' x = fun y -> x + y // 1 nested lambda
let add'' = fun x -> (fun y -> x + y) // 2 nested lambdas
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
type Add = int -> int -> int
let add: Add =
fun x y ->
x + y
// val add: x: int -> y: int -> int
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 aninterface
. 📍
Conversion to a Func
We can convert a lambda into a Func<>
but explicitly by passing through its constructor:
let isNull = System.Func<_, _>(fun x -> x = null)
// val isNull: System.Func<'a,bool> when 'a: equality and 'a: null
Conversion to a LINQ Expression 🚀
function
keyword
function
keywordDefine 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
let ouiNon x =
match x with
| true -> "Oui"
| false -> "Non"
// val ouiNon: x: bool -> string
// 👉 Same with `function`:
let ouiNon =
function
| true -> "Oui"
| false -> "Non"
// val ouiNon: _arg1: bool -> string
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 📍
type Person = { Name: string; Age: int }
let name { Name = x } = x // Person -> string
let age { Age = x } = x // Person -> int
let age' person = person.Age // Alternative w/o deconstruction
let bob = { Name = "Bob"; Age = 18 } // Person
let bobAge = age bob // int = 18
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.
For the sake of cohesion, when these parameters form a whole.
To avoid the long parameter list code smell.
They can be grouped together in a tuple and even deconstructed.
// V1 : too many parameters
let f x y z = ...
// V2 : parameters grouped in a tuple
let f params =
let (x, y, z) = params
...
// V3 : idem with tuple deconstructed on site
let f (x, y, z) = ...
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.
Conclusion
Resist the temptation to use a tuple all the time (because it's familiar, a habit from C♯)
Use it only when it makes sense to group parameters together.
If relevant, even use a dedicated (anonymous) record type. 📍
Recursive function
Function that calls itself
Special syntax with keyword
rec
otherwise errorFS0039: ... is not defined
Very common in F♯ to replace
for
loopsBecause 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 conjecture
let rec steps (n: int) : int =
if n = 1 then 0
elif n % 2 = 0 then 1 + steps (n / 2)
else 1 + steps (3 * n + 1)
Tail recursion
Type of recursion where the recursive call is the last instruction 🔗 Tail recursion | Wikipedia
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
let steps (number: int) : int =
[<TailCall>]
let rec loop count n =
if n = 1 then count
elif n % 2 = 0 then loop (count + 1) (n / 2)
else loop (count + 1) (3 * n + 1)
loop 0 number
Notes
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.Line 3:
loop
is a name idiomatic for this type of recursive inner-function. It's "accumulator" parameter is usually namedacc
, unless there is an obvious better name likecount
here.Line 8: we start the "loop" with
count = 0
.Lines 5 and 6: the recursive calls to
loop
are really the last call, hence the tail recursion.We can verify that the function is compiled as a
while
loop in SharpLab.
🚀 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:
let append list1 list2 =
let rec loop list cont =
match list with
| [] -> cont list2
| head :: tail -> loop tail (fun rest -> cont (head :: rest))
loop list1 id
Notes
The
loop
inner function is recursively reading thelist1
, accessing the current item as thehead
and passing thetail
to the next iteration of the loop.Line 4: when we reach the end of
list1
, we call the continuation function, called herecont
. Another usual name for the continuation function parameter isnext
.Line 5: the
head
is captured/accumulated in the continuation function built with a lambda that takes a list calledrest
, put thehead
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 | Wikipedia 🔗 Understanding continuations | fsharp for fun and profit
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
.
// ⚠️ Convoluted algo, just for illustration purposes
let rec Even x = // 👈 Keyword `rec`
if x = 0 then true
else Odd (x-1) // 👈 Call to `Odd` defined below
and Odd x = // 👈 Keyword `and`
if x = 0 then false
else Even (x-1) // 👈 Call to `Even` defined above
Function overload
Warning
A function cannot be overloaded!
Each version of the functions should have a dedicated and unique name.
Example:
LINQ
Select
method has an overload taking the current index as additional parameterThe equivalent in the
List
module are the 2 functionsmap
andmapi
: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
:
type ComparisonResult = Bigger | Smaller | Equal // Union type 📍
let private compareTwoStrings (comparison: StringComparison) string1 string2 =
let result = System.String.Compare(string1, string2, comparison)
if result > 0 then
Bigger
elif result < 0 then
Smaller
else
Equal
// Partial application of the 'comparison' parameter
let compareCaseSensitive = compareTwoStrings StringComparison.CurrentCulture
let compareCaseInsensitive = compareTwoStrings StringComparison.CurrentCultureIgnoreCase
Note that compareTwoStrings
is declared as private
: to hide implementation details
.
Parameter order
The additional parameter is placed at a different location in C♯ and F♯:
Comes last in C♯:
String.Compare(String, String, StringComparison)
String.Compare(String, String)
Comes first in F♯, to enable its partial application:
compareTwoStrings : StringComparison -> String -> String -> ComparisonResult
compareCaseSensitive : String -> String -> ComparisonResult
inline
function
inline
functionPrinciple
Inline extansion ou inlining : compiler optimization to replace a function call with the its body. 🔗 Inline expansion | Wikipedia
Performance gain 👍
Longer compilation ⚠️
💡 Same as the refactoring Inline Method
inline
keyword
inline
keywordTells the compiler to "inline" the function
Typical usage: small "syntactic sugar" function/operator
Examples from FSharp.Core:
let inline ignore _ = ()
let inline (|>) x f = f x
let t = true |> ignore
// ~= ignore true // After the inlining of |>
// ~= () // After the inlining of ignore
The other uses of inline
functions relate to SRTP 📍
Last updated
Was this helpful?