Mark van Gulik wrote:
> >cstb@pacbell.net
>
> >...snip...
> > implementers have in effect created multiple, incompatible
> > protocols through the 'accident' of implementing behavior
> > based on what can be done efficiently for each particular case,
> > rather than implementing one protocol polymorphically, ...snip...
>
> Yes. Does this count as an anti-pattern, or is it broader
> than that? Maybe "Focused on how instead of what"?
> "Context over Essense"? "Form over Function"? "YA[re]GNI"?
As anti-pattern, its a variant of premature optimization.
But anti-pattern doesn't really fit - the collection hierarchy
as-is wasn't constructed to solve a problem in a context,
rather it is synthesized from various solutions, and has evolved
over time, forming collections of solutions that solve widely
varied problems in widely varied contexts. Only when you step
back and look at the totality of the current hierarchy is it
clear that a unifying principle would improve it even further.
In comparison, the equivalent of "collection hierarchy" in most
other languages is mostly missing and without any apparent
structure, polymorphic or otherwise.
One fascinating aspect of Smalltalk is that it seems to guide
even the discovery of how to improve its own implementation.
[complete BS, but a useful fiction for PR purposes -ed]
I don't recall ever having the experience of, say, looking at
large chunks of C libraries, and exclaiming: "Wow! Look at this -
its nearly brilliant ;-). Just needs another pass, applying
its own principles, and look what we'll get!"
>
> I'm all for protocols, as long as you can attach code to them (having
> tools to *mechanically* reason about them is nice, too).
Right. Dolphin has started exploring the former, for example.
> if you have an Orderable protocol which Magnitude and String can
> choose to implement, then you shouldn't have to implement
> #between:and: in both places (and everywhere else).
Why not?
If I can *write* it once, then ditto the implementation
into any class that wants to 'implement' it {unchanged},
I get the same advantage (OAOO). What's missing is that
protocols are not first class - they currently have no
runtime representation or behavior (except in Dolphin).
>...snip...
>
> If the protocol is for read-like access then this can work, but
> Smalltalk isn't normally structured into these protocols. For
> example, the #at: and #at
ut: methods of OrderedCollection are both
> in the category "accessing".
Categories are not protocols - that's a misnomer on the part
of one vendor. Protocols need to be first class.
>...snip...
>
> protocols are hardly a description of a system instance.
> They are a description of "code". Thus, they are a static
> description. It is their static nature that is useful. Your notion
> of protocol is simply my notion of static type.
Ok. (I'll suggest that changing your nomenclature would be a plus
- the overloaded meaning misleads; the contexts overlap.
)
>...snip...
>
> if you don't have a subtyping relationship between
> ReadableOrderedCollection and OrderedCollection, then behavior like
> "reversed" or "asArray" can't be defined in one-and-only-one place.
Sure it can:
1) In Collection (the usual place)
2) In Object (the preferred place)
> Hence the code smell of singular (and zero) *inheritance*.
>
> Alternatively, if you allow protocols fullfilment and implementation
> inheritance to diverge, I don't believe you can (A) correctly reason
> in the absence of complete source code, and (B) determine the scope
> of a change, even of a pure implementation method.
Perhaps this is just nomenclature again, but I'm confining the term
'inheritance' to the super-sub class hierarchy.
Hence, protocols could be
'inherited' from the super chain,
and also
'mixed-in' (copied) from the 'reference implementation',
as well as
fulfilled but reimplemented in-situ.
>...snip...
>
> I assert it is impossible unless obligations exactly
> match benefits. Thus, the obligations (protocol fullfilment) must
> correspond with the benefits (protocol-method inheritance).
Protocol obligations
are fulfilled by reference, inheritance, or reimplementation.
Benefits
are derivable from cross-pollination as well as from inheritance -
benefits can (should) extend by way of interface-set-membership,
rather than constrained to inheritance through the class hierarchy.
[Still hierarchical, and inheritable, but from the protocol hierarchy.
The two (class and protocol) hierarchies intersect in multiple
places, yet are not synonymous. The class hierarchy (code reuse)
is seen to be a proper subset of the protocol hierarchy
(behavior reuse), hence protocols are more powerful than classes.
]
> Refinement of protocols is another feature one would be foolish to
> live without. It doesn't make much sense to be able to define a
> protocol like ReadableOrderedCollection without being able to refine
> it to "ReadableAndRandomWritableOrderedCollection".
If by 'refinement' you mean 'a convenient notation and tools'
so that one can define new protocols by reference:
protocolB ::= protocolA + {additional selectors}
and be systematically assured that all protocols are unique,
sure.
>
> > >SortedCollection then properly
> > > inherits from ...(wrong answer snipped)...
> >
> > ...anything the implementers choose, because the controlling idea
> > is -implements the protocol-, not -inherits from some particular
> > place-.
>
> They're inseparable because of the symmetric nature of the contract
> (assuming the protocol can provide actual functionality).
Not!
Obligation occurs with respect to the protocol hierarchy,
whereas implementation occurs with respect to the class hierarchy.
> ...snip...
>
> I don't know where the fallacy of separable hierarchies came about,
> but I have yet to see anything useful added to the literature
> as a consequence.
Strongtalk appears (to me) equivalent to what I'm describing,
and is published. There may be room for expounding further though.
> I'm certain Java's
> protocol-ish type system was only implemented as a stop gap measure
> to avoid being *completely* crippled by single inheritance, without
> spending vast fortunes getting a C++-ish multiple inheritance model
> operational.
And it is far less powerful, *because* it is tied to the bumper
of a statically typed vehicle.
>
> > Inheritance is (in Smalltalk) an implementation detail.
> > Unlike a statically typed language, where inheritance has effects
> > which are forcibly exposed, in Smalltalk they can all be
> > encapsulated.
> > [They aren't, always, but they can be. And should be].
>
> Correct, *except* for code duplication. Note that code duplication
> snowballs when a poorly supported
or poorly trained
programmer, regardless of
> protocol attempts to actually
> provide something (i.e., behavior). Why would anyone use a protocol
> system in which ...snipped...
No reason to. Again, the only protocol *system* I've seen
implemented so far is that of Strongtalk.
> >...snip... implementation details ...snip...
> > unless I get some useful property as a result of this choice.
> >
> >...snip...
>
> The issue is obvious. (Safe) code reuse. See also modularity.
Ok. But like tennis shoe sniffing at the airport,
constraints can and should be relaxed, if they don't
provide actually increased safety.
> ... snip ...
>
> > > Interval might, however, be a workable subtype
> > > of ReadableSequenceableColection.
> >
> > Not a terribly useful notion, in any case.
>
> Huh? It's the whole premise of Interval being a collection in the
> first place! .. snip ...
I should have elaborated here.
Not terribly useful, as compared to
supporting the full collectionProtocol.
*Part* of its implementation might benefit from immutability, but
not Interval as a whole. That's the premature optimization thing
again, seems to me. The more interesting properties of Interval
as collection are
o known to have a dense representation
o known to be a monotonically increasing discrete (sub)range
In the general case, I don't see much advantage to branching the
protocol hierarchy at 'Sequenceable', as 'sequence' constraints
arise from how/why the collection is enumerated, not from how
it is implemented.
Are there boundary conditions/ordering dependencies at play
in the algorithm which is driving this enumeration? Each *use*
of enumeration should make this distinction. Some implementations
of collection really need to supply distinct variants of enumeration,
while other collections do not. {An implementation might fail
to provide the full collection protocol - we might call this
a 'feature', but it isn't a 'property'.}
>
> ..snip...
> > > non-subtyping inheritance even foils the
> > > usefulness of tests.
> > ...snip...
> > but rather that
> > >>canSum
> > ^(self allSatisfy: [:elt| elt understandsArithmetic])
> > is true whenever the collections of interest to the system under
> > test are sent the message #sum.
>
> At first this seemed an interesting point, encapsulating the
> structure ...snip... In this case the double-dispatch is
> probably inappropriate,
to do this:
aRuntimeObject>>calculateTotal
someCollection canSum ifFalse: [^self errorBadContents].
^someCollection sum
but probably ok to do this:
TestCaseSubclass>>testSumability
fixture := SomeCollection loadedFrom: someSource.
self assert: fixture canSum
although neither is my preferred path.
Similarly, your suggestion
> as a named structured type
> (i.e., "Summable ::= Collection of ArithmeticValue") not only
> solves the problem but names the abstraction.
is an improvement over the god awful statically typed
collection contents approach, but again misses the mark.
In detail -
If I actually want to be assured of a container
that has only arithmetic values,
there are two ways to do this without static types:
1) Verify the constructed contents at key points prior to use
(the canSum) approach
2) Verify that the construction process allows only
arithmetic values to be added.
(equivalent to net result of a named structured type,
or of any container with statically typed contents
)
Ideally, we'll pick (2), as the error is caught closest
to its source, and the overhead is incurred just once.
So the extra 'typing' is just allowing us to 'trust'
the *contents* of a passed-in container,
without seeing the client code.
The problem is this -
I now have a name that guarantees sumability.
It even says so.
But that isn't what I actually want.
Why? Too specific. I'll need billions of these things.
In this example, what I probably want to ensure
(without realizing it yet - too early/not enough refactoring)
is that the thing 'passed-in' is reducibleToArithmeticValue.
Because if it is, I can treat the parameter as a composite -
so if I ask it to plus-reduce, and
it *is* anArithmeticValue (scalar) -> it answers itself,
otherwise, it sums itself and answers the sum.
Hence, the protocol I'm after is #reducibleToArithmeticValue,
and said protocol is *not* a named structured type.
I claim that, in general:
Protocols are not obtained via the composition of named types.
Protocols must be 'learned', i.e. discovered, and then
implemented on purpose.
Static typing leads one to believe that the phenomenon of
accidental success (named composition yielding useful results)
is 'the way' to go about extending; Hey, it works.
[ Equations of the form {ax^2 + by^2 = 0} can be solved
by selecting {x=0, y=0}; Hey, it works.
]
Static typing sends the developer down the rat hole
of over-specification, leading to premature optimization,
and over time, to exceptionally brittle (aka static) systems.
> Your #canSum is completely inappropriate, since it neither part
> of a protocol nor part of an implementation.
Right - it is only there because I'm developing test first.
It is part of the testing protocol, and otherwise unused.
> *Every* collection must understand it (and therefore
> implement it), regardless of the protocols those collections
> purport to support.
If we push #sum up to Collection, yes. Otherwise, just those
onto which #sum has been grafted.
We seem to agree it was a bad idea - I was eliminating your
criteria for badness, to force using mine ;-)
> Thus, only by building things *outside* your type system
> can you support this apparently simple concept. Either reductio ad
> absurdum, or this simple concept can not be safely typed (like Arrays
> in Java).
Umm - not my type system, just my response of a way
of getting results beyond what you suggested was possible
in a hypothetical situation. In order to hone our arguments.
>
> > > ..snip ...
> > > multiple inheritance (and more complex type constructions)
> > > have their fate tied to strong typing. One without the
> > > other is a disaster.
> >
> > Static typing - yes.
> > But one with the other is also a disaster, so no harm done, right?
> > ;-}
>
> Most implementations of both have been disasters,
Agreed.
> but I believe (1) a
> proper synthesis is possible,
Q: Having worked through the above,
do you still believe it?
> and (2) I have produced the core of such
> a viable synthesis in Avail. The key has almost nothing to do with
> how little or how much functionality to include, but rather *which*
> meta-abstractions produce the right kinds of systems. Sort of like
> almost ignoring the axioms but choosing the substitution rules with
> great care.
I'd like to see it - can you email some links/reference material?
Thanks Mark - a lot of work to get through,
but a wonderful exploration, and useful exchange.
Regards,
-cstb