Difference between revisions of "Objectless Calls"
m |
m |
||
Line 5: | Line 5: | ||
== The Creation Problem == | == The Creation Problem == | ||
+ | To date the Eiffel language suffers from a problem whereby creation contracts cannot be specified correctly or can be specified but not guarenteed. Take the following code with a creation routine needing to validate a creation argument using a class query: | ||
+ | |||
+ | <e>class A | ||
+ | |||
+ | create | ||
+ | make | ||
+ | |||
+ | feature {NONE} -- Initialization | ||
+ | |||
+ | make (a_arg: ?VALUE) | ||
+ | require | ||
+ | a_arg_is_valid_value: is_valid_value (a_arg) | ||
+ | do | ||
+ | ... | ||
+ | end | ||
+ | |||
+ | feature -- Query | ||
+ | |||
+ | is_valid_value (a_value: ?VALUE): BOOLEAN | ||
+ | -- Determines if an argument is valid | ||
+ | do | ||
+ | Result := a_value /= Void and then a_value.has_value | ||
+ | end | ||
+ | |||
+ | end</e> | ||
+ | |||
+ | Here there is the classic chicken and egg problem; A client needs to validate any prospective creation argument to create an instance of <e>A</e> but requires an instance of <e>A</e> in order to validate it. | ||
+ | |||
+ | There is a solution, but it is far from elegant, fragments class design and opens avenues to bugs. The solution is to refactor and pull up <e>is_valid_value</e> into a validation "helper" class to assist <e>A</e>: | ||
+ | |||
+ | <e>class A_VALIDATION | ||
+ | |||
+ | feature -- Query | ||
+ | |||
+ | is_valid_value (a_value: ?VALUE): BOOLEAN | ||
+ | -- Determines if an argument is valid | ||
+ | do | ||
+ | Result := a_value /= Void and then a_value.has_value | ||
+ | end | ||
+ | |||
+ | end</e> | ||
+ | |||
+ | Now there has been a validator class defined, with implementation that actually belongs in <e>A</e>, <e>A</e> can now be defined as: | ||
+ | |||
+ | <e>class A | ||
+ | |||
+ | inherit {NONE} | ||
+ | A_VALIDATION | ||
+ | |||
+ | create | ||
+ | make | ||
+ | |||
+ | feature {NONE} -- Initialization | ||
+ | |||
+ | make (a_arg: ?VALUE) | ||
+ | require | ||
+ | a_arg_is_valid_value: is_valid_value (a_arg) | ||
+ | do | ||
+ | ... | ||
+ | end | ||
+ | |||
+ | end</e> | ||
+ | |||
+ | A client not has to create an instance of <e>A_VALIDATOR</e> in order to create an instance of <e>A</e>. The result is a performance hit because of an additional object creation and heap allocation. | ||
+ | |||
+ | <e>process (a_value: ?VALUE) | ||
+ | local | ||
+ | l_a: A | ||
+ | do | ||
+ | if (create {A_VALIDATOR}).is_valid_value (a_value) then | ||
+ | create l_a.make (a_value) | ||
+ | ... | ||
+ | end | ||
+ | end</e> | ||
+ | |||
+ | Apart from the fragmentation and additional creation, it seems like the problem is solved. However, take the following adjustments likely to transpire when an developer other than the orginal (maybe event the orginal developer) subclasses <e>A</e> to create a new type <e>B</e>: | ||
+ | |||
+ | <e>class B | ||
+ | |||
+ | inherit | ||
+ | A redefine make, is_valid_value end | ||
+ | |||
+ | create | ||
+ | make | ||
+ | |||
+ | feature {NONE} -- Initialization | ||
+ | |||
+ | make (a_arg: ?VALUE) | ||
+ | do | ||
+ | ... | ||
+ | end | ||
+ | |||
+ | feature -- Query | ||
+ | |||
+ | is_valid_value (a_value: ?VALUE): BOOLEAN | ||
+ | -- Determines if an argument is valid | ||
+ | do | ||
+ | Result := Precursor {A} (a_value) and then a_value.value.is_equal ("predefined") | ||
+ | end | ||
+ | |||
+ | end</e> | ||
+ | |||
+ | The affect isn't that obvious. This is a common scenario and is actually a bug because the implementation does not follow the existing model of <e>A</e>, that is to say fragment and implement an additional class <e>B_VALIDATOR</e>. The real implementation should have been: | ||
+ | |||
+ | <e>class B_VALIDATION | ||
+ | |||
+ | inherit | ||
+ | A_VALIDATOR redefine is_valid_value end | ||
+ | |||
+ | feature -- Query | ||
+ | |||
+ | is_valid_value (a_value: ?VALUE): BOOLEAN | ||
+ | -- Determines if an argument is valid | ||
+ | do | ||
+ | Result := Precursor {A} (a_value) and then a_value.value.is_equal ("predefined") | ||
+ | end | ||
+ | |||
+ | end</e> | ||
+ | |||
+ | Now there has been a validator class defined, with implementation that actually belongs in <e>B</e>, <e>B</e> can now be defined as: | ||
+ | |||
+ | <e>class B | ||
+ | |||
+ | inherit | ||
+ | A redefine make, is_valid_value end | ||
+ | |||
+ | inherit {NONE} | ||
+ | B_VALIDATION | ||
+ | |||
+ | create | ||
+ | make | ||
+ | |||
+ | feature {NONE} -- Initialization | ||
+ | |||
+ | make (a_arg: ?VALUE) | ||
+ | require | ||
+ | a_arg_is_valid_value: is_valid_value (a_arg) | ||
+ | do | ||
+ | ... | ||
+ | end | ||
+ | |||
+ | end</e> | ||
+ | |||
+ | Only now can a client safely create an instance of <e>B</e>, but this time using <e>B_VALIDATOR</e>. So for a simple two class model there now exists four classes and an additional performace hit for the creation (or instance access of using a once function) for the validator instance. | ||
== Overuse of Inheritance == | == Overuse of Inheritance == | ||
Line 23: | Line 167: | ||
feature -- Access | feature -- Access | ||
− | + | objectless f: STRING_8 | |
− | + | -- Proposal for an objectless function. | |
− | + | -- This seems much more readable | |
− | + | do | |
− | + | end | |
− | + | ||
− | + | f: STRING_8 | |
− | + | -- Proposal for an objectless function, alternative style. | |
− | + | -- Less readable as an objectless call | |
− | + | objectless do | |
− | + | end | |
end</e> | end</e> | ||
Line 45: | Line 189: | ||
create | create | ||
− | + | make | |
− | + | ||
feature {NONE} -- Initialization | feature {NONE} -- Initialization | ||
− | + | make (a_arg: ?VALUE) | |
− | + | require | |
− | + | a_arg_is_valid_value: is_valid_value (a_arg) | |
− | + | do | |
− | + | ... | |
− | + | end | |
− | + | ||
feature -- Query | feature -- Query | ||
− | + | objectless is_valid_value (a_value: ?VALUE): BOOLEAN | |
− | + | -- Determines if an argument is valid | |
− | + | do | |
− | + | Result := a_value /= Void and then a_value.has_value | |
− | + | ensure | |
− | + | a_value_attached: Result implies a_value /= Void | |
− | + | a_value_has_value: Result implies a_value.has_value | |
− | + | end | |
− | + | ||
end</e> | end</e> | ||
Line 72: | Line 216: | ||
<e>process (a_value: ?VALUE) | <e>process (a_value: ?VALUE) | ||
− | + | -- Processes a value node. | |
− | + | local | |
− | + | l_a: A | |
− | + | do | |
− | + | if {A}.is_valid_value (a_value) then | |
− | + | -- The value is valid, so we can safely create an instance of {A}. | |
− | + | create l_a.make (a_value) | |
− | + | ... | |
− | + | end | |
− | + | end</e> | |
− | + | ||
=== Extensions === | === Extensions === | ||
One extension to the proposal and possible advantage over languages with a similar mechanism is the possibility to redefine a objectless routine. The semantics of this have clear rules that govern the behavoir of calls and the calling convention used. This extension requires consideration now because of impacts on user code in the future if the language is to adopt the extension. | One extension to the proposal and possible advantage over languages with a similar mechanism is the possibility to redefine a objectless routine. The semantics of this have clear rules that govern the behavoir of calls and the calling convention used. This extension requires consideration now because of impacts on user code in the future if the language is to adopt the extension. | ||
Line 89: | Line 233: | ||
<e>class A | <e>class A | ||
− | + | ||
feature -- Access | feature -- Access | ||
− | + | objectless f: STRING do ... end | |
− | + | ||
end</e> | end</e> | ||
Line 99: | Line 243: | ||
inherit | inherit | ||
− | + | A | |
− | + | ||
end</e> | end</e> | ||
Line 111: | Line 255: | ||
inherit | inherit | ||
− | + | A redefine f end | |
− | + | ||
feature -- Access | feature -- Access | ||
− | + | objectless f: STRING do ... end | |
− | + | ||
end</e> | end</e> | ||
Line 127: | Line 271: | ||
inherit | inherit | ||
− | + | A | |
− | + | redefine | |
− | + | f | |
− | + | end | |
− | + | ||
feature -- Access | feature -- Access | ||
− | + | f: STRING | |
− | + | do | |
− | + | Result := Precursor {A}.twin | |
− | + | Result.as_lower | |
− | + | end | |
− | + | ||
end</e> | end</e> | ||
Revision as of 08:55, 14 March 2008
In the proposal the author highlight some of the advantages of adding a new mechanism to the Eiffel language to permit objectless (known as static in C/C++ and derivative languages) calls. The introduction of objectless calls in Eiffel resolves issues regarding contract guarantees, creation contracts, overuse of "shared" inheritance and can solve a performance related issue using dynamic dispatch.
It is in the author's opinion that objectless calls are a necessary addition for the continued improvement of the Eiffel language, and were overlooked when defining the ECMA standard.
Contents
The Creation Problem
To date the Eiffel language suffers from a problem whereby creation contracts cannot be specified correctly or can be specified but not guarenteed. Take the following code with a creation routine needing to validate a creation argument using a class query:
class A create make feature {NONE} -- Initialization make (a_arg: ?VALUE) require a_arg_is_valid_value: is_valid_value (a_arg) do ... end feature -- Query is_valid_value (a_value: ?VALUE): BOOLEAN -- Determines if an argument is valid do Result := a_value /= Void and then a_value.has_value end end
Here there is the classic chicken and egg problem; A client needs to validate any prospective creation argument to create an instance of A
but requires an instance of A
in order to validate it.
There is a solution, but it is far from elegant, fragments class design and opens avenues to bugs. The solution is to refactor and pull up is_valid_value
into a validation "helper" class to assist A
:
class A_VALIDATION feature -- Query is_valid_value (a_value: ?VALUE): BOOLEAN -- Determines if an argument is valid do Result := a_value /= Void and then a_value.has_value end end
Now there has been a validator class defined, with implementation that actually belongs in A
, A
can now be defined as:
class A inherit {NONE} A_VALIDATION create make feature {NONE} -- Initialization make (a_arg: ?VALUE) require a_arg_is_valid_value: is_valid_value (a_arg) do ... end end
A client not has to create an instance of A_VALIDATOR
in order to create an instance of A
. The result is a performance hit because of an additional object creation and heap allocation.
process (a_value: ?VALUE) local l_a: A do if (create {A_VALIDATOR}).is_valid_value (a_value) then create l_a.make (a_value) ... end end
Apart from the fragmentation and additional creation, it seems like the problem is solved. However, take the following adjustments likely to transpire when an developer other than the orginal (maybe event the orginal developer) subclasses A
to create a new type B
:
class B inherit A redefine make, is_valid_value end create make feature {NONE} -- Initialization make (a_arg: ?VALUE) do ... end feature -- Query is_valid_value (a_value: ?VALUE): BOOLEAN -- Determines if an argument is valid do Result := Precursor {A} (a_value) and then a_value.value.is_equal ("predefined") end end
The affect isn't that obvious. This is a common scenario and is actually a bug because the implementation does not follow the existing model of A
, that is to say fragment and implement an additional class B_VALIDATOR
. The real implementation should have been:
class B_VALIDATION inherit A_VALIDATOR redefine is_valid_value end feature -- Query is_valid_value (a_value: ?VALUE): BOOLEAN -- Determines if an argument is valid do Result := Precursor {A} (a_value) and then a_value.value.is_equal ("predefined") end end
Now there has been a validator class defined, with implementation that actually belongs in B
, B
can now be defined as:
class B inherit A redefine make, is_valid_value end inherit {NONE} B_VALIDATION create make feature {NONE} -- Initialization make (a_arg: ?VALUE) require a_arg_is_valid_value: is_valid_value (a_arg) do ... end end
Only now can a client safely create an instance of B
, but this time using B_VALIDATOR
. So for a simple two class model there now exists four classes and an additional performace hit for the creation (or instance access of using a once function) for the validator instance.
Overuse of Inheritance
The Performance Problem
A Proposal
In order to complete this document the author indicates a proposal for such a mechanism for permitting objectless calls.
Due to objectless calls being a language mechanism rather than a run-time attribute (such as a once's process/thread status) a new keyword is going to be required, minimizing possible conflicts with feature names use in existing systems. For this proposal the author suggest objectless, indicating a objectless call.
Objectless class should be permitted on function, both regular and once functions. It is up to debate to indicate if procedures and class attributes are permitted to be objectless
The objectless keyword should appear as a routine modifier, as with the frozen
keyword. Just as the use of frozen
is used to dictate to subclasses a routine cannot be redefined/undefined, objectless has a similar standing when being called, indicating (not dictating) to clients to use the static calling convention. Secondary to the argument such placement is clear to a reader the call is objectless. The only alternative foreseeable placement for an objectless keyword would be a prior to a routine's type keyword do, once, deferred, external
, however in the case of the previous arguments of readability, it's the author's opinion the keyword should not appear there. There may be a suggestion to using a routine's note
clause to indicate objectless status but this is a bad idea, not only for readability but because note clauses are informative structures that should not change routine semantics.
class A feature -- Access objectless f: STRING_8 -- Proposal for an objectless function. -- This seems much more readable do end f: STRING_8 -- Proposal for an objectless function, alternative style. -- Less readable as an objectless call objectless do end end
Using objectless is an indication to client, use the static calling convention is permitted but is not necessary. Making the same call on an attached entity will make
Solving the Creation Problem
class A create make feature {NONE} -- Initialization make (a_arg: ?VALUE) require a_arg_is_valid_value: is_valid_value (a_arg) do ... end feature -- Query objectless is_valid_value (a_value: ?VALUE): BOOLEAN -- Determines if an argument is valid do Result := a_value /= Void and then a_value.has_value ensure a_value_attached: Result implies a_value /= Void a_value_has_value: Result implies a_value.has_value end end
For clients wanting to create an instance of A
, the following code can now be written and guarenteed.
process (a_value: ?VALUE) -- Processes a value node. local l_a: A do if {A}.is_valid_value (a_value) then -- The value is valid, so we can safely create an instance of {A}. create l_a.make (a_value) ... end end
Extensions
One extension to the proposal and possible advantage over languages with a similar mechanism is the possibility to redefine a objectless routine. The semantics of this have clear rules that govern the behavoir of calls and the calling convention used. This extension requires consideration now because of impacts on user code in the future if the language is to adopt the extension.
For the purpose of explaining the semantics of redefinition, the following classes are used.
class A feature -- Access objectless f: STRING do ... end end
class B inherit A end
Objectless Redefinition
An objectless routine may be redefined objectlessly with redefined functionality.
When used as a client, calling {A}.f
will always take the version from A
. When calling {B}.f
the version from A
will be called unless B
redefines f
:
class B inherit A redefine f end feature -- Access objectless f: STRING do ... end end
Still, when call {A}.f
the version from A
will be taken, but calling {B}.f
will call the version from B
as f
has been redefined objectlessly.
Instance Redefinition
An objectless routine may be redefined to an instance routine but not the other way around. This is permitted because the original objectless routine requires no access to instance attributes or instance routines of the class. An instance redefined objectless routine may call the precursor safely in this respect.
class B inherit A redefine f end feature -- Access f: STRING do Result := Precursor {A}.twin Result.as_lower end end
In instance redefinition, when call {A}.f
the version from A
will be taken because a client called f
using the static calling convention. However, attempting to call {B}.f
will result in a compile-time error because f
is not accessible statically, since it has been redefined as an instance routine.
Instance Call Semantics
When calling f
on an attached entity, of type A
or a subtype, the call semantics behave in the same way as every other call - using dynamic dispatch. If an instance of B
is polymorphically assigned to an instance of A
and B
redefines f
, either objectlessly or otherwise, the call is made on the dynamic type of the attached entity, in this case B
.
From within the confines of A
, or a subclass of it, an an unqualified call to f
the call should be treated in the same way as using an attached entity Current.f
, that is, the call is dynamically dispatched. This behavoir can be overridden using the static access calling convention.
For example, when B
redefines f
, in A
, it can be written {A}.f
to ensure the version of f
in A
is called. Conversely in A
calling f
unqualified will dispatch the call, resulting in f
's implementation in B
being called.