F#
New to F#?
If you are truly brand-new to the F# language, you might want to start by reading the F# documentation of Microsoft.
Some other great resources (in no particular order) are:
- Essential F#
- F# for Fun and Profit
- F# Fundamentals Tutorial | Learn Functional Programming | Step-by-Step Guide
- F# Foundation Slack
- F# on Discord
Used F# features
F# has a lot of nice language features, although not all of them are used in Fantomas. We wish to highlight the most important ones that we use before continuing:
-
Partial active patterns, these are heavily used in
SourceParser.fs
. In short, we use theUntyped Abstract Syntax Tree
created by the F# parser, we don't use all the information in that tree to restore the source code.
For example SynExpr.For, the definition looks like:
type SynExpr =
...
/// F# syntax: expr; expr
///
/// isTrueSeq: false indicates "let v = a in b; v"
| Sequential of
debugPoint: DebugPointAtSequential *
isTrueSeq: bool *
expr1: SynExpr *
expr2: SynExpr *
range: range
However, in Fantomas we have a partial active pattern that we use to easily grab the information we need from the AST.
These partial actives are mostly used and defined in ASTTransformer.fs
.
let (|Sequentials|_|) e =
let rec visit (e: SynExpr) (finalContinuation: SynExpr list -> SynExpr list) : SynExpr list =
match e with
| SynExpr.Sequential(_, _, e1, e2, _) -> visit e2 (fun xs -> e1 :: xs |> finalContinuation)
| e -> finalContinuation [ e ]
match e with
| SynExpr.Sequential(_, _, e1, e2, _) ->
let xs = visit e2 id
Some(e1 :: xs)
| _ -> None
Notice the underscores, we don't use the DebugPointAtSequential
info and range
, so we drop that information in the result of the partial active pattern.
- Custom operators. In F# there are some special operators like |> and >>.
Note that these are just functions themselves as well. Instead of specifying all the arguments after the function name, (infix) operators let you specify an argument before the operator and after.
In F#, you are able to create your own operators as well. In Fantomas, the most notable are !-
and +>
. We will cover them later, but if you peek in CodePrinter.fs
, they are heavily used there.
In Fantomas we use signature files to define the module boundaries. Everything that is both defined in the implementation file (the *.fs
file) and in the signature file (the *.fsi
file) is considered to be visible to other modules.
If a signature file is present, there is no need to specify private
in a function you don't want to be visible to other modules. Just don't add a val
entry to the signature file and it will be private automatically.
You can look at a signature file to get a glimpse of what the module really does.
In contrast to partial active patterns, where we want to hide some AST information, it can occur that we need to extend the type of an AST node.
We do this by adding a new type member to an existing Syntax tree type.
Example in Trivia.fs
:
type CommentTrivia with
member x.Range =
match x with
| CommentTrivia.BlockComment m
| CommentTrivia.LineComment m -> m
The type SynMemberFlags
does not expose any range information, but we can extend it to do so.
The .FullRange
naming convention is used to indicate that we are not satisfied by the original range or it is lacking all together.
Don't worry just yet about this implementation, so keep in mind that with this feature we can later use memberFlags.FullRange
on a SynMemberFlags
instance.
This is well-known concept in F# and for completion sake we do mention this. In F#, you can pass a function as an argument to another function. Fantomas is full of this kind of functions, so be sure to grasp this concept before continuing.
There are places in the code base where we use some more advanced recursion techniques. ASTTransformer.fs
is one of them.
A very good explanation of what happens here can be found in this blogpost.
We use event sourcing to capture the instructions on how to write the new code. Instead of writing the new code directly to for example a StringBuilder
, we write it to a list of events.
That list of events will contain instructions like Write "let"
, IndentBy 4
, WriteLine
etc. So it is useful to have some notion of event sourcing.
Although, it really is an implementation detail in Context.fs
, think of it as writing a letter with a pen and a paper.
We first rehearse what we want to say, then we write the letter. Not write evey word as we are making up the letter, but write the letter as a whole once we know the content.
These events are used to achieve this.