Forget / Keep Mechanism

Construction.png Not Ready for Review: This Page is Under Development!

Introduction

By allowing covariant feature redefinition and hiding of features, the Eiffel language introduces the problem of cat-calls (Changed Availability or Type). The forget / keep mechanism prevents this by introducing new derived types whenever a feature is covariantly redefined or the export status is restricted. These derived types are then used to prevent cat-calls through the conformance rules to the original types.

Example classes

The following classes will be used for illustration:

class ANIMAL
 
feature
 
  eat (f: FOOD)
 
  sleep
 
end
class CAT
 
inherit
 
  ANIMAL
    redefine
      eat
    export
      {NONE} sleep
    end
 
feature
 
  eat (f: CAT_FOOD)
 
end
class FOOD
 
end
 
 
class CAT_FOOD
 
inherit
 
  FOOD
 
end
class LIST [G]
 
feature
 
  has (g: G): BOOLEAN
 
  put (g: G)
 
  item: G
 
end

The problem

The problem of cat-calls has two variants, one because a feature is covariantly redefined:

local
  a: ANIMAL
  c: CAT
  food: FOOD
do
 
  a := c
    -- illegal since `eat' for type CAT takes arguments only of type CAT_FOOD
  a.eat (food)
 
end

And one because the visibility of features change:

local
  a: ANIMAL
  c: CAT
do
 
  a := c
    -- `sleep' is hidden for type CAT and should not be called
  a.sleep
 
end

The idea

To solve the problem of cat-calls, the idea is that whenever you covariantly redefine a feature of a parent type, you don't actually inherit this feature and redefine it, but you inherit from a parent type which does not have this feature at all. The same goes for changing the export status. When you restrict the export status of a feature, you don't inherit this feature from the parent but introduce it in the current type and inherit from a parent which does not know this feature at all.

As a consequence of this, the types as we know them don't necessarily conform to each other. If the type CAT inherits from the type ANIMAL but covariantly redefines the feature eat than the type CAT does not conform to ANIMAL anymore, but only to a type ANIMAL without the feature eat.

This gives two corner cases, the first one is a type which keeps all features even if they get covarianlty redefined by other types. We call this type the keep all type since it keeps all features. All features defined in the class can be called on an instance of this type, but all subtypes which have a covariantly redefined feature or change an export status of a feature don't conform to the keep all type anymore.

The second case is a type which looses all feature which get covariantly redefined later on or whose export status is restricted. This type is called the forget all type since it forgets all problematic features. All subtypes will conform to this type even if they covariantly redefine a feature or change the export status. Since this type forgets all of these features, they cannot be called on such an instance.

Forget mechanism

The forget mechanism introduces a new keyword forget which can be used to remove covarianlty redefined features and features which change the export status. By removing features, a new type is introduced. The new type has all features the original type has except those listed in the forget clause. If forget all is used, all features which are covariantly redefined or have a changed export status will be removed from the new type.

Default behavior

In the forget mechanism the normal declaration of a type will keep all covariantly redefined features or features whose export status is restricted (keep all type). This implies that all subtypes which change an export status or covariantly redefine a feature don't conform to this type anymore.

local
    -- normal declaration means 'keep all'
  animal: ANIMAL
  cat: CAT
do
    -- legal since the feature `eat' is kept in the normal type
  animal.eat (food)
 
    -- legal since the feature `sleep' is kept in the normal type
  animal.sleep
 
    -- illegal since CAT does not conform to ANIMAL (keep all) anymore
  animal := cat
end

Syntax

-- normal declaration is an object with all features (keep all) and no
  -- subtype which has a covariantly redefined feature conforms to this type
a1: ANIMAL
 
  -- a type which forgets all covariantly redefined features, thus all 
  -- subtypes conform to this type. This is also called a 'poly' type
a2: 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 change export status 
  -- of `eat' or `sleep' will conform to this type
a3: ANIMAL forget eat, sleep end

