1

I'm learning SML and trying to make a datatype, called mySet, that can be any list of ints or reals, but with no duplicates and in sequential order. So far, I've made the datatype and some functions that do what I need to a list and then return it within that datatype which work fine. But I realized that the constructor for the datatype could also be used instead which completely bypasses the requirements. For what I need, I can just use the function, but I'd really like to know if there's any way I can patch up that problem? If a list doesn't follow the requirements, most of my functions for the datatype wouldn't work right.

datatype 'a set = Set of 'a list | Empty;

(* takes (item, list) and removes any copies of item from list *)
fun cleanList(a, []) = []
    |cleanList(a, b::rest) =
        if b = a then cleanList(a, rest)
        else
            b::cleanList(a, rest);

(*uses cleanList to make a list with all the items, no copies*)
fun removeDup([]) = []
| removeDup(a::rest) =
    let
        val cleanRest = cleanList(a, rest);
    in
        a::removeDup(cleanRest)
    end;


(*uses above 2 functions, then puts the list in order *)
fun makeSet([]) = Empty
    |makeSet(inputList) =
        let
            val cleanList = removeDup(inputList)
            val sortedList = ListMergeSort.sort (fn(x,y) => x > y) cleanList;
        in
            Set(sortedList)
        end;
        

val testList = [27, 81, 27, 3, 4, 5, 4, 27, 81, 3, 3, 7];

makeSet(testList); (* returns Set [3,4,5,7,27,81] *)

Set([1,1,1,1,1,1]); (*Set [1,1,1,1,1,1] which I don't want to allow *)

1 Answers1

0

I realized that the constructor for the datatype could also be used instead which completely bypasses the requirements. For what I need, I can just use the function, but I'd really like to know if there's any way I can patch up that problem?

There is! Your basic constructor will break your data type's invariants, so you want to hide it and only expose a smart constructor that fails deliberately on certain input and doesn't allow invalid states.

As molbdnilo says, this is called an abstract type because you hide the way it is implemented and expose it through its smart constructor interface, which has whatever behavior you want it to. You can also call it an opaque type.

What each method of achieving this have in common is that you have a local scope in which the datatype is declared, but where only the external interface of the smart constructor leaves. In the interest of exploring how few language features you need, I tried to simply write:

val (fmap, pure) =
  let
    datatype 'a maybe = Just of 'a | Nothing
    fun fmap f Nothing = Nothing
      | fmap f (Just x) = Just (f x)
    fun pure x = Just x
  in (fmap, pure)
  end

But my SML compiler actually rejected this program:

!   in (fmap, pure)
!       ^^^^
! Type clash: expression of type
!   ('a -> 'b) -> 'a maybe -> 'b maybe
! cannot have type
!   'c
! because of a scope violation:
! the type constructor maybe is a parameter 
! that is declared within the scope of 'c

So we need to whip out one of SML's language features designed specifically for this:


Update: @ruakh pointed out that I had forgotten about local.

Here is an example of the same thing using local-in-end:

local
  datatype 'a maybe = Just of 'a | Nothing
in
  fun fmap f Nothing = Nothing
    | fmap f (Just x) = Just (f x)
  fun pure x = Just x
end

The data type is shared between the two functions fmap and pure, but the definition is hidden from the external interface. You can have multiple local definitions including helper functions.

And the constructors are hidden:

> New type names: =maybe
  val ('a, 'b) fmap = fn : ('a -> 'b) -> 'a maybe -> 'b maybe
  val 'a pure = fn : 'a -> 'a maybe

let and local are discussed further in Difference between "local" and "let" in SML


Here is an example of the same thing using abstype:

abstype 'a maybe = Just of 'a | Nothing
with
  fun fmap f Nothing = Nothing
    | fmap f (Just x) = Just (f x)
  fun pure x = Just x
end

And you can see how Just and Nothing are hidden:

> New type names: maybe
  type 'a maybe = 'a maybe
  val ('a, 'b) fmap = fn : ('a -> 'b) -> 'a maybe -> 'b maybe
  val 'a pure = fn : 'a -> 'a maybe

But you can also use an opaque module. This StackOverflow answer covers precisely how the skeleton for a set functor works. Chapter 7 of ML for the Working Programmer covers how to define a module that takes a module as argument (a functor). This is an alternative to parametric polymorphism.

sshine
  • 15,635
  • 1
  • 41
  • 66
  • You can also fix your first version by using `local` instead of `let`, which IMHO is more readable anyway. The additional features of `abstype ... with ... end` over `local ... in ... end` are (1) that the `abstype` version automatically exposes the type constructor, whereas the `local` version doesn't (though you can expose it yourself with an explicit `type 'a maybe = 'a maybe` if you want) and (2) that `abstype` hides that this is an equality type, whereas `local` still allows client code to test whether e.g. `pure 3 = pure 3`. – ruakh Oct 04 '20 at 01:22
  • Thanks a lot! I actually meant to write the `local` example, too, and completely forgot about it. – sshine Oct 04 '20 at 04:19