Usage-site variance

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

Introduction

The usage-site variance allows the programmer to choose which kind of variance he wants to use when a generic derivation is declared. This allows to use every generic as novariant, covariant or contravariant in contrast to the definition-site variance where it is set in the class definition.

Syntax

The syntax used here to specify the variance of a generic is simple and may be changed to something more verbose but also clearer:

  • To specify a novariant generic, the normal syntax can be used: LIST [ANY]
  • To specify a covariant generic, we use a plus sign: LIST [+ANY]
  • To specify a contravariant generic, we use a minus sign: LIST [-ANY]

Semantics

See the introduction to examples for information about the classes used.

Conformance rules

Depending on the variance of the generic, the conformance rules differ:

  • A generic conforms to a novariant generic if it has the exact same generic parameter. Thus LIST [T] only conforms to LIST [T]. Note that LINKED_LIST [T] also conforms to LIST [T] as long as the generic parameter matches.
  • A generic conforms to a covariant generic if its generic parameter conforms to the generic parameter of the covariant generic. Thus LIST [U] conforms to LIST [+T].
  • A generic conforms to a contravariant generic if its generic parameter is a parent of the generic parameter of the contravariant generic. Thus LIST [T] conforms to LIST [-U].

For the conformance of tuples, we keep that a tuple with more elements conforms to a tuple with less elements as long as the common elements conform:

Applicable features

Depending on the variance of the generic, the applicable features differ:

  • On a novariant generic, all features can be used.
  • On a covariant generic, only features which have the formal generic as a result type can be used. If the formal generic appears in an argument this feature is invalid.
  • On a contravariant generic, only feature which have the formal generc as an argument type can be used. If the formal generic appears as a result type this feature is invalid.

Examples

See the introduction to examples for information about the classes used.

A simple generic algorithm

It is easy to declare an algorithm over generics as long as only read-access is needed. Just declare input to the algorithm as covariant list.

class PERSON_PRINTER
feature
  print_all (a_list: LIST [+PERSON])
      -- Print all attributes of each person in the list.
    do
      across a_list as it
      loop
          -- Reading is valid on a covariant list
          it.item.print
      end
    end
end
 
class EXAMPLE
feature
  make
    local
      l_students: LIST [STUDENT]
      l_professors: LIST [PROFESSOR]
      l_person_printer: PERSON_PRINTER
    do
      create l_person_printer
        -- LIST [STUDENT] conforms to LIST [+PERSON]
      l_person_printer.print_all (l_students)
        -- LIST [PROFESSPR] conforms to LIST [+PERSON]
      l_person_printer.print_all (l_professor)
    end
end

Comparator

In the example of sorting a list with a comparator, contravariant generics can be used. Due to the use of COMPARATOR [-G] when sorting a LIST [G], we allow to use a comparator which can sort more generally than just objects of type G. This allows to use a comparator for ANY objects to sort a list of strings.

class SORTER [G]
feature
  sort (a_list: LIST [G]; a_comparator: COMPARATOR [-G])
    do
        -- Somewhere in the loop:
      a_comparator.compare (l_string_1, l_string_2)
    end
end
 
class EXAMPLE
feature
  make
    local
      l_list: LIST [STRING]
      l_string_sorter: SORTER [STRING]
      l_string_comparator: COMPARATOR [STRING]
      l_any_comparator: COMPARATOR [ANY]
    do
        -- COMPARATOR [STRING] conforms to COMPARATOR [-STRING]
      l_string_sorter.sort (l_list, l_string_comparator)
        -- COMPARATOR [ANY] conforms to COMPARATOR [-STRING]
      l_string_sorter.sort (l_list, l_any_comparator)
    end
end

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]]
    -- 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 is covariant 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]]
    -- An agent which takes an argument of type T.
