Type-oriented programming/Printable version
This is the print version of Type-oriented programming You won't see this message or any elements not part of the book's content when you print or preview this page. |
The current, editable version of this book is available in Wikibooks, the open-content textbooks collection, at
https://en.wikibooks.org/wiki/Type-oriented_programming
Types and properties
Types can be viewed of bundles of properties and functions operating on them. Such functions are conventionally called methods. Here’s an example of a simple type:
type Person { property name String property age Int }
This type declaration states that the Person
type has two properties. An instance of this type can be created using a new
statement:
var p = new Person { name = "Jane", age = 18 }
The properties of a type’s instance can be accessed via the dot operator, for example:
return p.name
NB: The pseudocode can be tried out using the Funcy app, which can be downloaded for free from
Apple’s App Store (iOS/macOS),
Google Play (Android) or Amazon Appstore. The code to be executed must be placed in a main {}
block. The example above can be tried out by running the following code:
type Person { property name String property age Int } main { var p = new Person { name = "Jane", age = 18 } return p.name }
Restricted properties
While subtyping is often ampliative, that is, a subtype contains all the properties of its supertype(s), it’s often useful to remove an inherited property from a subtype. For example, a complex has a real part and an imaginary part. Every real number is also a complex number but its imaginary part is always equal to zero so we don’t want this property to take up space in the instances. However we can’t remove the imaginary part altogether because it might be used by code working with complex numbers. What one can do is assign the property a fixed value:
type Complex { property real Float property imag Float func _add(x Complex) Complex { return new Complex { real=self.real+x.real, imag=self.imag+x.imag } } func _mul(x Complex) Complex { return new Complex { real=self.real*x.real-self.imag*x.imag, imag=self.real*x.imag+self.imag*x.real } } func description() String { return self.real.description() + "+" + self.imag.description() + "i" } } type Real : Complex { restrict imag Float = 0.0 } main { var x = new Complex { real=2.0, imag=3.0 } var y = new Real { real=2.0 } return (x*y).description() }
Note that the Real
type now has only one stored property, but the methods in the Complex
type can still be used with the subtype.
NB: The pseudocode can be tried out using the Funcy app, which can be downloaded for free from
Apple’s App Store (iOS/macOS),
Google Play (Android) or Amazon Appstore. The code to be executed must be placed in a main {}
block.
Self at the type level
Just like the self
variable refers contextually to the instance in whose method it’s used, the Self
type variable contextually refers to the type. Let’s have a look at a simple example:
type A { sfunc create() Self { return new Self } } type B : A {} main { var obj = $B.create() return obj }
The create
method’s return type is Self
so in the example above it refers to the B
type because we called the type method using $B.create()
.
Using Self
gives the type system considerable power since it squares well with type operators, as illustrated by the following example:
type Maybe[T] { property val T func description() String { return self.val.isNil() ? "none" : self.val.description() } func map[U](f Func[T,U]) Self[U] { return new Self[U] { val = self.val.isNil() ? nil : f(self.val) } } } main { var x = new Maybe[Int] { val=3 } var y = x.map[Float](\(x Int) Float . x.asFloat()) return y.description() + " " + y.typeName() }
Note that the map
method has a type argument and how new Self[U]
is used. In this case Self
is a contextual type operator.
NB: The pseudocode can be tried out using the Funcy app, which can be downloaded for free from
Apple’s App Store (iOS/macOS),
Google Play (Android) or Amazon Appstore. The code to be executed must be placed in a main {}
block.
Type variance
Consider the following type hierarchy:
type A {} type B : A {} type F[T] {}
Without type arguments, F
is a type operator
while F[A]
and F[B]
are proper types.
In general, neither is a subtype of the other.
However it may sometimes make sense for them to be in a subtype relation
depending on the hierarchy of their argument(s).
If we declare F
as follows
type F[cov T] {}
then F[B]
will be a subtype of F[A]
because B
is a subtype of A
.
Conversely, if we declare F
as
type F[con T] {}
then F[A]
will be a subtype of F[B]
.
In the former case, we say that F
is covariant
in its type argument whereas in the latter case
its contravariant (since the hierarchy is reversed).
A real-world example of type variance are function types.
When we expect a function whose return type is A
(that is, of type Func[A]
), we can always
use a function of type Func[B]
in its stead.
On the other hand, when we expect a function whose
argument is of type B
(that is, of type
Func[B,X]
, we can always use a function of type
Func[A,X]
in its stead.
In sum, function types are covariant in their return type
and contravariant in their arguments’ types.
NB: The pseudocode can be tried out using the Funcy app, which can be downloaded for free from
Apple’s App Store (iOS/macOS),
Google Play (Android) or Amazon Appstore. The code to be executed must be placed in a main {}
block.
Bounded type arguments
Consider the following code:
type Equatable { func equals(x Any) Bool } type Complex : Equatable { property real Float property imag Float func equals(x Any) Bool { if x is Complex then { var x = x as Complex return self.real == x.real & self.imag == x.imag } return false } } type Pair[T] { property first T property second T } main { var x = new Complex { real=2.0, imag=3.0 } var y = new Complex { real=2.0, imag=3.0 } return x.equals(y) }
We now want to implement the equals
method also for Pair
:
func equals(p Pair[T]) Bool { return self.first.equals(p.first) & self.second.equals(p.second) }
Note, however, that this code won’t compile since the compiler can’t be sure
that T
represents an equatable type. We need to change the
declaration of Pair
to
type Pair[T:Equatable] { ... }
Now it’s ensured that T
is equatable and we can check
pairs for equality:
main { var x = new Complex { real=2.0, imag=3.0 } var y = new Complex { real=2.0, imag=4.0 } var p1 = new Pair[Complex] { first=x, second=y } var p2 = new Pair[Complex] { first=x, second=y } return p1.equals(p2) }
Here, the T
type argument is called bounded
since there’s a type constraint placed on it.
NB: The pseudocode can be tried out using the Funcy app, which can be downloaded for free from
Apple’s App Store (iOS/macOS),
Google Play (Android) or Amazon Appstore. The code to be executed must be placed in a main {}
block.
Functors
A functor is a type operator equipped with a map
method
declared as follows:
type Functor[T] { func map[U](f Func[T,U]) Self[U] }
The map
method must satisfy some formal requirements that we ignore here for now. The important point is that map
takes
a function of type Func[T,U]
(where U
is
a type argument of the method) and returns an instance of type
Self[U]
, that is, the same functor possibly with a different
type argument. You’ve already met the Maybe
functor:
type Maybe[T] : Functor[T] { property val T func map[U](f Func[T,U]) Self[U] { return new Self[U] { val = self.val.isNil() ? nil : f(self.val) } } func description() String { return self.val.isNil() ? "none" : self.val.description() } }
In the next section, we’ll use Maybe
to explain
what a monad is and how a generic Monad
type
can be implemented.
NB: The pseudocode can be tried out using the Funcy app, which can be downloaded for free from
Apple’s App Store (iOS/macOS),
Google Play (Android) or Amazon Appstore. The code to be executed must be placed in a main {}
block.