[MLton] Combining signatures in SML'97
Vesa Karvonen
vesa.karvonen at cs.helsinki.fi
Sun Dec 24 22:43:12 PST 2006
Working on the extended basis library I noticed that I have duplicated
many specifications in various signatures. For example, both ARRAY and
VECTOR have the specs
val toList : 'a t -> 'a List.t
val isoList : ('a t, 'a List.t) Iso.t
Likewise ARRAY and BUFFER have the spec
val duplicate : 'a t -> 'a t
Also, INTEGER, INT_INF, and WORD have the specs
val isEven : t UnPr.t
val isOdd : t UnPr.t
val isZero : t UnPr.t
and there are many more examples. Instead of duplicating specs and
documentation, I'd like to be able to write signatures for concepts such
as "ORDERED", "SIGNED", and "BOUNDED" once and for all and then create
signatures by combining such concept (or mixin) signatures (using
include).
A common SML convention is to name the main (usually abstract) type of a
signature "t". Unfortunately, if two SML signatures have a specification
in common, they can not both be included side-by-side. Thus the "t"
convention effectively prevents combining signatures in SML'97. It is not
that having a naming convention would be bad. The problem is that the "t"
convention isn't a good fit to the language. Indeed, if, as described in
http://mlton.org/References#RamseyFisherGovereau05,
SML had "An Expressive Language of Signatures" then the convention would
be just fine. While the "t" convention is not appropriate for concept
signatures, it could be used in other kind of signatures.
So, instead of always using "t" or some artificial variation of "t" like
"also_t", "t'", or "t0", concept signatures need a naming convention that
ensures, as far as possible, that we can combine those signatures when it
makes sense. A natural convention is to name the types in a concept
signature after the name of the signature. For example, in a signature
named "ORDERED", the main type would be named "ordered":
signature ORDERED = sig
type ordered
val != : ordered BinPr.t
val < : ordered BinPr.t
val <= : ordered BinPr.t
val == : ordered BinPr.t
val > : ordered BinPr.t
val >= : ordered BinPr.t
val compare : ordered Cmp.t
val max : ordered BinOp.t
val min : ordered BinOp.t
end
In a signature named "SIGNED", the main type would be named "signed":
signature SIGNED = sig
type signed
val abs : signed UnOp.t
val copySign : signed BinOp.t
val sameSign : signed BinPr.t
val sign : signed -> Int.t
val signBit : signed UnPr.t
end
In a signature named "SCANABLE", the main type would be named "scanable":
signature SCANABLE = sig
type scanable
val scan : (Char.t, 'a) Reader.t -> (scanable, 'a) Reader.t
end
Likewise, in a signature named "BITWISE_OPERABLE", the main type would be
named, perhaps unsurprisingly, "bitwise_operable":
signature BITWISE_OPERABLE = sig
type bitwise_operable
val andb : bitwise_operable BinOp.t
val notb : bitwise_operable UnOp.t
val orb : bitwise_operable BinOp.t
val xorb : bitwise_operable BinOp.t
val << : bitwise_operable * Word.t -> bitwise_operable
val ~>> : bitwise_operable * Word.t -> bitwise_operable
end
When multiple types are needed the signature name can be used as a prefix.
For example, in a signature named "FORMATTABLE" one would have two types
named "formattable" (for the main type) and "formattable_format" (for the
other type):
signature FORMATTABLE = sig
type formattable
type formattable_format
val fmt : formattable_format -> formattable -> String.t
end
Note that, thanks to type inference, we rarely need to mention the names
of types except in signatures.
In case we have a variation of a concept, such as "SCANABLE_FROM_FORMAT"
which is a variation of "SCANABLE" we could perhaps make an exception and
use the shorter name as the overlapping value specifications already
prevent including both:
signature SCANABLE_FROM_FORMAT = sig
type scanable
type scanable_format
val scan :
scanable_format -> (Char.t, 'a) Reader.t -> (scanable, 'a) Reader.t
end
When mechanically combining concept signatures that often occur together,
we could use a convention of concatenating the names of the signatures
with "and" as in "FORMATTABLE_and_SCANABLE_FROM_FORMAT" and
"FORMATTABLE_and_SCANABLE":
signature FORMATTABLE_and_SCANABLE = sig
include FORMATTABLE
include SCANABLE where type scanable = formattable
end
signature FORMATTABLE_and_SCANABLE_FROM_FORMAT = sig
include FORMATTABLE
include SCANABLE_FROM_FORMAT
where type scanable = formattable
where type scanable_format = formattable_format
end
The use of the lowercase "and" is supposed to make it clear that the
signature is a simple combination. Note that had we used unprefixed type
name "format" in "FORMATTABLE" and "SCANABLE_FROM_FORMAT" we would not
have been able to combine the signatures.
As one can see these naming conventions make it possible to combine
signatures using include. As further examples, one could have in
signatures "INTEGER", "INT_INF", "REAL", and "WORD" something like
signature INTEGER = sig
eqtype int
(* ... *)
include FORMATTABLE_and_SCANABLE_FROM_FORMAT
where type formattable = int
where type formattable_format = StringCvt.radix
include ORDERED where type ordered = int
include SIGNED where type signed = int
end
signature INT_INF = sig
include INTEGER
(* ... *)
include BITWISE_OPERABLE where type bitwise_operable = int
end
signature REAL = sig
type real
(* ... *)
include FORMATTABLE_and_SCANABLE
where type formattable = real
where type formattable_format = StringCvt.realfmt
include ORDERED where type ordered = real
include SIGNED where type signed = real
end
signature WORD = sig
eqtype word
(* ... *)
include BITWISE_OPERABLE where type bitwise_operable = word
include FORMATTABLE_and_SCANABLE_FROM_FORMAT
where type formattable = word
where type formattable_format = StringCvt.radix
include ORDERED where type ordered = word
end
The main drawback of this convention is that the combined signature (and
any implementing structure) will have many equal types, but that isn't
much of a problem as SML can express type equality without revealing
implementation details.
-Vesa Karvonen
More information about the MLton
mailing list