Skip to main content

SML Module Syntax Cheatsheet

By Thea Brick, January 2023

Signature

Signatures contain specifications which dictate what declarations a structure ascribing to said signature must make. A signature declaration appears as follows:

signature YOUR_SIGNATURE_NAME =
sig
(* zero or more specifications here *)
end

signature ANOTHER_NAME = YOUR_SIGNATURE_NAME

Signatures are generally use all capital letters.

Specifications

The following may appear in a signature:

Specification Explanation
val x : int

Structure must declare a variable called x with type int.

val fact : int -> int

Structure must declare a variable called fact with type int -> int.

(* abstract type specification *)
type 'a t

Structure must declare a type 'a t. Called abstract because the structure defines the implementation.

(* concrete type specification *)
type 'a t = 'a list

Structure must declare a type 'a t that is 'a list. Called concrete because the signature defines the implementation.

datatype 'a tree = Empty
| Node of 'a tree * 'a * 'a tree

Structure must declare datatype 'a tree = Empty | Node of 'a tree * 'a * 'a tree.

exception Exn

Structure must declare an exception Exn.

structure Str : SIG

Structure must declare a structure Str ascribing to SIG.

Structures

A structure is a series declaration which ascribe (or match) the signature.

structure YourStructure : YOUR_SIGNATURE =
struct
(* zero or more declarations matching YOUR_SIGNATURE *)
end

The structure must at least have every declaration specified in the signature, but may contain more.

A structure may not contain signature and functor declarations.

The following syntax allows SML to infer the signature to the structure based on the declarations made:

structure YourStructure =
struct
(* declarations matching YOUR_SIGNATURE *)
end

Functors

A functor takes a structure ascribing to a signature and outputs a new structure.

functor YourFunctor(
(* zero or more specifications for the input structure *)
) : YOUR_OUTPUT_SIG =
struct
(* declarations matching YOUR_OUTPUT_SIG *)
end

Here is an example of using this syntax:

functor Combine(
val x : int
val fact : int -> ints
structure A : A_SIG
structure B : B_SIG
) =
struct
(* omitted, x, fact, A, and B may be used in here *)
end

Functor Syntax Sugar

If a functor is taking in only one structure, the following syntax may be used:

functor YourFunctor(YourStructure : YOUR_SIGNATURE) : YOUR_OUTPUT_SIG =
struct
(* declarations matching YOUR_OUTPUT_SIG, YourStructure may be used *)
end

Transparent and Opaque Ascription

The symbol : describes transparent ascription. The symbol :> describes opaque ascription. Both ascriptions only allow things specified to be used. Opaque limits this further by not letting the implementation of abstract types to be used. Here is an example:

structure Example :> sig
type 'a t (* abstract *)
val isEmpty : 'a t -> bool
end = struct
type 'a t = 'a list
val isEmpty = fn [] => true | _ => false
val test = 123
end

(* does not compile, but would if transparent ascription was used. *)
val res = Example.isEmpty []

(* will never compile *)
val res2 = Example.test

where Syntax

The where keyword allows for using opaque ascription while deliberately exposing specific abstract types. It appears after the signature where used.

signature EXAMPLE =
sig
type 'a t (* abstract *)
type 'a u
val isEmpty : 'a t -> bool
end
structure Example :> EXAMPLE where type 'a t = 'a list = struct
type 'a t = 'a list
type 'a u = int
val isEmpty = fn [] => true | _ => false
end

(* this will compile now *)
val res = Example.isEmpty []

(* this will not compile *)
val res2 : 'a Example.u = 123