Forget / keep

Research: This page describes research about Eiffel, not the actual language specification.

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
  -- covariantly 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 loses 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
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. This prevents loading of classes at runtime.

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.