Syntax
Declaration
let f x y = x + y + 1
Binding made with
letkeywordBinds both name (
f) and parameters (xandy)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
returnkeyword 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:
funkeyword is 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
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 ❗
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
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
delegatetype or aninterface. 📍
Conversion to a Func
We can convert a lambda into a Func<> but explicitly by passing through its constructor:
Conversion to a LINQ Expression 🚀
Conversion to a LINQ Expression 🚀
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
function keywordDefine an anonymous function with an implicit
matchexpression surrounding the body.Short syntax equivalent to
fun x -> match x with.Takes 1 parameter which is implicit
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.
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.
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
recotherwise errorFS0039: ... is not definedVery common in F♯ to replace
forloopsBecause 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
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
Notes
Line 2:
TailCallattribute was added in F♯ 8 to indicate (to the compiler and to the reader) that the function should be tail recursive.Line 3:
loopis a name idiomatic for this type of recursive inner-function. It's "accumulator" parameter is usually namedacc, unless there is an obvious better name likecounthere.Line 8: we start the "loop" with
count = 0.Lines 5 and 6: the recursive calls to
loopare really the last call, hence the tail recursion.We can verify that the function is compiled as a
whileloop 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:
Notes
The
loopinner function is recursively reading thelist1, accessing the current item as theheadand passing thetailto 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
headis captured/accumulated in the continuation function built with a lambda that takes a list calledrest, put theheadon 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] :: list2The real implementation of
List.appendis 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.
Function overload
Warning
A function cannot be overloaded!
Each version of the functions should have a dedicated and unique name.
Example:
LINQ
Selectmethod has an overload taking the current index as additional parameterThe equivalent in the
Listmodule are the 2 functionsmapandmapi: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:
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♯:
Comes first in F♯, to enable its partial application:
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:
The other uses of inline functions relate to SRTP 📍
Last updated