Conformance

The conformance between ANIMAL, ANIMAL forget eat, ANIMAL forget sleep and ANIMAL forget all is as follows:

local
  normal_animal: ANIMAL
  forget_eat_animal:   ANIMAL forget eat end
  forget_sleep_animal: ANIMAL forget sleep end
  forget_all_animal:   ANIMAL forget all end
 
    -- this is equivalent to the forget all clause as long as
    -- no other features of ANIMAL are covarianlty redefined
  forget_eat_sleep_animal: ANIMAL forget eat, sleep end
do
    -- these assignments are legal since all features present in the
    -- forget types are also present in the normal type
  forget_eat_animal   := normal_animal
  forget_sleep_animal := normal_animal
  forget_all_animal   := normal_animal
 
    -- these assignment are illegal since the forget types lack
    -- at least one feature and thus cannot be used as an ANIMAL
  normal_animal := forget_eat_animal
  normal_animal := forget_sleep_animal
  normal_animal := forget_all_animal
 
    -- these assignments are legal since the forget all type
    -- has fewer or equal features than the other forget types
  forget_all_animal := forget_eat_animal
  forget_all_animal := forget_sleep_animal
 
    -- these assignments are illegal since the forget all type
    -- lacks one feature that the other types has (eat or sleep
    -- respectively)
  forget_eat_animal := forget_all_animal
  forget_sleep_animal := forget_all_animal
 
    -- these assignments are illegal since both types lack
    -- a feature which the other type has
  forget_eat_animal := forget_sleep_animal
  forget_sleep_animal := forget_eat_animal
 
end

Keep mechanism

The keep mechanism is the contrary to the forget mechanism. It introduces a new keyword keep which can be used to keep covarianlty redefined features and features which change the export status.

Default behavior

In the keep mechanism the normal declaration of a type will loose all covariantly redefined features or features whose export status is restricted (forget all type). This implies that all subtypes still conform to a normally declared type, but all covariantly redefined features will not be available.

local
    -- normal declaration means 'forget all'
  animal: ANIMAL
  cat: CAT
do
    -- illegal since the feature `eat' is covariantly redefined
  animal.eat (food)
 
    -- illegal since the feature `sleep' changes export status
  animal.sleep
 
    -- legal since animal has no problematic features
  animal := cat
end

Syntax

-- normal declaration is an object which forgets all covariantly
  -- redefined features. all subtypes conform to this type
b1: ANIMAL
 
  -- a type which keeps all features and looses conformance from subtypes which
  -- covariantly redefine features. this is also called 'mono'
