Forget / keep
Introduction
The forget / keep mechanism enhances the type system. It allows to change types on the feature level by forgetting specific features (especially features which are covariantly redefined) so that these features can't be called anymore. It also allows to specifically keep certain features so that they are surely available which in turn changes the conformance rules to only allow types which have this feature with the exact same signature.
Syntax
-- forget types -- A type which forgets all covariantly redefined features, thus all -- subtypes conform to this type. a: ANIMAL forget all end -- A type which only forgets the features `eat' and `sleep' but not other -- covarianlty redefined features or features whose export status is -- restricted. All subtypes which only redefine or restrict export status -- of `eat' or `sleep' will conform to this type. a: ANIMAL forget eat, sleep end -- keep types -- A type which keeps all features and looses conformance from subtypes which -- covariantly redefine features or restrict export status. b: ANIMAL keep all end -- A type where all subtypes conform except those who covariantly redefine -- feature `eat'. b: ANIMAL keep eat end
Semantics
The meaning of a forget clause is that the actual type which is used does not have the features listed in the forget clause. forget all
is a bit misleading as it does not forget all features, but only features which are covariantly redefined or have the export status changed to be more restrictive later in the inheritance hierarchy. A better naming would be something along the lines of forget covariance
but this does not address the export status change.
The meaning of a keep clause is the reverse. If a feature is listed in a keep clause it can be called on that type with the arguments listed in that type. This means in turn that all subclasses which change the signature of those features covariantly don't conform anymore as a call would not be safe anymore.
Default behavior
The mechanism has two possible default behaviors. If a type is declared without specifying which features to keep or to forget, it can either mean a type which keeps all features or a type which forgets all features. The default behavior has a big impact on the language:
- In the keep all case, all subclasses which have a covariantly redefined feature don't conform to the parent type anymore as it keeps this feature.
- In the forget all case, all subclasses conform to the parent per default, but all features which are covariantly redefined (even much later in the inheritance hierarchy) are not callable since they are forgotten.
Conformance rules
Conformance is best explained on the feature level. A type SUB
conforms to a type PARENT
if all features which are available in PARENT
are also available in SUB
. You have to keep in mind that the type PARENT
can forget certain features. Thus SUB
only conforms to the type PARENT forget x
if SUB also has a forget x
clause. For the keep clause it is a little bit different: A type SUB
only conforms to the type PARENT keep x
if SUB
does not restrict the export status of x
or changes the arguments covariantly, thus it has the same interface regarding feature x
.
Examples
See the introduction to examples for information about the classes used.
Cats and Dogs
The examples are taken from the reference article
forget mechanism
The example regarding covariance on the feature level with the forget mechanism looks as follows:
local a: ANIMAL -- means ANIMAL keep all c: CAT do -- illegal assignment, ANIMAL and CAT don't conform -- since CAT does not have the same signature for `eat' a := c a.eat (food) end
local a: ANIMAL forget all end c: CAT do -- legal, CAT conforms to ANIMAL forget all a := c -- illegal, ANIMAL forget all doesn't have a feature eat a.eat (food) end
keep mechanism
The example regarding covariance on the feature level with the keep mechanism looks as follows:
local a: ANIMAL -- means ANIMAL forget all c: CAT do a := c -- illegal call since the type ANIMAL does not have a feature `eat' anymore a.eat (food) end
local a: ANIMAL keep all end c: CAT do -- illegal, CAT does not conform to ANIMAL keep all a := c -- legal, ANIMAL keep all still has the feature `eat' a.eat (food) end
A Simple Generic Algorithm
deferred class PERSON_PRINTER feature print_attributes (a_list: LIST [PERSON] forget all) -- Print all attributes of each person in the list. deferred end end deferred class EXAMPLE feature example -- Shows example usage of PERSON_PRINTER. local l_students: LINKED_LIST [STUDENT] l_professors: LINKED_LIST [PROFESSOR] l_person_printer: PERSON_PRINTER do create l_person_printer l_person_printer.print_attributes (l_students) l_person_printer.print_attributes (l_professor) end end
By forgetting all covariantly redefined features the generics conform and we can reuse this algorithm for all implementations of LIST
of descendants of PERSON
.
Agents
For agents, we will look at an example with the type T
and a subtype U
. We will see an agent declaration and then check which arguments that are allowed:
Calling agents
local an_agent: PROCEDURE [ANY, TUPLE [T] forget all] -- An agent which takes an argument of type T. do -- The following calls are surely permitted (and also correct) since -- the tuple generic inside the tuple has a 'forget all' clause and -- thus allows the tuple of type U to be passed. an_agent.call ([T]) an_agent.call ([U]) -- Due to the tuple conformance, the following calls are also -- permitted. Note that they are both correct. an_agent.call ([T, ...]) an_agent.call ([U, ...]) end
We see that this solution allows the full range of applicable arguments for calling agents.
Assigning agents
local an_agent: PROCEDURE [ANY, TUPLE [T] forget all] -- An agent which takes an argument of type T. do agent_empty := agent () do end --> PROCEDURE [ANY, TUPLE [] forget all] agent_any := agent (a: ANY) do end --> PROCEDURE [ANY, TUPLE [ANY] forget all] agent_t := agent (t: T) do end --> PROCEDURE [ANY, TUPLE [T] forget all] agent_u := agent (u: U) do end --> PROCEDURE [ANY, TUPLE [U] forget all] agent_tt := agent (t: T; t2: T) do end --> PROCEDURE [ANY, TUPLE [T, T] forget all] -- This assignment is naturally allowed. an_agent := agent_t -- Although it would be safe, the following assignments are not permitted by -- the solution as it does not allow contravariance. an_agent := agent_empty an_agent := agent_any -- The following assignments are not permitted by the solution. This is the correct -- behaviour as you could either create a catcall by passing a T argument to the -- `agent_u' or pass the wrong number of arguments by passing a [T] tuple to the -- `agent_tt'. -- The reason that they are not permitted is that the signature of 'call' changes -- and thus don't conform anymore (with 'keep all' as default) an_agent := agent_u an_agent := agent_tt end
Due to the lack of support for contravariance, not all safe assignments are possible with this solution.
Since declaring an agent can be difficult (there needs to be a "forget all" somewhere?), an alternate syntax for agents could be useful where the compiler can derive the correct type for an agent.
Issues
Whole-system analysis
Consider the following code fragment which shows an object test:
local a: ANIMAL forget all do if {x: ANIMAL forget all} a then x.sleep end end
Now if the system is extended with a reference to the class CAT, the code becomes invalid as now ANIMAL forget all
does not have a feature sleep
anymore.
Object test
Consider the following code fragment which shows an object test:
local a: ANIMAL forget all c: CAT -- cat features 'eat' and 'sleep' do -- Valid a := c if {x: ANIMAL forget eat} a then x.sleep end end
For this to work, the runtime must have information about the covariantly redefined features of each class. This adds a considerable amount of work to the object test as you need to check if all features which are covariantly redefined between the test type (ANIMAL
) and the dynamic type (CAT
) are listed in the forget clause.
Conclusion
This proposal solves catcalls as a whole. This include catcalls which are introduced by covariant argument redefinition through formal arguments in generic classes. It is not as expressive as other solutions (not fully applicable to agents, comparator example) and it is likely that it adds a lot of syntax to the code.