do
  agent_empty := agent () do end --> PROCEDURE [ANY, TUPLE []]
  agent_any := agent (a: ANY) do end --> PROCEDURE [ANY, TUPLE [+ANY]]
  agent_t := agent (t: T) do end --> PROCEDURE [ANY, TUPLE [+T]]
  agent_u := agent (u: U) do end --> PROCEDURE [ANY, TUPLE [+U]]
  agent_tt := agent (t: T; t2: T) do end --> PROCEDURE [ANY, TUPLE [+T, +T]]
 
    -- This assignment is naturally allowed.
  an_agent := agent_t
 
    -- This assignment is allowed since the tuple is declared contravariant
    -- and the empty tuple is an ancestor of TUPLE [+T]
  an_agent := agent_empty
 
    -- This assignment is allowed and correct. The reason is that TUPLE [+ANY]
    -- is an ancestor of TUPLE [+T].
  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'.
  an_agent := agent_u
  an_agent := agent_tt
end

We see that this solution allows all possible agents to be assigned.

Since declaring an agent can be difficult (where did I need to put the "+" and the "-"?), an alternate syntax for agents could be useful where the compiler can derive the correct type for an agent.

Vision 2 Example

A field which makes heavy use of agents and benefits a lot of them is event driven programming. With the current agent mechanism the programmer is sometimes forced to use a hack to circumvent the type checker in order to gain flexibility.

The following code is taken from EV_PND_MOTION_ACTION_SEQUENCE. Here we gain the flexibility that an agent which listens to the event does not have to be a feature which takes all the data provided if the event occurs. Maybe the programmer is just interested in the fact that an actual event occurred and not in the data about it.

force_extend (action: PROCEDURE [ANY, TUPLE]) is
      -- Extend without type checking. Not type save! Use on your own risk!
    do
      extend (agent wrapper (?, ?, ?, action))
    end
 
  wrapper (an_x, a_y: INTEGER; a_pick_and_dropable: EV_ABSTRACT_PICK_AND_DROPABLE;
        action: PROCEDURE [ANY, TUPLE])
        -- Use this to circumvent tuple type checking. (at your own risk!)
        -- Calls `action' passing all other arguments.
    do
      action.call ([an_x, a_y, a_pick_and_dropable])
    end

If this solution is built into Eiffel the designer of Vision2 would do a small change in ACTION_SEQUENCE. The changes makes sure that we can accept agents to features defined in descendants of ANY (therefore the +ANY) and that we accept agents which are not interested in all data but just (possibly) take parts of it (therefore -EVENT_DATA).

class
  ACTION_SEQUENCE [EVENT_DATA -> TUPLE create default_create end]
inherit
  ARRAYED_LIST [PROCEDURE [+ANY, -EVENT_DATA]]
  -- old ARRAYED_LIST [PROCEDURE [ANY, EVENT_DATA]]

The wrapper code can be removed from EV_PND_MOTION_ACTION_SEQUENCE and we also change one line. This change empowers the system to accept descendants of EV_ABSTRACT_PICK_AND_DROPABLE as event data. The two integers do not need be marked covariantly as they are frozen and no descendants exist for them.

class
  EV_PND_MOTION_ACTION_SEQUENCE
 
inherit
    -- old EV_ACTION_SEQUENCE [TUPLE [x: INTEGER; y: INTEGER; pick_and_dropable: EV_ABSTRACT_PICK_AND_DROPABLE]]
  EV_ACTION_SEQUENCE [TUPLE [x: INTEGER; y: INTEGER; pick_and_dropable: +EV_ABSTRACT_PICK_AND_DROPABLE]]

Now with this the actual signature of {EV_PND_MOTION_ACTION_SEQUENCE}.extend looks like this:

extend (v: PROCEDURE [ANY, -TUPLE [x: INTEGER_32; y: INTEGER_32; pick_and_dropable: +EV_ABSTRACT_PICK_AND_DROPABLE]]

For the sake of completeness the signature of {EV_PND_MOTION_ACTION_SEQUENCE}.call. Note again that the the field `pick_and_dropable' is covariant allowing to call the event with tuples containing descendants of EV_ABSTRACT_PICK_AND_DROPABLE.

call (event_data: TUPLE [x: INTEGER_32; y: INTEGER_32; pick_and_dropable: +EV_ABSTRACT_PICK_AND_DROPABLE])

Conclusion

The usage-site variance is a mechanism which allows both expressive and type-safe generics. It can model agents and comparator types correctly.