b2: ANIMAL keep all end
 
  -- a type where all subtypes conform except those who covariantly redefine
  -- feature `eat'
b3: ANIMAL keep eat end

Conformance

The conformance between ANIMAL, ANIMAL keep eat, ANIMAL keep sleep and ANIMAL keep all is as follows:

local
  normal_animal: ANIMAL
  keep_eat_animal:   ANIMAL keep eat end
  keep_sleep_animal: ANIMAL keep sleep end
  keep_all_animal:   ANIMAL keep all end
 
    -- this is equivalent to the keep all clause as long as
    -- no other features of ANIMAL are covarianlty redefined
  keep_eat_sleep_animal: ANIMAL keep eat, sleep end
do
 
    -- these assignments are illegal since the normal ANIMAL type
    -- does not have the eat or sleep features
  keep_eat_animal   := normal_animal
  keep_sleep_animal := normal_animal
  keep_all_animal   := normal_animal
 
    -- these assignment are legal since the normal type has
    -- fewer features than the keep types
  normal_animal := keep_eat_animal
  normal_animal := keep_sleep_animal
  normal_animal := keep_all_animal
 
    -- these assignments are illegal since the keep all type
    -- has more features than the other keep types
  keep_all_animal := keep_eat_animal
  keep_all_animal := keep_sleep_animal
 
    -- these assignments are legal since the keep all type
    -- has more features than the other keep types
  keep_eat_animal := keep_all_animal
  keep_sleep_animal := keep_all_animal
 
    -- these assignments are illegal since both types lack
    -- a feature which the other type has
  keep_eat_animal := keep_sleep_animal
  keep_sleep_animal := keep_eat_animal
 
end

Covariant feature redefinition

Cat-call problem

With covariant feature redefinition you run into cat-call problems as this example shows:

local
  a: ANIMAL
  c: CAT
  food: FOOD
do
 
  a := c
    -- eat for type CAT takes arguments only of type CAT_FOOD
  a.eat (food)
 
end

With forget mechanism

With the forget mechanism the default behavior for a type is to keep all problematic features. Types which have covariant redefined features will not be conform to the normal type. A feature which is covariantly redefined changes its inherit clause implicitly to inherit from a parent class which has the redefined feature in a forget clause.

what you write what is implied
class CAT
 
inherit
 
  ANIMAL
    redefine
      eat
    end
 
feature
 
  eat (f: CAT_FOOD)
 
end
class CAT
 
inherit
 
  ANIMAL forget eat
    redefine
      eat
    end
 
feature
 
  eat (f: CAT_FOOD)
 
end

Now the cat-call example with the new forget types:

local
  a: ANIMAL
  c: CAT
do
    -- illegal assignment, ANIMAL and CAT don't conform
    -- since CAT implicitly inherits from ANIMAL forget 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

With keep mechanism

With the keep mechanism the default behavior of a type is to loose all problematic features. Thus when a type covariantly redefines a feature, this implicitly changes the parent to loose this feature by default.

what you write what is implied
class CAT
 
inherit
 
  ANIMAL
    redefine
      eat
    end
 
feature
 
  eat (f: CAT_FOOD)
 
end

feature eat will not be present anymore in type ANIMAL. Only ANIMAL keep eat end or ANIMAL keep all end will have the feature eat.

Now the cat-call example with the new types:

local
  a: ANIMAL
  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

Feature hiding

Cat-call problem

By restricting the export status of features, a cat-call problem is introduced:

local
  a: ANIMAL
  c: CAT
do
 
  a := c
    -- sleep is hidden for type CAT and should not be called
  a.sleep
 
end

With forget mechanism

With the forget mechanism the default behavior for a type is to keep all problematic features. Types which change the export status of features will not be conform to the normal type. A feature which has a restricted export status changes its inherit clause implicitly to inherit from a parent class which has the feature in a forget clause.

what you write what is implied
class CAT
 
inherit
 
  ANIMAL
    export
      {NONE} sleep
    end
 
end
class CAT
 
inherit
 
  ANIMAL forget sleep
    export
      {NONE} sleep
    end
 
end

Now the cat-call example:

local
  a: ANIMAL
  c: CAT
do
    -- illegal assignment, ANIMAL and CAT don't conform
    -- since CAT implicitly inherits from ANIMAL forget sleep
  a := c
  a.sleep
 
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 sleep
  a.sleep
 
end

With keep mechanism

With the keep mechanism the default behavior of a type is to loose all problematic features. Thus when a type changes the export status of a feature, this implicitly changes the parent to loose this feature by default.

what you write what is implied
class CAT
 
inherit
 
  ANIMAL
    export
      {NONE} sleep
    end
 
end

feature sleep will not be present anymore in type ANIMAL. Only ANIMAL keep sleep end or ANIMAL keep all end will have the feature sleep.

Now the cat-call example:

local
  a: ANIMAL
  c: CAT
do
  a := c
    -- illegal call since the type ANIMAL does not have a feature `sleep' anymore
  a.sleep
 
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 `sleep'
  a.sleep
 
end

Generics

Conformance of generics - two generic conform if their generic parameters conform - introduce another kind of covariantly redefined features. Every feature which has a generic argument, e.g. put (g: G) in class LIST [G] can be regarded as covariantly redefined since put in LIST [ANY] takes an argument of type ANY and put in LIST [ANIMAL] takes an argument of type ANIMAL. But LIST [ANIMAL] conforms to LIST [ANY], thus the feature put is actually covariantly redefined in LIST [ANIMAL].

Cat-call problem

The conformance rules of generics alos introduce cat-call problems:

local
    -- feature `put' has argument type ANY
  any_list: LIST [ANY]
    -- feature `put' has argument type ANIMAL
  animal_list: LIST [ANIMAL]
do
    -- animal_list conforms to any_list, but arguments of the feature
    -- `put' are different
  any_list := animal_list
 
    -- since any type can be put into any_list this will add
    -- an integer to the animal_list
  any_list.put (5)
 
