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
Marks program elements that are no longer in use.
type ObsoleteAttribute =
  inherit Attribute
  new: unit -> unit + 2 overloads
  member DiagnosticId: string
  member IsError: bool
  member Message: string
  member UrlFormat: string

--------------------
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>
Represents a generic collection of key/value pairs.

TKey: The type of keys in the dictionary.
TValue: The type of values in the dictionary.

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>
Multiple items
val string: value: 'T -> string

--------------------
type string = String
Gets an ICollection`1 containing the keys of the IDictionary`2.

returns: An ICollection`1 containing the keys of the object that implements IDictionary`2.

property IDictionary.Keys: ICollection<string> with get
type unit = Unit
Defines methods to manipulate generic collections.

T: The type of the elements in the collection.

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

Type something to start searching.