CLEAN allows functions and operators to be overloaded. Type classes and type constructor classes are provided (which look similar to Haskell (Hudak et al. 1992) and Gofer (Jones, 1993), although our classes have slightly different semantics) with which a restricted context can be imposed on a type variable in a type specification.
If one defines a function it should in general have a name that is different from all other function names defined within the same scope and name space (see 2.1). However, it is sometimes very convenient to overload certain functions and operators (e.g. +,-,==), i.e. use identical names for different functions or operators that perform similar tasks albeit on objects of different types.
In principle it is possible to simulate a kind of overloading by using records. One simply defines a record (see 5.2) in which a collection of functions are stored that somehow belong to each other. Now the field name of the record can be used as (overloaded) synonym for any concrete function stored on the corresponding position. The record can be regarded as a kind of dictionary in which the concrete function can be looked up.
A disadvantage of such a dictionary record is that it is syntactically not so nice (e.g. one explicitly has to pass the record to the appropriate function) and that one has to pay a huge price for efficiency (due to the use of higher order functions) as well. CLEAN's overloading system as introduced below enables the CLEAN system to automatically create and add dictionaries as argument to the appropriate function definitions and function applications. To avoid efficiency loss the CLEAN compiler will substitute the intended concrete function for the overloaded function application where possible. In worst case however CLEAN's overloading system will indeed have to generate a dictionary record that is then automatically passed as additional parameter to the appropriate function.
In a type class definition one gives a name to a set of overloaded functions (this is similar to the definition of a type of the dictionary record as explained above). For each overloaded function or operator which is a member of the class the overloaded name and its overloaded type is specified. The type class variables are used to indicate how the different instantiations of the class vary from each other. CLEAN offers multi-parameter type constructor classes, similar to those available in Haskell.
TypeClassDef | = | class ClassName {[.]TypeVariable}+ [ClassContext] | ||
[[where] { {ClassMemberDef}+ }] ; | ||||
| | class FunctionName {[.]TypeVariable}+ :: FunctionType; | |||
| | class (FunctionName) [FixPrec] {[.]TypeVariable}+ :: FunctionType; |
ClassMemberDef | = | FunctionTypeDef | ||
[MacroDef] |
With an instance declaration an instance of a given class can be defined (this is similar to the creation of a dictionary record). When the instance is made one has to be specify for which concrete type an instance is created. For each overloaded function in the class an instance of the overloaded function or operator has to be defined. The type of the instance can be found via uniform substitution of the type class variables by the corresponding type instances specified in the instance definition.
TypeClassInstanceDef | = | instance QClassName Type+ [ClassContext] | ||
[where] { {FunctionDef}+ } ; |
One can define as many instances of a class as one likes. Instances can be added later on in any module that has imported the class one wants to instantiate.
When an instance of a class is defined a concrete definition has to be given for all the class members. |
When an overloaded name is encountered in an expression, the compiler will determine which of the corresponding concrete functions/operators is meant by looking at the concrete type of the expression. This type is used to determine which concrete function to apply.
All instances of a type variable of a certain class have to be of a flat type (see the restrictions mentioned in 6.11).
If it is clear from the type of the expression which one of the concrete instantiations is meant the compiler will in principle substitute the concrete function for the overloaded one, such that no efficiency is lost.
However, it is very well possible that the compiler, given the type of the expression, cannot decide which one of the corresponding concrete functions to apply. The new function then becomes overloaded as well.
This has as consequence that an additional restriction must be imposed on the type of such an expression. A class context has to be added to the function type to express that the function can only be applied provided that the appropriate type classes have been instantiated (in fact one specifies the type of the dictionary record which has to be passed to the function in worst case). Such a context can also be regarded as an additional restriction imposed on a type variable, introducing a kind of bounded polymorphism.
FunctionType | = | [{ArgType}+ ->] Type [ClassContext] [UnqTypeUnEqualities] | ||
ClassContext | = | | ClassConstraints {& ClassConstraints} | ||
ClassConstraints | = | ClassOrGenericName-list {SimpleType}+ | ||
ClassOrGenericName | = | QClassName | ||
| | FunctionName {|TypeKind|} |
CLEAN's type system can infer class contexts automatically. If a type class is specified as a restricted context the type system will check the correctness of the specification (as always a type specification can be more restrictive than is deduced by the compiler).
The concrete functions defined in a class instance definition can also be defined in terms of (other) overloaded functions. This is reflected in the type of the instantiated functions. Both the concrete type and the context restriction have to be specified.
The CLEAN type system offers the possibility to use higher order types (see 3.7.1). This makes it possible to define type constructor classes (similar to constructor classes as introduced in Gofer, Jones (1993)). In that case the overloaded type variable of the type class is not of kind X, but of higher order, e.g. X -> X, X -> X -> X, etcetera. This offers the possibility to define overloaded functions that can be instantiated with type constructors of higher order (as usual, the overloaded type variable and a concrete instantiation of this type variable need to be of the same kind). This makes it possible to overload more complex functions like map and the like.
CLEAN 2.0 offers the possibility to define generic functions. With generic functions one is able to define a function like map once that works for any type (see 7.2).
Identical instances of the same class are not allowed. The compiler would not know which instance to choose. However, it is not required that all instances are of different type. It is allowed to specify an instance of a class of which the types overlap with some other instance given for that class, i.e. the types of the different class instances are different but they can be unified with each other. It is even allowed to specify an instance that works for any type, just by instantiating with a type variable instead of instantiating with a concrete type. This can be handy to define a simple default case (see also the section one generic definitions). If more than one instance is applicable, the compiler will always choose the most specific instantiation.
It is sometimes unclear which of the class instances is the most specific. In that case the lexicographic order is chosen looking at the specified instances (with type variables always > type constructors).
It is possible that a CLEAN expression using overloaded functions is internally ambiguously overloaded. The problem can occur when an overloaded function is used which has on overloaded type in which an overloaded type variable appears on the right-hand side of the ->. If such a function is applied in such a way that the overloaded type does not appear in the resulting type of the application, any of the available instances of the overloaded function can be used.
In that case that an overloaded function is internally ambiguously overloaded the compiler cannot determine which instance to take: a type error is given. |
One can solve such an ambiguity by splitting up the expression in parts that are typed explicitly such that it becomes clear which of the instances should be used.
f:: String -> String
f x = Write (MyRead x)
where
MyRead:: Int -> String
MyRead x = Read x
The members of a class consist of a set of functions or operators that logically belong to each other. It is often the case that the effect of some members (derived members) can be expressed in others. For instance, <> can be regarded as synonym for not (==). For software engineering (the fixed relation is made explicit) and efficiency (one does not need to include such derived members in the dictionary record) it is good to make this relation explicit. In CLEAN the existing macro facilities (see Chapter 10.3) are used for this purpose.
A class definition seems sometimes a bit overdone when a class actually only consists of one member. Special syntax is provided for this case.
TypeClassDef | = | class ClassName {[.]TypeVariable}+ [ClassContext] | ||
[[where] { {ClassMemberDef}+ }] ; | ||||
| | class FunctionName {[.]TypeVariable}+ :: FunctionType; | |||
| | class (FunctionName) [FixPrec] {[.]TypeVariable}+ :: FunctionType; |
The instantiation of such a simple one-member class is done in a similar way as with ordinary classes, using the name of the overloaded function as class name.
In the definition of a class one can optionally specify that other classes that already have been defined elsewhere are included. The classes to include are specified as context after the overloaded type variable. It is not needed (but it is allowed) to define new members in the class body of the new class. In this way one can give a new name to a collection of existing classes creating a hierarchy of classes (cyclic dependencies are forbidden). Since one and the same class can be included in several other classes, one can combine classes in different kinds of meaningful ways. For an example have a closer look at the CLEAN standard library (see e.g. StdOverloaded and StdClass)
To export a class one simply repeats the class definition in the definition module (see Chapter 2). To export an instantiation of a class one simply repeats the instance definition in the definition module, however without revealing the concrete implementation (which can only be specified in the implementation module).
TypeClassInstanceExportDef | = | instance ClassName InstanceExportTypes ; | ||
InstanceExportTypes | = | {Type+ [ClassContext]}-list | ||
| | Type+ [ClassContext] [where] {{FunctionTypeDef}+ } | |||
| | Type+ [ClassContext] [Special] | |||
Special | = | special {{TypeVariable = Type}-list { ; {TypeVariable = Type}-list }} |
For reasons of efficiency the compiler will always make specialized efficient versions of overloaded functions inside an implementation module. For each concrete application of an overloaded function a specialized version is made for the concrete type the overloaded function is applied to. So, when an overloaded function is used in the implementation module in which the overloaded function is defined, no overhead is introduced.
However, when an overloaded function is exported it is unknown with which concrete instances the function will be applied. So, a dictionary record is constructed in which the concrete function can be stored as is explained in the introduction of this section. This approach can be very inefficient, especially in comparison to a specialized version. One can therefore ask the compiler to generate specialized versions of an overloaded function that is being exported. This can be done by using the keyword special. If the exported overloaded function will be used very frequently, we advise to specialize the function for the most important types it will be applied on.
A specialised function can only be generated if a concrete type is definend for all type variables which appear in the instance definition of a class. |
The types of class instance members may be specified. Strictness annotations specified in the type of the instance member of the class definition should be included, because the annotations are copied when the type of the class instance member is instantiated.
Additional strictness annotations are permitted. For example:
class next a where
next :: a -> a
instance next Int where
next :: !Int -> Int
next x = x+1
If such an instance is exported, the type of the instance member must be included in the definition module:
instance next Int where
next :: !Int -> Int
If no additional strictness annotations are specified, it can still be exported without the type.
Semantic restrictions:
When a class is instantiated a concrete definition must be given for each of the members in the class (not for derived members). | |
The type of a concrete function or operator must exactly match the overloaded type after uniform substitution of the overloaded type variable by the concrete type as specified in the corresponding type instance declaration. | |
The overloaded type variable and the concrete type must be of the same kind. | |
A type instance of an overloaded type must be a flat type, i.e. a type of the form T a1 ... an where ai are type variables which are all different. | |
It is not allowed to use a type synonym as instance. | |
The Start function cannot have an overloaded type. | |
For the specification of derived members in a class the same restrictions hold as for defining macros. | |
A restricted context can only be imposed on one of the type variables appearing in the type of the expression. | |
The specification of the concrete functions can only be given in implementation modules. | |
A specialised function can only be generated if a concrete type is defined for all type variables which appear in the instance definition of a class . | |
A request to generate a specialised function for a class instance can only be defined in a definition module. |