Usage-site variance

Revision as of 14:43, 23 June 2007 by Seilerm (Talk | contribs) (Vision 2 Example)

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
      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 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.

force_extend (action: PROCEDURE [ANY, TUPLE]) is
      -- Extend without type checking.
    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]) is
        -- 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 ancestors 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]
            -- Note `+' in front of `TUPLE' as it is indeed covariant: We accept all kind
            --  of EVENT_DATA as long it is a descendant of `TUPLE', like `TUPLE [ANY, INTEGER]'.
 
inherit
	-- old ARRAYED_LIST [PROCEDURE [ANY, EVENT_DATA]]
        ARRAYED_LIST [PROCEDURE [+ANY, -EVENT_DATA]]
		rename
			make as arrayed_list_make
		export
			{ACTION_SEQUENCE} same_items, subarray
		redefine
			default_create,
			set_count
		end

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]]
   ...
end

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.