Usage-site variance
Contents
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 toLIST [T]
. Note thatLINKED_LIST [T]
also conforms toLIST [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 toLIST [+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 toLIST [-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:
-
TUPLE [T, U]
conforms toTUPLE [T]
-
TUPLE [U, U]
conforms toTUPLE [+T]
-
TUPLE [T, U]
conforms toTUPLE [-U]
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.