-
Notifications
You must be signed in to change notification settings - Fork 0
Typedef
You can declare new composite types. For example if we didn't have lists already:
typedef lst(#t) { pair(#t, lst(#t)) | null }
Creates a new type lst(#t)
(list of t) and two constructors pair
and null
that construct
values of that type.#t
is a type variable and stands for any type during the definition.
Given the above, you can create a lst
of integers like this:
pair(1, pair(2, pair(3, null)))
In fact, the list
type is not built in, it is defined exactly like this in a standard prelude, specifically as:
typedef list(#t) { nil | cons(#t, list(#t)) }
[]
and @
being just syntactic sugar for calls to nil
and cons
.
Much like constants, type constructors can be used in the formal arguments to composite functions, where they act as templates for the actual arguments. This plays well with the pattern matching discussed previously:
fn lst_map {
(f, null) { null }
(f, pair(h, t)) { pair(f(h), lst_map(f, t)) }
}
Note the type of lst_map
is polymorphic: (#a -> #b) -> lst(#a) -> lst(#b)
, it doesn't care about
the actual types of #a
and #b
(see Type Notation for a discussion of ->
).
The sugared constructors are also usable here, so map
in the listutils.fn
library is
fn map {
(_, []) { [] }
(f, h @ t) { f(h) @ map(f, t) }
}
Here's another example, using typedef to create an enumeration:
typedef colours { red | green | blue }
fn colour_to_string {
(red) { "red" }
(green) { "green" }
(blue) { "blue" }
}
Type constructors that take no arguments, like red
, green
and blue
above, behave as constants of
the given type (colours
in this case.)
Here's yet another example, creating a binary tree with an insert function
let
typedef tree(#t) { branch(tree(#t), #t, tree(#t)) | leaf }
fn insert {
(v, leaf) {
branch(leaf, v, leaf)
}
(v, x = branch(left, v, right)) {
x
}
(v, branch(left, u, right)) {
if (v < u) {
branch(insert(v, left), u, right)
} else {
branch(left, u, insert(v, right))
}
}
}
in
print(insert(1, insert(3, insert(2, insert(4, leaf)))))
is
branch(
branch(
branch(leaf, 1, leaf),
2,
branch(leaf, 3, leaf)
),
4,
leaf
)
Again the type of insert is polymorphic: #t -> tree(#t) -> tree(#t)
, e.g. it doesn't care what type
#t
is, as long as it's consistent and supports the <
operator.
As a final, more complete example, this gist shows a lambda interpreter for a very small language written in F♮ making use of typedef.
The basic built-in types int
, char
, bool
, nothing
and list(t)
are available when you
want to constrain the types in a typedef more tightly. For example:
typedef named_list(#t) { named(list(char), list(#t)) }
The pseudo-type string
is an alias for list(char)
so the following
example will do exactly the same thing as the previous one:
typedef named_list(#t) { named(string, list(#t)) }
It was noted earlier that lists for example can only be of a single type, but the typedef
construct allows us to get around that restriction in a type safe way. For example:
typedef either(#t, #u) { left(#t) | right(#u) }
Allows us to create lists like:
mix = [ left(1), left(2), right("hello"), right("goodbye") ];
After this, the type of mix
is list(either(int, string))
. you could for example map
over this list with a function like:
fn len_or_val {
(left(v)) { v }
(right(v)) { list.length(v) }
}
Which is of type either(int, list(#a)) -> int
.
Another great example is the maybe
type. This is defined in the standard
prelude:
typedef maybe(#t) { nothing | some(#t) }
This can be very useful when writing functions like a dictionary lookup that can
legitimately fail to find a value. Rather than relying on some special out-of-bounds
value, or causing an error in this situation, you return a maybe
of the value, forcing
your caller to handle the situation:
// lookup:: dict(#k, #v) -> #k -> maybe(#t)
let
fn lookup(dict, key) {
....
some(value)
...
nothing
}
in
...
switch(lookup(d, k)) {
(some(v)) { // handle success
}
(nothing) { // handle failure
}
}
While the type declarations described above are suitable for most purposes, it is sometimes useful
to be able to explicitly name the components of a constructor. So there is an alternative syntax
for a constructor within a typedef
as follows:
typedef named_list(#t) { nl{ name: list(char), list: list(#t) } | ... }
This form of constructor with curly braces instead of round braces and tags identifying each component
can be freely mixed with the other style in a single typedef
. Being able to name the fields means
you don't have to add explicit wildcards for the components you aren't interested in when pattern
matching:
fn printName (nl{ name: x }) { print(x) }
The downside of this type of constructor is that they cannot be curried as normal constructors can.
x = nl{ name: "mine", list: [1, 2, 3] }; // all fields must be supplied.
There exists a very primitive aliasing system for types, basically
alias <name> = <type>;
so for example the string
alias mentioned earlier is defined in the preamble as
alias string = list(char);
Really the aliasing system was only defined to support that single use-case, you can't currently even access aliases defined in another namespace. I may extend it later to allow that, and to take arguments.
Next: Print
CEKF(s) a.k.a. F Natural a.k.a F♮