Post by Paul Rubin
I don't know ML and am wondering if it has a solution for an annoying
situation that comes up in Haskell sometimes. I want to enforce a data
invariant using smart constructors, which means using the module system
to limit where new values in the datatype can be introduced. For
example, I might want a number type that tracks whether the number is
module Foo (Number, mkNumber) where
data Number = Prime Integer | Composite Integer
mkNumber :: Integer -> Number
mkNumber n | isPrime n = Prime n
| otherwise = Composite n
By not exporting the Prime and Composite constructors, the module
prevents someone from making a value like "Composite 5".
The problem is that I'd like outside of the module to be able to pattern
case n of
Prime p -> "has no factors";
Composite c -> "is factorable"
but there's no apparent nice way to do this in Haskell (there are some
ugly ways involving adding more types to the module, etc).
Does ML have a way to do this?
There is no way to have constructors visible in patterns but not expressions. For an abstract data type, the constructors can only be referenced inside the module (or abstype) that implements it. So this is just a question of finding a suitable interface to the module. For your example, it could provide
val getInt : Number -> Integer
val isPrime : Number -> bool
In the case when the most suitable interface is precisely the internal datatype representation, then you could expose the internal datatype declaration via another datatype name that is not abstract and provide a function to convert to that. Here's a modified version of your example but using odd/even rather than prime/composite:
signature TEST_1 =
val mkT : int -> t
val succ : t -> t
datatype dest =
Even of int
| Odd of int
val destT : t -> dest
structure Test1 :> TEST_1 =
datatype t =
Even of int
| Odd of int
fun mkT n = if n mod 2 = 0 then Even n else Odd n
val succ = fn Even n => Odd (n + 1) | Odd n => Even (n + 1)
datatype dest = datatype t
fun destT x = x
datatype dest is the external view of the abstract datatype t. Note that there is no actual duplication internally: see last two lines of the structure. So you can then do
case destT (mkT 2) of
Even _ => "even"
| Odd _ => "odd";
Although it is possible to write
this has type dest, not t, so you cannot do
succ (Even 3);
Personally, I would not encourage a module's interface to be strongly oriented towards the internal representation of its abstract type(s) but more oriented towards the required operations. This makes it easier to change the internal representation/implementation in future.