Definition-site variance

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

Introduction

The defintion-site variance allows the programmer to choose which kind of variance he wants to use in the defintion of a generic class. Unlike the usage-site variance, this fixes a specific class to the kind of variance which it supports.

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: class LIST [G] end
  • To specify a covariant generic, we use a plus sign: class READ_LIST [+G] end
  • To specify a contravariant generic, we use a minus sign: class WRITE_LIST [-G] end

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:

  • If a generic class is declared novariant, two generics conform if their base class conforms and the generic parameters match exactly. Thus LIST [T] only conforms to LIST [T].
  • If a generic class is declared covariant, a generic conforms to another if its base class conforms and the generic parameter conforms to the other. Thus READ_LIST [U] conforms to READ_LIST [T].
  • If a generic class is declared contravariant, a generic conforms to another if its base class conforms and the generic parameter is an ancestor of the other. Thus WRITE_LIST [U] conforms to WRITE_LIST [T].

Tuples are problematic in this solution as have to declare the generic tuple parameters as invariant. For the conformance of tuples, we keep that a tuple with more elements conforms to a tuple with less elements. The common elements have to match exactly due to their invariance:

Applicable features

Depending on the variance of a generic, it can only occur in certain places:

  • A novariant generic can be used as argument or result type.
  • A covariant generic can only be used as a result type.
  • A contravariant generic can only be used as an argument type.

Examples

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

A simple generic algorithm

In order to use lists as read-only lists, the library design has to be adapted. LIST [G] needs to inherit from READ_LIST [+G]. That way you can assign a LIST [STRING] to a READ_LIST [STRING] and, through transitivity of conformance, to a READ_LIST [ANY].

class PERSON_PRINTER
feature
  print_all (a_list: READ_LIST [PERSON])
      -- Print all attributes of each person in the list.
    do
      from
        a_list.start
      until
        a_list.after
      do 
          -- Reading is valid on a covariant list
        a_list.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 READ_LIST [PERSON]
      l_person_printer.print_all (l_students)
        -- LIST [PROFESSOR] conforms to READ_LIST [PERSON]
      l_person_printer.print_all (l_professor)
    end
end

Comparator

For the comparator example to work, the comparator class has to be declared as contravariant generic: class COMPARATOR [-G].

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

Again, to use definition-site variance you have to adapt the library design. The procedure class has to be defined as follows:

class PROCEDURE [+BASE_TYPE, -OPEN_ARGS -> TUPLE]
end

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 as the generic 
    -- parameter in the tuple is novariant since it is not specified
    -- further in the procedure class.
  an_agent.call ([T])
  an_agent.call ([T, ...])
 
    -- Because the tuple is novariant, the following calls are
    -- illegal although they would be safe.
  an_agent.call ([U])
  an_agent.call ([U, ...])
end

In contrast to usage-site variance you cannot express that the generic parameters of the tuple are covariant. Because of that, the call with a subtype of the actual parameter is not permitted by the type system.

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 not allowed although it would be correct. The reason 
    -- is that TUPLE [ANY] is not an ancestor of TUPEL [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

Again, the fact that the generic tuple parameters are novariant causes that a safe assignment is prohibited.

Covariant tuples

The agent mechanism in definition-site variance is not as expressive as it can be because the generic TUPLE parameters cannot be specified as covariant in the class definition of PROCEDURE. Since TUPLE is a special case, you could think about allowing a special notation to denote totally covariant tuples. If this would be available, the mechanism would allow all possible assignments and calls for the agents.

A possible syntax to designate that the generic tuple arguments will be contravariant would be:

class PROCEDURE [+BASE_TYPE, -OPEN_ARGS -> TUPLE[+]]
end

Conclusion

This solution requires that the libraries are designed in a way which separates reading and writing to generic data structures. It allows full use of comparator objects but does not fully support agents.

On the plus side, the client of a library does not need to put variance markers as in the usage-site variance proposal which makes the use of the solution easier for the normal programmer.