Discussion:
ML and smart constructors question
(too old to reply)
Paul Rubin
2012-05-19 21:29:11 UTC
Permalink
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
prime:

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
match on the constructors, just not create new values:

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?

Thanks.
NatarovVI
2012-05-21 14:56:37 UTC
Permalink
Post by Paul Rubin
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
...
Post by Paul Rubin
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?
sure i'm only try my teeth on ML border but...

1) structure can match two or more signatures. f.e. one with
constructors, other - without. you can use first or second where needed.

2) you always can introduce and export function wich returns "data kind".
like "Prime" or "Composite". and match on result of such function,
not on data constructor itself.
if "match" and "constructor" must be different, let it be different?))
and even in Haskell this works?

or i don't understand your problem?
p***@lineone.net
2012-05-21 16:40:50 UTC
Permalink
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 =
sig
type t
val mkT : int -> t
val succ : t -> t

datatype dest =
Even of int
| Odd of int
val destT : t -> dest
end

structure Test1 :> TEST_1 =
struct
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
end

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

open Test1;
case destT (mkT 2) of
Even _ => "even"
| Odd _ => "odd";

Although it is possible to write

Even 3;

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.

Phil

Loading...