Shane
Shane

Reputation: 1216

Trying to understand how functors are implemented in FSharpPlus

Can someone explain how this code works in F#:

https://github.com/fsprojects/FSharpPlus/blob/7b17b918d417b652fed1fea91e10c418cab9c4a6/src/FSharpPlus/Control/Functor.fs#L109

static member inline Invoke (mapping: 'T->'U) (source: '``Functor<'T>``) : '``Functor<'U>`` = 
    let inline call (mthd: ^M, source: ^I, _output: ^R) = ((^M or ^I or ^R) : (static member Map : (_*_)*_ -> _) (source, mapping), mthd)
    call (Unchecked.defaultof<Map>, source, Unchecked.defaultof<'``Functor<'U>``>)

Specifically the call function, which uses a syntax which I do not understand

e.g.

Upvotes: 2

Views: 243

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80915

This voodoo syntax is called "Statically Resolved Type Parameters", or SRTPs for short.

Normal type parameters are denoted with a tick, like 'a. These type parameters are natively supported by the .NET runtime, so types and functions with such type parameters are compiled directly to classes and methods in IL.

SRTPs are denoted with a circumflex, like ^M, and are not supported by the .NET runtime, and therefore cannot be compiled to IL, and therefore have to be "unrolled" during compilation time. This means that for every use site of thus parametrized function, the compiler must determine what concrete types the SRTPs should be, and insert the whole body of the function at call site, also known as "inlining". This is what the inline keyword is for.

The point of SRTPs is to express a constraint saying "any type that has a method or property with this signature". In your particular example, this is expressed on this line:

    let inline call (mthd: ^M, source: ^I, _output: ^R) = ((^M or ^I or ^R) : (static member Map : (_*_)*_ -> _) (source, mapping), mthd)
               ^^^^        ^^          ^^           ^^     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^
               (1)          \----------||-----------/                       (3)                        (4)                  (5)
                                       (2)

The line means:

  1. Define a function named call,
  2. With three tupled parameters of types ^M, ^I, and ^R respectively,
  3. And make sure that at least one of those types has a static method named Map,
  4. And that method takes a tuple (^I * ^R) * ^M,
  5. And we call that method passing it ((source, mapping), mthd) as parameter.

As to why it returns a tuple - it doesn't. The tuple on the last line is not returned from the Invoke function, but passed as parameter to the call function. It's the result of call that is returned from Invoke.

Upvotes: 5

Related Questions