Luke Palmer wrote:
Jonathan Lang wrote:
> Agreed.  The question is whether you think of a role as a set of
> methods ("intension set") or as a set of instances ("extension set").
> FWIW, it wasn't until this thread that I even registered that the
> latter possibility existed.  Given the above contrast between roles
> and subtypes, I would really like to keep the extension set mindset
> limited to subtypes and the intension set mindset limited to roles: a
> role is an interface (i.e., a set of methods); a type is a set of
> possible instances.

Eh, a proposal is nothing if you can't make sense of it from both
mindsets, because they both are real.  While you may think of a role
as an interface, it still does have objects that can obey it, so the
set of such objects still exists.  My brain seems to prefer the
extensional mindset, but it's important to try to make sense of
everything from both.  Hmm, the extensional is the more "spookily
theoretical" of the two, since Perl will know the interface, but it
won't create the set of all possible, eventual instances.

OK.  With this in mind, I suppose that set operations, per se, are
indeed out.  Let's see what can be done with "does" and "done_by" (for
lack of a better name).

First, note that

 role Foo does Bar does Baz { ... }

is exactly the same as

 role Foo {
   does Bar;
   does Baz;
   ...
 }

Likewise, I'd expect

 role Foo done_by Bar { ... }

to be equivalent to

 role Foo { done_by Bar; ... }

or

 role Foo { ... ; done_by Bar }

The contents of the curly braces should always define the interface
that the role brings to the table.  Furthermore, "done_by" statements
should have no effect whatsoever on Foo's interface, other than to
define restrictions on what is legal in Foo (maybe).  In extension set
terminology, the above means that "Foo = ...; Bar (+)= Foo".  In
effect, "Foo done_by Bar" means "modify Bar to include 'does Foo'".

In this way, mixtures of "does" and "done_by" work in an intuitive
way: only "does" helps establish what the role is capable of, while
"done_by" feeds those capabilities into another, existing role.

Which gets back to the issue of whether roles should be mutable or
not.  If roles are immutable, then "done_by" is going to have to levy
some rather heavy-handed restrictions on the new role, specifically to
avoid new capabilities being back-edited into existing roles.  If
roles are mutable, "done_by" needn't impose any restrictions - but the
creator of the new role has the responsibility to ensure that any
additions that he makes doesn't break any existing classes.

Unfortunately, this approach completely misses the boat in terms of
what I was looking for in supertyping - namely, the ability to reuse
portions of an existing interface (such as the aforementioned "extract
an Order role out of the Num role") - useful when using modules where
the module designer overcomplicated things, but you don't want to
rebuild everything from scratch.  That, I think, is the common feature
of both goals: the ability to compensate for another coder who didn't
do things exactly the way you want them done, but who was reasonably
close.

Note that I *am* arguing for a back-editing behavior.  You are of
course allowed to remove anything (by not declaring it), and you are
also allowed to add new methods as long as they have a default
implementation.  You are not allowed to add new unimplemented, or
required, methods.

Note that the "default implementation" requirement need only be
specified in terms of the done_by roles: "method foo(Foo: ) { ... }"
is perfectly legitimate, as long as &Foo::foo:(Bar: ) and
&Foo::foo:(Baz: ) are more solidly defined, given "Foo done_by(Bar)
done_by(Baz)".

(BTW: as I see it, all methods defined in Foo are "required".  I'd
prefer "unimplemented" or "abstract" as the description for methods
with '{ ... }' as the body.)

The reasoning for this is mostly utility.  How are you supposed to
generalize Num to Complex (okay, fine, I'll try to come up with
another example.  Is it okay if it is also from math?) if Num didn't
know that an "imaginary part" existed?

I suspect that Num vs. Complex is considered to be a poor choice for
an example because Num and Complex are going to be standard types
already.

You could really consider that syntax sugar if you like:

    role Foo {
        method foo() { "hello" }
    }
    role Bar superdoes Foo {
        method bar() { "world" }
    }

Is equivalent to declaring Bar with no methods, and then adding:

    multi sub bar(Bar:) { "world" }

Modulo some minor differences, but the semantics are essentially the
same.  I don't think we'd want to disable back-editing because then we
would be encouraging the idiom I just demonstrated, which is less
clear (IMO).

My understanding is that subs can't have invocants; only methods and
submethods can, and both of those must be defined within a role or
class.

Back-editing seems weird from an intensional mindset, but perfectly
natural from an extensional mindset (you are just defining functions
on things).  If you don't think of a role as a set of methods, but
rather a tree of sets of methods (like inheritance), then back-editing
seems more natural (you just insert a different parent above the old
role).

As you say, though, both mindsets should be accommodated.  Part of the
beauty of role composition, as opposed to class inheritance, is that
you _don't_ have to think of it as a hierarchy of sets; it all
flattens out into a single, consistent set.  From the "roles as
interfaces" perspective, the wierdest aspect of back-editing is that
roles are mutable now; you can't assume that just because a role
doesn't have a given capability when you first load it into your
program, that it will never have that capability.  You _can_ assume
that whatever capabilities it does have won't change; back-editing can
add new tools, but it can't change existing ones.

> One problem with this is that the default return value for Num.im
> ought to be zero; the default return value for Complex.im ought to be
> whatever the imaginary component of the number is.  So role Complex
> would need both
>
>     method im (Complex: ) { return .im }
>     method im (Num: ) { return 0 }

Good point.  Hmm, that's a bit awkward.

It's also syntax that S12 already permits, for the purpose of Dogwood
disambiguation.

For now (because of this example, in fact), I'm inclined to change the
proposal to "please don't design the language to prevent a module from
implementing supertyping".  I think disallowing reopening of roles
will prevent that.

Maybe we allow reopening of roles, but new method declarations have to
come with defaults; that way you are not (as above) invalidating
existing objects, just adding behavior to them.  That is a tricky
issue, though, because such definitions might add conflicts and
invalidate those objects anyway.  Maybe more-recently added methods
have less priority than older ones.  That stinks.  Hmm.  I need to
think about this.

I suspect that this is the core of the problem: if you back-edit a new
method into a role, you don't necessarily have to give it a default
(roles can be abstract); but you _do_ have to make sure that every
class that already does that role (directly or indirectly) has a valid
definition for the new method (classes must be concrete).  This is the
responsibility of the creator of the new role; and since classes are
open by default, it's doable: you just have to make sure to have a
series of "class ___ is also ..." statements that precede the new
role's definition, inserting the appropriate method definitions.  If
roles are open (or can be opened), then you can likewise amend them
preemptively to resolve conflicts that the new role will introduce:

 role Num { ... }
 ...
 role Num is also { method im() { return 0 } }
 role Complex done_by Num {
   method im() { ... }
   ...
 }

If "back-editing" is allowed, then roles ought to be able to be open.
It isn't strictly necessary; but it reduces the clumsiness of
"back-editing" by avoiding the need to handle all of the newly
introduced conflicts in the classes.

Of course, with open roles, the above could also be:

 role Num { ... }
 ...
 role Complex {
   method im() { ... }
   ...
 }
 role Num is also { does Complex; method im() { return 0 } }

This, perhaps, would be the most intuitive way of handling
"back-edit"-capable supertyping: no new syntax is involved.

--
Jonathan "Dataweaver" Lang

Reply via email to