Read-write types
Research: This page describes research about Eiffel, not the actual language specification.
Contents
Introduction
The read-write proposal gives the programmer the possibility to specify per type declaration a read type and a write type. The read type is used to specify applicable features and their return type (reading from the type). The write type is used to do the check on the feature signature for calling features (writing to the type).
This solution is a superset of two other solutions: interval types (without the generic rule) and usage-site variance. Thus it is the more general mechanism than these two as it solves normal and generic types in the same way and can express the same and more than the other two solutions.
It involves syntax overhead for complex problems which is due to the greater expressiveness. But with sensible default values it is as easy to use in the normal case as the other two solutions.
Syntax
In order to express the read type and the write type, a type declaration is split into two parts. For the following discussion two keywords - read and write - are used.
If either the read or the write type is omitted, a default behaviour is assumed which will be shown later on. A type can be declared in the following way:
a: ANY -- Default semantics apply b: read ANY write ANY c: read LIST [read ANY write NONE] write NONE d: read LIST [STRING] write NONE [STRING]
The syntax is very similar to the interval types, just replace write with .. and omit the read keyword. Then the only difference is the generic parameters in the write type which don't appear in the interval definition, especially the generic parameters for class NONE are a special notation.
Semantics
The meaning of a read-write type read A write B
is the following: All features which are available are those of A
and all calls have to be valid with the signature of the type B
. If the write type is NONE
, then the call has to be system-valid for all descendants of the read type. This is essentially the same as interval types.
The difference is, that the write type can be NONE
but still has generic parameters. This is needed to distinguish the covariantly redefined features from the formal features and apply different settings for these feature groups.
If the read type has generic parameters, but the write type does not, then no features with formal arguments may be called. If the read type has generics and the write type has too, than the formal arguments for the signature are taken from the write type. This is the same as for covariantly redefined features where the signature is taken from the write type.
Some examples:
local a: read ANY write NONE -- features from ANY, whole-system analysis (polymorphic) b: read ANY write STRING -- features from ANY, signature from STRING c: read STRING write STRING -- features from STRING, signature from STRING (monomorphic) -- read-only list d: read LIST [read ANY write NONE] write NONE -- list with monomorphic STRING elements e: read LIST [read STRING write STRING] write NONE [read STRING write STRING] do b.is_equal ({STRING}) -- b.to_upper -- feature 'to_upper' is not in ANY c.is_equal ({STRING}) c.to_upper d.item = {read ANY write NONE} -- d.put -- no generics in write type, thus no features with formal arguments e.item = {read STRING write STRING} e.put ({read STRING write STRING}) end
Conformance rules
- A read-write type
a
conforms to a read-write typeb
if the read type ofa
conforms to the read type ofb
and the write type ofb
conforms to the write type ofa
. - The same goes for the generics part, the generics of the read type of
a
have to conform to the generics of the read type ofa
and the generics of the write type ofb
have to conform to the generics of the write type ofa
Some examples:
local a: read ANY write NONE b: read ANY write STRING c: read STRING write STRING d: read LIST [read ANY write NONE] write NONE -- read-only list e: read LIST [read STRING write STRING] write NONE [read STRING write STRING] f: read LINKED_LIST [read ANY write NONE] write LINKED_LIST [read ANY write NONE] do -- valid a := b a := c b := c d := e d := f end
Default behaviour
Although this solution has some syntax overhead, the default behaviour gets rid of most of it.
It is assumed here that NONE
also conforms to expanded types. See the conclusion of interval types for more information.
The default for a normal type X
is a polymorphic type:
a: X -- means 'read X write NONE'
The default for a generic type is a read-write type which is polymorphic in the base class and invariant in the generic parameter:
b: X [Y] -- means 'read X [Y] write NONE [Y]'
And if you write the read type but omit the write type, then the write type is always NONE
. This is useful for generics as you can define a read-only type which is covariantly in the generic parameter:
c: read X -- means 'read X write NONE', (the same as just 'X') d: read X [Y] -- means 'read X [Y] write NONE'
Thanks to these defaults, code which does not rely on generic conformance should work as before:
By choosing these defaults, most code which does not rely on covariant generic conformance should work as before:
local any: ANY -- means 'read ANY write NONE' list: LIST [ANY] -- means 'read LIST [read ANY write NONE] write NONE [read ANY write NONE]' l_list: LINKED_LIST [ANY] -- means 'read LINKED_LIST [read ANY write NONE] write NONE [read ANY write NONE]' do any := "abc" any := 8 list := l_list list.put (any) list.put ("abc") any := list.item.out end
If you want to assign generics with different parameters, you need to mark the target as read-only by specifying the type only as a read type, i.e. read LIST [ANY]
if you want to assign any possible list.
Comparison with other solutions
Interesting about this solution is that it two others can be mapped to it:
Interval types
For non-generic types, the read-write types are just like interval types:
a: ANY..NONE <=> read ANY write NONE b: X..Y <=> read X write Y
The conformance rules and the default behaviour is the same.
Usage-site variance
The usage-site variance can also be expressed by read-write types:
-- invariant a: LIST [ STRING] <=> read LIST [STRING] write NONE [STRING] -- covariant b: LIST [+STRING] <=> read LIST [STRING] write NONE -- contravariant c: LIST [-STRING] <=> read LIST [ANY] write NONE [STRING]
Note that due to the default behaviour of read-write types, the invariant generics is the default in read-write types and, covariant generics are defined as read LIST [STRING]
.
Examples
See the introduction to examples for information about the classes used.
For more examples, look at the examples section of interval types and usage-site variance. With the above mapping, all examples can easily be changed into read-write type examples.
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 read-only list.
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 read-only 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 [PROFESSPR] conforms to read LIST [PERSON] l_person_printer.print_all (l_professor) end end
Issues
Keywords
For this description, the keywords read and write were used. This can easily be changed. For example, to be more like the interval types the read keyword can be omitted and the write keyword replaced by two dots.
Syntax overhead
The read-write types can produce a considerable amount of syntax overhead. Thanks to the default behaviour and the shorthand for read-only types, the average situation requires only one keyword read from time to time. In general no annotation should be necessary at all.
Conclusion
The read-write types are a powerful solution. It can handle non-generic and generic types in the same way but still allows to address features with formal arguments and other covariantly redefined features separately. Since both interval types and usage-site variance can be expressed through it, it has all the advantages of these and with sensible default values does not produce much syntax overhead in the general case.