end

With forget mechanism

Default is keep all types and features with generic arguments are treated as covariantly redefined.

local
  any_list: LIST [ANY]
  animal_list: LIST [ANIMAL]
do
    -- illegal since animal_list does not conform to any_list anymore
  any_list := animal_list
 
  any_list.put (5)
 
end
local
  any_list: LIST [ANY] forget all end
  animal_list: LIST [ANIMAL]
  animal: ANIMAL
do
    -- legal since animal_list conforms to LIST [ANY] forget all
  any_list := animal_list
 
    -- illegal since the forget all list does not have any features
    -- with generic arguments anymore
  any_list.put (5)
 
    -- illegal, same reason as above
  any_list.has (animal)
 
end

With keep mechanism

Default is forget all types and features with generic arguments are treated as covariantly redefined.

local
  any_list: LIST [ANY]
  animal_list: LIST [ANIMAL]
  animal: ANIMAL
do
    -- legal since any_list is 'forget all' and has no problematic features
  any_list := animal_list
 
    -- illegal since the feature 'put' is treated as covariantly redefined
    -- and thus not present in the 'forget all' type
  any_list.put (5)
 
    -- illegal, same reason as above
  any_list.has (animal)
 
end
local
  any_list: LIST [ANY] keep all end
  animal_list: LIST [ANIMAL]
do
    -- illegal since animal_list does not conform to 
    -- any_list keep all anymore
  any_list := animal_list
 
  any_list.put (5)
 
  any_list.has (animal)
 
end

Conformance

class A end
 
class B 
inherit A redefine x (covariantly) end
end
 
class C
inherit A
end
a: LIST [A]
b: LIST [A forget all end]
c: LIST [A] forget all end
d: LIST [A forget all end] forget all end
 
e: LIST [B]
f: LIST [B forget all end]
g: LIST [B] forget all end
h: LIST [B forget all end] forget all end
 
i: LIST [C]
k: LIST [C forget all end]
l: LIST [C] forget all end
m: LIST [C forget all end] forget all end
do
    -- illegal
  a := b
  b := a
 
  ...

The Java solution

With Java 1.5, generics were introduced in the language. A mechanism to prevent cat-calls with generics was introduced as well:

Official Sun tutorial (PDF)

Another tutorial

An example

Consequences

Arguments with like

A declaration like Current is a covariant redefinition for all subtypes. Thus the mechanism affects all types which have such arguments and their subtypes (see also next section):

With the forget mechanism the normal type keeps all features (keep all type) but subtypes with covariant feature redefinitions will loose conformance to this type. Thus with a like Current argument in a feature f, no subtype will conform to it's parent anymore unless the parent is declared forget f.

With the keep mechanism the normal type forgets all problematic features (forget all type) and subtypes will still conform to this type. But this means that a feature f with a like Current argument will always be forgotten in a normal type and can only be used in keep f types.

ANY

ANY has the features ANY.is_equal and ANY.copy with a like Current argument:

class ANY
 
feature
 
  is_equal (other: like Current): BOOLEAN
 
  copy (other: like Current)
 
end

Since all types are descendants of ANY, all types have covariantly redefined features. Thus the both mechanisms - forget and keep - have an impact on the whole type hierarchy:

