In Moscow ML we deliberately did not attempt to guess what type identifier the programmer would like to see when reporting the type of a defined value; all types are expanded to their basic constituents.

Surely you are familiar with the following points, but students may not be, and hence may be misled:

The user-defined type identifiers are purely cosmetic in SML, and therefore may give the programmer a false sense of type security. E.g. define

   type dollars = int;
   type cents   = int;
   type amount = dollars * cents
   fun add ((d1, c1) : amount, (d2, c2) : amount) = (d1+d2, c1+c2) : amount
and one ML systems would report add : amount * amount -> amount. However, one would get exactly the same reassuring type if one had mistakenly defined:
   fun add' ((d1, c1) : amount, (d2, c2) : amount) = (c1+c2, d1+d2) : amount
[Incidentally, the other system you mention will report
   add : ((cents * cents) * (cents * cents)) -> (cents * cents) 
in both cases.] One may go on to define (by mistake?) a mixture of cents and dollars
   val x = 4  : dollars;
   val y = 99 : cents;
   val z1 = [x, y];
   val z2 = [y, x];
and one ML system says that
   val z1 = [4,99] : dollars list
   val z2 = [99,4] : cents list    
whereas the next ML system reports type cents list for both z1 and z2. The outcome is an artefact of the type checker, not the SML semantics.

Personally I have found the following two alternatives preferable:

  1. Either make a number of single-constructor datatypes
       datatype dollars = D of int
       datatype cents   = C of int
    

  2. Or use a record type with appropriate labels
       type amount = { dollars : int, cents : int }
    
    Both provide better type security and consistent type reporting across ML systems (and cost nothing in terms of runtime space and time overhead).

    Peter Sestoft