Logo telplin

Motivation

Introducing signature files to an existing code base is tedious by hand. Writing the signatures is only half the work: you also have to decide what belongs in them, and the implementation files deserve a cleanup once the signatures are in charge.

Telplin does all of that in one verified run:

The result is a reviewable diff that pins down the actual API of every file, derived from the compiler's own type information rather than guesswork. Every default has an opt-out flag, see Usage.

The merits of signature files

Beyond documenting an API, signature files buy an F# code base a few concrete things.

Reference assemblies

Reference assemblies (<ProduceReferenceAssembly>true</ProduceReferenceAssembly> in the fsproj) let a build skip recompiling downstream projects when the public API did not change. That check hinges on a stable mvid, and without signature files it is fragile in F#: adding a let private binding can change the mvid even though the public API did not move. With signature files in place, the mvid only changes when a signature changes.

A snappier IDE

With partial type checking (enablePartialTypeChecking in the F# checker), the background checker of your IDE skips implementation files that are backed by a signature. Imagine the following file structure:

A.fsi
A.fs
B.fsi
B.fs
C.fs
D.fs

Opening D.fs requires type information for every file before it, but A.fs and B.fs can be skipped: their signatures carry the same information. In a large code base this makes the editor feel a lot snappier.

Faster compilation

Graph-based type checking (<ParallelCompilation>true</ParallelCompilation> in the fsproj) lets the compiler check files in parallel. Signature files help twice: an implementation is verified against its own signature in parallel, and files that depend on it only need the signature to check themselves.

What about --allsigs?

The F# compiler can generate signature files itself during a build, by adding <OtherFlags>--allsigs</OtherFlags> to the fsproj. Its output has improved considerably over the years, and it is a perfectly reasonable way to get valid signatures out of a build.

Generating the signature is also where --allsigs stops, and where Telplin starts. --allsigs exposes every declaration as is; Telplin checks what the rest of the project actually uses and trims each signature down to that, then cleans up the implementation files, lists the signatures in the project file, and verifies the whole project before writing anything. That is nowadays the main reason to reach for it.

Closer to the source

Telplin also stays closer to how the implementation is written. Given

module MyNamespace.MyModule

open System
open System.Collections.Generic

[<Literal>]
let Warning = "Some warning"

type Foo() =
    [<Obsolete(Warning)>]
    member this.Bar(x: int) = 0

    member this.Barry(x: int, y: int) = x + y
    member this.CollectKeys(d: IDictionary<string, string>) = d.Keys

--allsigs produces

module MyNamespace.MyModule

[<Literal>]
val Warning: string = "Some warning"

type Foo =

    new: unit -> Foo

    [<System.Obsolete (Warning)>]
    member Bar: x: int -> int

    member Barry: x: int * y: int -> int

    member
      CollectKeys: d: System.Collections.Generic.IDictionary<string,string> ->
                     System.Collections.Generic.ICollection<string>

while Telplin writes

module MyNamespace.MyModule

open System
open System.Collections.Generic

[<Literal>]
val Warning: string = "Some warning"

type Foo =
    new: unit -> Foo

    [<Obsolete(Warning)>]
    member Bar: x: int -> int

    member Barry: x: int * y: int -> int
    member CollectKeys: d: IDictionary<string, string> -> ICollection<string>

Both are valid, but Telplin keeps the open statements, leaves types unqualified, and formats the result with Fantomas, so the signature reads like the implementation it belongs to.

And since Telplin ships independently of the dotnet SDK, a fix lands shortly after it is merged instead of waiting for an SDK release.

namespace System
namespace System.Collections
namespace System.Collections.Generic
Multiple items
type LiteralAttribute = inherit Attribute new: unit -> LiteralAttribute

--------------------
new: unit -> LiteralAttribute
val Warning: string = "Some warning"
Multiple items
type Foo = new: unit -> Foo member Barry: x: int * y: int -> int member CollectKeys: d: IDictionary<string,string> -> ICollection<string>

--------------------
new: unit -> Foo
Multiple items
type ObsoleteAttribute = inherit Attribute new: unit -> unit + 2 overloads member DiagnosticId: string member IsError: bool member Message: string member UrlFormat: string
<summary> Marks program elements that are no longer in use.</summary>

--------------------
ObsoleteAttribute() : ObsoleteAttribute
ObsoleteAttribute(message: string) : ObsoleteAttribute
ObsoleteAttribute(message: string, error: bool) : ObsoleteAttribute
val this: Foo
val x: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val y: int
val d: IDictionary<string,string>
type IDictionary<'TKey,'TValue> = inherit ICollection<KeyValuePair<'TKey,'TValue>> inherit seq<KeyValuePair<'TKey,'TValue>> inherit IEnumerable override Add: key: 'TKey * value: 'TValue -> unit override ContainsKey: key: 'TKey -> bool override Remove: key: 'TKey -> bool override TryGetValue: key: 'TKey * value: byref<'TValue> -> bool member Item: 'TValue member Keys: ICollection<'TKey> member Values: ICollection<'TValue>
<summary>Represents a generic collection of key/value pairs.</summary>
<typeparam name="TKey">The type of keys in the dictionary.</typeparam>
<typeparam name="TValue">The type of values in the dictionary.</typeparam>
Multiple items
val string: value: 'T -> string

--------------------
type string = String
property IDictionary.Keys: ICollection<string> with get
<summary>Gets an <see cref="T:System.Collections.Generic.ICollection`1" /> containing the keys of the <see cref="T:System.Collections.Generic.IDictionary`2" />.</summary>
<returns>An <see cref="T:System.Collections.Generic.ICollection`1" /> containing the keys of the object that implements <see cref="T:System.Collections.Generic.IDictionary`2" />.</returns>
type unit = Unit
type ICollection<'T> = inherit seq<'T> inherit IEnumerable override Add: item: 'T -> unit override Clear: unit -> unit override Contains: item: 'T -> bool override CopyTo: array: 'T array * arrayIndex: int -> unit override Remove: item: 'T -> bool member Count: int member IsReadOnly: bool
<summary>Defines methods to manipulate generic collections.</summary>
<typeparam name="T">The type of the elements in the collection.</typeparam>

Type something to start searching.