Shorter and simpler doesn’t always mean better. It is important how many bugs your code contain, how long it takes to develop it and to debug it, and how easy it is to test and maintain it. Roughly, the quality of code is the spent effort related to the provided functionality.
Speaking in terms of lines of code, types always add extra cost, so we might think that it is better to use languages without types, like Forth, or with dynamic types like Python. But types make it easier to understand programs, which reduces cognition burden and makes us more efficient, enables static analysis that detects common programmer errors before runtime that reduces the debugging time (which commonly contributes to 80% of the overall lifecycle of a program), and make our programs faster, which makes our programs more competitive and allows us to spend less time on optimization. Therefore, while types make programs more complex and larger they overall contribute positively to the final revenue figure. Therefore, my personal moto is,
Going back to your example. Whenever I run into code like
type t = Foo of x | Bar of x
I immediately see a refactoring opportunity. I try to factor out the common type x
as it is clear that we can write generic functions that ignore whether t
is Foo
or Bar
but depend only on x
. A common transformation1 is,
type kind = Foo | Bar
type t = { kind : kind; data : x}
or even,
type foo
type bar
type 'a t = {kind : 'a; data : x}
(* where 'a ranges over foo, bar *)
In case when we don’t need to dispatch over the kind at all, we can eschew it altogether, e.g.,
type foo
type bar
type 'a t = x
The trick type 'a t = x
is called phantom typing. To make the trick work we need to rely on the module system. Here is a nearly complete example, where we decided to represent both molecule
and atom
as integers. But at the same time we made everything to prevent confusion between molecule and atoms. We also declared a polymorhic type 'a element
for operations common between molecules and atoms. And we breached our abstraction a bit by declaring its implementation as int.
module Element : sig
type 'a t = private int
val create : 'a -> int -> 'a t
(* the rest of polymorphic element interface *)
end = struct
type 'a t = int
let create _ x = x
end
type 'a element = 'a Element.t
module Atom : sig
type cls
val create : string -> cls element
(* the rest of the atom-specific interface *)
end = struct
type cls = string
let registry = Hashtbl.create ()
let create name =
let id = position registry name in
Element.create name id
...
end
type atom = Atom.cls
module Molecule : sig
val create : atom element list -> molecule element
end = struct
type cls = atom element
let registry = Hashtbl.create ()
let create atoms =
let id = position registry atoms in
Element.create atoms id
end
type molecule = Molecule.cls
1) recall that algebraic types form algebra with sum and product types and compare it with the identity
(ax + bx) = (a + b)x