Read-write types

Revision as of 17:12, 6 July 2007 by Peter gummer (Talk | contribs) (Deleted a sentence that was repeated with slightly different wording. Hope I picked the right one!)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

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, then 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 type b if the read type of a conforms to the read type of b and the write type of b conforms to the write type of a.
  • 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 of a and the generics of the write type of b have to conform to the generics of the write type of a.

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'

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.