1.7 Datatypes
So far, we have only seen built-in types like Number and (Listof String). Sometimes, it’s useful to define your own name as a shorthand for a type, such as defining Groceries to be equivalent to (Listof String):
(define-type-alias Groceries (Listof String)) (define shopping-list : Groceries '("milk" "cookies"))
Note that, by convention, all type names are capitalized. Plait is case-sensitive.
But what if the data that you need to represent is not easily encoded in existing types, such as when you need to keep track of a tiger’s color and stripe count (which doesn’t work as a list, since a list can’t have a number and a string)? And what if your type, say Animal, has values of different shapes: tigers that have color and stripe counts, plus snakes that have a color, weight, and favorite food?
The define-type form handles those generalizations. The general form is
(define-type Type (variant-name_1 [field-name_1 : Type_1] [field-name_2 : Type_2] ...) (variant-name_2 [field-name_3 : Type_3] [field-name_4 : Type_4] ...) ...)
with any number of variants and where each variant has any number of typed fields. If you’re used to Java-style classes, you can think of Type as an interface, and each variant is a class that implements the interface. Unlike Java classes, a variant name doesn’t work as a type name; it only works to create an instance of the variant.
For example, the following definition is suitable for representing animals that can be either tigers or snakes:
(define-type Animal (tiger [color : Symbol] [stripe-count : Number]) (snake [color : Symbol] [weight : Number] [food : String]))
After this definition, Animal can be used as a type, while tiger and snake work as functions to create Animals:
> (tiger 'orange 12) - Animal
(tiger 'orange 12)
> (snake 'green 10 "rats") - Animal
(snake 'green 10 "rats")
The definition of Animal creates several additional functions:
tiger?, which takes an Animal and determines whether it was created by tiger (as opposed to snake);
snake?, which takes an Animal and determines whether it was created by snake (as opposed to tiger);
tiger-color and tiger-stripe-count, which take a tiger Animal and extract its color and stripe count, respectively; and
snake-color, snake-weight, and snake-food, which take a snake Animal and extract its color, weight, and favorite food, respectively.
The name tiger? was formed by adding a ? to the end of the variant name tiger, tiger-color is formed by adding a - between the variant name and field name, and so on.
> (define tony (tiger 'orange 12)) > (define slimey (snake 'green 10 "rats")) > (tiger? tony) - Boolean
#t
> (tiger? slimey) - Boolean
#f
> (tiger-color tony) - Symbol
'orange
> (snake-food slimey) - String
"rats"
> (tiger-color slimey) - Symbol
tiger-color: contract violation
expected: tiger?
given: (snake 'green 10 "rats")
in: the 1st argument of
(->
tiger?
(or/c
undefined?
...pkgs/plait/main.rkt:1013:41))
contract from: tiger-color
blaming: use
(assuming the contract is correct)
at: eval:132:0
Note that the type of (tiger-color slimey) printed before an error was reported. That’s because (tiger-color slimey) is well-typed as far as Plait can tell, since tiger-color wants an Animal and slimey has type Animal. We’ll see that type-case provides an alterntive to selectors like tiger-color that is less dangerous than the selector.
Using Animal as a type and the tiger? and snake? predicates, we can write a function that extracts the color of any animal:
> (define (animal-color [a : Animal]) : Symbol (cond [(tiger? a) (tiger-color a)] [(snake? a) (snake-color a)])) > (animal-color tony) - Symbol
'orange
> (animal-color slimey) - Symbol
'green
When writing animal-color, what if we forget the snake? case? What if we get snake-color and tiger-color backwards? Unfortunately, the type checker cannot help us detect those problems. If we use type-case, however, the type checker can help more.
The general form of a type-case expresison is
(type-case Type value-expression [(variant-name_1 field-var_1 field-var_2 ...) result-expression_1] [(variant-name_2 field-var_3 field-var_4 ...) result-expression_2] ...)
The value-expression must produce a value matching Type. Every variant of Type must be represented by a clause with a matching variant-name. For that clause, the number of field-vars must match the declared number of fields for the variant. The type checker can check all of those requirements.
To produce a value, type-case determines the variant that is instanited by the result of value-expression. For the clause matching that variant (by name), type-case makes each field-var stand for the corresponding field (by position) within the value, and then evaluates the corresponding result-expression. Here’s animal-color rewritten with type-case:
> (define (animal-color [a : Animal]) : Symbol (type-case Animal a [(tiger col sc) col] [(snake col wgt f) col])) > (animal-color tony) - Symbol
'orange
> (animal-color slimey) - Symbol
'green
Put the definitions of Anmal and animal-color in DrRacket’s definitions area. Then, you can mouse over a in animal-color to confirm that it means the a that is passed as an argument. Mouse over col to see that it means one of the variant-specific fields. Try changing the body of animal-color to leave out a clause or a field variable and see what error is reported when you hit Run.
You should think of type-case as a pattern-matching form. It matches a value like (tiger 'orange 12) to the pattern (tiger col sc) so that col stands for 'orange and sc stands for 12. A value like (snake 'green 10 "rats") does not match the pattern (tiger col sc), but it matches the pattern (snake col wgt f).
At the end of Lists, we saw a got-milk? function that uses cond, similar to the way the dangerous version of animal-color uses cond. The type-case form works on list types with empty and (cons fst rst) patterns, so here’s an improved got-milk?:
> (define (got-milk? [items : (Listof String)]) (type-case (Listof String) items [empty #f] [(cons item rst-items) (or (string=? item "milk") (got-milk? rst-items))])) > (got-milk? empty) - Boolean
#f
> (got-milk? '("cookies" "milk")) - Boolean
#t
Note that there are no parentheses around empty in
got-milk?. That’s because empty is never called as a
constructor function—
> (define-type Grade (letter [alpha : Symbol]) (pass-fail [pass? : Boolean]) (incomplete)) > (letter 'A) - Grade
(letter 'A)
> (pass-fail #t) - Grade
(pass-fail #t)
> (incomplete) - Grade
(incomplete)
> (define (passed-course? [g : Grade]) : Boolean (type-case Grade g [(letter a) (not (eq? a 'F))] [(pass-fail p?) p?] [(incomplete) #f])) > (passed-course? (letter 'B)) - Boolean
#t
> (passed-course? (incomplete)) - Boolean
#f
You can also use else for a final clause in type-case to catch any variants that are not already covered.
> (define (high-pass? [g : Grade]) : Boolean (type-case Grade g [(letter a) (eq? a 'A)] [else #f])) > (high-pass? (letter 'A)) - Boolean
#t
> (high-pass? (incomplete)) - Boolean
#f
When you use else, however, the type checker is less helpful for making sure that you’ve considered all cases.