As discussed in the previous section, the forget mechanism defaults to keep all types but the subtypes don't conform to their normal parents anymore. Since ANY introduces these features, no type will conform to any parent unless the parent is declared forget is_equal, copy.

With the keep mechanism and forget all types as default, all types still conform to their parents, but the features is_equal and copy cannot be used unless a type is declared as keep is_equal, copy. This declaration allows use of the features, but you will loose conformance of all subtypes.

Solution: Remove the like Current arguments from ANY. Change the argument of is_equal and copy to type ANY. Then either introduce a precondition to require conformance of argument and object or add a type checks to the actual implementation of the features. This is actually done in the ECMA standard but not yet implemented in EiffelBase.

COMPARABLE

In the class COMPARABLE all features are defined with a like Current argument.

With the forget mechanism this means that when a class inherits from COMPARABLE, no subtype will conform to this class anymore, unless all the compare features are forgotten.

Solution: Change the class to take a generic arugment and be able to compare to this argument (COMPARABLE [G])

export {NONE} all

With a declaration

class A
 
inherit
 
  B
    export
      {NONE} all
    end
 
end

the type A restricts the visibility of all features of ANY through restricting the features of B.

With the forget mechanism the type A will not conform to neither B nor ANY. Moreover the types B forget all and ANY forget all will not have any features since all features are affected by a change in export status.

With the keep mechanism and the default type being forget all type, any declaration of ANY somewhere will not have any features since all features are affected by an export status restriction in the declaration of A.

Solution: Introduce the inherit {NONE} construct since most declarations of the type export {NONE} all are actually meant as non-conforming inheritance in the first place. Since no conformance between parents from a inherit {NONE} clause exists anyway, the forget / keep mechanism is not applied to these cases.

agents

...

Algorithms

...

Generics

...

SET.has

...

Various problems

Let's say the class ANIMAL has some other features as well and a class SNAKE is introduced:

class ANIMAL
 
feature
 
  eat (f: FOOD)
 
  a: ANIMAL
 
  feed_a
    local
      f: FOOD
    do
      a.eat (f)
    end
 
  x
    local
      f: FOOD
    do
      eat (f)
    end
 
  y
    local
      a: ANIMAL
      f: FOOD
    do
      a.eat (f)
    end
 
  z
    local
      a: ANIMAL
    do
      a := Current
    end
 
end
 
class SNAKE
 
inherit
 
  ANIMAL
    redefine
      a
    end
 
feature
 
  a: CAT
 
end

If now CAT covariantly redefines eat the features x, y and z in class CAT are problematic. Depending on the default type (keep all or forget all) the features are not valid anymore and have to be forgotten in class CAT as well.

In class SNAKE the feature feed_a of class ANIMAL will become invalid by the redefinition of the feature a.

Information hiding

Consider the following implementation:

class ANIMAL
 
feature
 
  eat (f: FOOD)
 
  gain_weight
    do
      weight := weight + 3
    end
 
end

And the use of class ANIMAL:

local
  a: ANIMAL forget eat
do
    -- all ok
  a.gain_weight
 
end

If now the implementation changes that another feature uses the eat in it's implementation, the type ANIMAL forget eat will loose more features depending on the implementation.

New implementation:

gain_weight
    do
      eat (food)
    end
local
  a: ANIMAL forget eat
do
    -- this will not work anymore
  a.gain_weight
 
end

Uniqueness of forget types

In general, the notion of TYPE forget f may not be unique. If a feature f uses a feature g and vice-versa, the types TYPE forget f and TYPE forget g are equivalent since both have to forget f and g.

Solution for uniqueness: All (public) features which are forgotten have to be named explicitly. Thus if you write TYPE forget f this is not valid until you list all features which are affected. This still leaves the problem of information hiding. You need to know which features use another feature in order to list it in a specific feature in a forget clause. And if later the implementation changes, you have to adapt your forget clauses.

Conclusion