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