4

When I only have datatype Nt = int | string, sml doesn't complain. But when I also have val n = 6 : Nt, ml doesn't accept 6 as an Nt. Why is this? I do know that, normally there should be data constructers before int and string, but here I'm having that to define functions that can take either int or string.

rem
  • 893
  • 4
  • 18

2 Answers2

10

You are misinterpreting the code. To be clear, you cannot define datatypes without constructors. But ML has different name spaces for types and values. The occurrences of int and string in your example are value identifiers. As such, they just define new nullary constructors, and have absolutely zero to do with the types of the same name. You can now define val n = int : Nt. It is as if you had written datatype Nt = foo | bar.

Andreas Rossberg
  • 34,518
  • 3
  • 61
  • 72
6

Having a function that can either take an int or a string can be interpreted in two ways. You could mean that you want a function that could take anything and do something general with it – that would be a polymorphic function. E.g.

fun id x = x

can take both ints and strings and return them, but not do much with their content in specific. If you want a function that can take either an int or a string and do something different with them, depending on which input you have, you could use a union type, e.g.

datatype Nt = Int of int      (* constructor has the type int -> Nt *)
            | Str of string   (* constructor has the type string -> Nt *)

val sample_1 = Int 42
val sample_2 = Str "Hello"

Here, Int and Str are value constructors that work like functions in that they take a value of type int/string, respectively, as argument and return a value of the union type Nt. I've named them something other than int and string to signify that the value constructors are different from the types int and string. If they did not take an argument, their only use would be to distinguish one from the other (in which case they would be isomorphic to true/false).

A function that takes such a value as input would have to match against the pattern constructors of the same names. Here are some functions that would take this union type as argument:

fun isAnInt (Int i) = true
  | isAnInt (Str s) = false

fun intVal (Int i) = i
  | intVal (Str i) = 0

fun strVal (Int i) = Int.toString i
  | strVal (Str s) = s

fun sumNt [] = 0
  | sumNt (x::xs) = intVal x + sumNt xs

fun concatNt [] = ""
  | concatNt (x::xs) = strVal x ^ concatNt xs

And here these functions are being tested:

val test_isAnInt_1 = isAnInt sample_1 = true
val test_isAnInt_2 = isAnInt sample_2 = false

val test_intVal_1 = intVal sample_1 = 42
val test_intVal_2 = intVal sample_2 = 0

val test_strVal_1 = strVal sample_1 = "42"
val test_strVal_2 = strVal sample_2 = "Hello"

val test_sumNt_1 = sumNt [] = 0
val test_sumNt_2 = sumNt [sample_1, sample_1, sample_2, sample_1] = 126
val test_sumNt_3 = sumNt [sample_2, sample_2, sample_2] = 0

val test_concatNt_1 = concatNt [] = ""
val test_concatNt_2 = concatNt [sample_1, sample_1, sample_1] = "424242"
val test_concatNt_3 = concatNt [sample_1, sample_2, sample_1] = "42Hello42"
sshine
  • 15,635
  • 1
  • 41
  • 66