Header menu logo fantomas

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:

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:

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.

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.

type SynExpr
type bool = System.Boolean
type 'T list = List<'T>
val id: x: 'T -> 'T
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>

Type something to start searching.