Version: 4.2.3
PLAI Typed Language
The PLAI Typed language is a statically typed language that resembles
the PLAI language, though with a much smaller set of syntactic forms
and built-in functions.
The PLAI Typed language does not have an entry in DrScheme’s
Choose Language dialog. Instead, choose the
Module language, and write your program like this:
#lang plai-typed |
... all code goes here .... |
The default output style of the Module language does not
match the PLAI style. To fix it: In the Choose Language
dialog, after you select the Module language, click
Show Details, and the select Constructor output
style.
The body of a plai-typed module is a sequence of
definitions and expressions.
1 Definitions
In a define-type declaration, the contract/predicate position
for variant fields changes to a colon followed by a type. In addition,
define and lambda forms support type annotations on
arguments and for results. The syntax is otherwise merely restricted
from the normal PLAI language.
(define id expr) |
(define id : type expr) |
(define (id id/type ...) expr) |
(define (id id/type ...) : type expr) |
|
id/type | | = | | id | | | | | | [id : type] |
|
The definition form with optional type annotations. A type written
after (id id/type ...) declares the result type of a
function.
Matches multiple results produced (via
values) by
expr.
(define-type tyid/abs | [variant-id (field-id : type)] | ...) |
|
|
tyid/abs | | = | | id | | | | | | (id 'id ...) |
|
Define a type (when tyid/abs is id) or type
constructor (when tyid/abs has the form (id 'id ...)) with its variants.
2 Expressions
An expression can be a literal constant that is a number (type
number), a string (type string), a symbol (type
symbol) written with quote or ',
#t (type boolean), or #f (type
boolean). An expression also can be a bound identifier (in
which acse its type comes from its binding).
A symbol.
A function call, which is normally written without the
#%app
keyword.
A procedure. When a type is written after (id/ty ...), it
declares he result type of the function.
|
(cond [test-expr expr] ...) | (cond [test-expr expr] ... [else expr]) |
|
Conditionals. Each
test-exprs must have type
boolean.
Sequence.
(local [definition ...] expr) |
Local binding.
Boolean combination. The
exprs must have type
boolean.
Builds a list. All elems must have the same type.
(values elem ...) → ('a * ...) |
elem : 'a |
Types multiple values into one; the type of each
elem is
independent. Match a
values result using
define-values.
(type-case tyid/abs val-expr | [variant-id (field-id ...) expr] ...) |
|
(type-case tyid/abs val-expr | [variant-id (field-id ...) expr] ... | [else expr]) |
|
|
tyid/abs | | = | | id | | | | | | (id 'id ...) |
|
Variant dispatch, where val-expr must have type
tyid/abs.
Either returns expr’s result or catches an exception raised
by expr and calls handle-expr.
List primitives.
Boolean primitive.
Numeric primitives.
Symbol primitive.
String primitives.
Comparison primitives.
Error primitive
Output primitive
Box primitives.
Vector primitives.
Test primitive forms that do not actually produce a void
value. Instead, they produce results suitable for automatic display
through a top-level expression. The
void type merely prevents your
program from using the result.
3 Types
Primitive types.
Type for a function.
Types for tuples.
Type for the empty tuple.
Type for a list of elements.
Type for a mutable box.
Type for a vector of elements.
4 Type Checking and Inference
Type checking and inference is just as in ML (Hindley-Milner), with
a few small exceptions:
Functions can take multiple arguments, instead of requring a tuple
of arguments. Thus, (number number -> number) is a different type
than either ((number * number) -> number), which is the tuple
variant, or (number -> (number -> number)), which is the curried
variant.
Since all top-level definitions are in the same
mutually-recursive scope, the type of a definition’s right-hand
side is not directly unified with references to the defined
identifier on the right-hand side. Instead, every reference to an
identifier – even a reference in the identifier’s definition – is
unified with a instantiation of a polymorphic type inferred for the
definition. Of course, the usual value restriction applies for
inferring polymorphic types.
Compare OCaml:
# let rec f = fun x -> x |
and h = fun y -> f 0 |
and g = fun z -> f "x";; |
This expression has type string but is here used with type int |
with
(define (f x) x) |
(define (h y) (f 0)) |
(define (g y) (f "x")) |
; f : ('a -> 'a) |
; h : ('a -> number) |
; g : ('a -> string) |
A minor consequence is that polymorphic recursion (i.e., a self
call with an argument whose type is different than that for the
current call) is allowed. Recursive types, however, are prohibited.
Since all definitions are recursively bound, and since the
right-hand side of a definition does not have to be a function, its
possible to refer to a variable before it is defined. The type
system does not prevent “reference to identifier before
definition” errors.
Type variables are always scoped locally within a type expression.
Compare OCaml:
# function (x : 'a), (y : 'a) -> x;; |
- : 'a * 'a -> 'a = <fun> |
with
> (lambda ((x : 'a) (y : 'a)) x) |
- ('a 'b -> 'a) |
|
> (define f : ('a 'a -> 'a) (lambda (x y) x)) |
> f |
- ('a 'a -> 'a) |
When typechecking fails, the error messages reports and highlights (in
pink) all of the expressions whose type contributed to the
failure. That’s often too much information. As usual, explicit type
annotations can help focus the error message.