Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

weak roles

4 views
Skip to first unread message

Yuval Kogman

unread,
Aug 7, 2006, 9:04:00 PM8/7/06
to perl6-l...@perl.org
The CPAN has many reinvented wheels. This happened since we have so
many authors (== opinions) over so many years.

For example, we have Mail::Message to Email::Simple, to
Mail::Internet to.... They all do the same thing differently.

Then there is Email::Abstract, which provides a simplified API for
all of these together, so that modules like Mail::Thread can work
with either.

With the introduction of roles, supposedly this is going to be less
of a problem. Email::Abstract would ideally be a role that the
organically grown message abstracting classes just 'do'.

However, suppose that Email::Abstract itself would be reinvented.
The message modules now have to choose between either
Email::Abstract or Email::Abstract::Better as the one role to do
(since they probably conflict), or it will have to do weird magic to
disambiguate in a context sensitive way.

I think a nice idea to work around this would be to introduce weak
roles, that are lexically scoped.

The way it could work is a bit like this:

class Mail::TheOneTrueWay {
does Mail::SomeAPI is weak {
method header; # this method has different semantics for
# each role. It would normally conflict.
}

does Mail::SomeOtherAPI is weak {
method header;
}

method body; # this method is shared between the two roles

}

Then, when a module is expecting a message object it declares the
role it wants this object to do for this scope:

sub analyze_mail ( Mail::SomeAPI $message ) {
$message.foo; # unambiguous
}

This way coercion can be avoided, which means that keeping mutable
coerced objects synchronized is no longer necessary, thus
simplifying the code path and encouraging even more
pluggability/polymorphism.

I'm not sure on the behavior of $obj.does outside of the role
strenghning scope.

I am pretty sure that this should be purely lexical, and not
dynamic, even for .does (if it's true on the inside).

Perhaps an invokation of an ambiguous method is an error for the
caller ($object responds to N flavours of the "header" method, you
must explicitly say which flavour you prefer right now).

Delegates are another way around this but let's face it:

1. they're not as popular as they should be

2. they're more classes to write

3. they're harder to use

Consequentially we have fairly few delegate based APIs for these
problems (Email:Abstract is the only one I know).

--
Yuval Kogman <nothi...@woobling.org>
http://nothingmuch.woobling.org 0xEBD27418

Mark Overmeer

unread,
Aug 8, 2006, 3:05:07 AM8/8/06
to perl6-l...@perl.org
* Yuval Kogman (nothi...@woobling.org) [060808 01:04]:

> The CPAN has many reinvented wheels. This happened since we have so
> many authors (== opinions) over so many years.
>
> For example, we have Mail::Message to Email::Simple, to
> Mail::Internet to.... They all do the same thing differently.

Sometimes, the external interface of a module looks the same, and
there are cases where the internals behave as such as well. In
general, however, the internals are more different then the user
code shows. That makes your proposal unworkable.

In this example:
Mail::Internet is deprecated: does not even support multiparts
Email::Simple does e-mail as simple as possible
Mail::Message does e-mail as RFC-compliant as possible

> Then there is Email::Abstract, which provides a simplified API for
> all of these together, so that modules like Mail::Thread can work
> with either.

For different approaches to tackle the (in this case e-mail) problems
you need different internal helper objects, different methods, different
attributes. It is in general hard to force an extra interface
definition inside the same class.

> Perhaps an invokation of an ambiguous method is an error for the
> caller ($object responds to N flavours of the "header" method, you
> must explicitly say which flavour you prefer right now).

But that's not all. A header contains fields, which are usually objects
as well. How would you include variation in those in the definition?
And different versions of one module which use your Email::Abstract
module? You must not try to understand the structure of other modules
into such a detail, because you cannot maintain it.

> Delegates are another way around this but let's face it:

Delegates are not sufficient to implement such couplings between
"unrelated" modules: you commonly need serious parameter rewrites.
Delegates are nice within a homogeneous concept.

The only way I have found to work is with "wrapper" objects which
translate between the two modules. The advantanges are that you have
localized the hassle of interfacing to one class, and you hide the
internal complexity of both sides (OO is about abstraction!).

Of course, we could use the Email::Abstract interface as a base-
class to all email related modules, but you know that this wouldn't
work for the Perl community...
--
Regards,

MarkOv

------------------------------------------------------------------------
Mark Overmeer MSc MARKOV Solutions
Ma...@Overmeer.net solu...@overmeer.net
http://Mark.Overmeer.net http://solutions.overmeer.net

Yuval Kogman

unread,
Aug 8, 2006, 4:50:04 AM8/8/06
to perl6-l...@perl.org
On Tue, Aug 08, 2006 at 09:05:07 +0200, Mark Overmeer wrote:

> Sometimes, the external interface of a module looks the same, and
> there are cases where the internals behave as such as well. In
> general, however, the internals are more different then the user
> code shows. That makes your proposal unworkable.

The whole point is to implement the same interface in terms of
different internals.

> For different approaches to tackle the (in this case e-mail) problems
> you need different internal helper objects, different methods, different
> attributes. It is in general hard to force an extra interface
> definition inside the same class.

That's why they are in separate roles that are "off" by default.

These roles are orthogonal.

> But that's not all. A header contains fields, which are usually objects
> as well. How would you include variation in those in the definition?

Every "off by default" role is distinct. You cannot use them
simultaneously if they conflict.

> And different versions of one module which use your Email::Abstract
> module? You must not try to understand the structure of other modules
> into such a detail, because you cannot maintain it.

Huh?

> Delegates are not sufficient to implement such couplings between
> "unrelated" modules: you commonly need serious parameter rewrites.
> Delegates are nice within a homogeneous concept.

Email::Abstract works. It's not the most pleasant thing, which is
why it's problematic, but it *does* do the job.

> The only way I have found to work is with "wrapper" objects which
> translate between the two modules.

a.k.a a delegate

> The advantanges are that you have
> localized the hassle of interfacing to one class, and you hide the
> internal complexity of both sides (OO is about abstraction!).

Which is much more work than what roles make you do.


>
> Of course, we could use the Email::Abstract interface as a base-
> class to all email related modules, but you know that this wouldn't
> work for the Perl community...

Base classes, as opposed to roles, don't work well at *all* for
these types of scenarios.

Yuval Kogman

unread,
Aug 8, 2006, 5:14:01 AM8/8/06
to perl6-l...@perl.org
On Tue, Aug 08, 2006 at 09:05:07 +0200, Mark Overmeer wrote:

> Of course, we could use the Email::Abstract interface as a base-
> class to all email related modules, but you know that this wouldn't
> work for the Perl community...

Actually I'd like to expand on this.

There are two problem with your solution, that are not community
related.

The first is that this is no different from using the role
Email::Abstract or whatever it'll be in the classes. Roles can be
abstract, too.

The second is the issue i raised though - when It's too difficult to
put together two (or more) abstract roles (or base classes) in the
same class, you may declare how to do all of them in a conflicting
way. That is you can be both Email::Abstract and
Email::Abstract::Better, even though they define different
interfaces by simply declaring that you cannot be both of them at
the same time, and whoever is invoking the methods must explicitly
say which behavior it prefers.

Luke Palmer

unread,
Aug 8, 2006, 7:14:40 AM8/8/06
to perl6-l...@perl.org
On 8/8/06, Yuval Kogman <nothi...@woobling.org> wrote:
> The way it could work is a bit like this:
>
> class Mail::TheOneTrueWay {
> does Mail::SomeAPI is weak {
> method header; # this method has different semantics for
> # each role. It would normally conflict.
> }
>
> does Mail::SomeOtherAPI is weak {
> method header;
> }
>
> method body; # this method is shared between the two roles
>
> }

This code raises a few questions:

Mail::TheOneTrueWay.new.header; # which one?

A related question: if Mail::SomeAPI and Mail::SomeOtherAPI both
exported a method, say "send", and you mentioned it outside the weak
role blocks, which one does it call.

Presumably the answer to both of these questions is "error". But that
answer has disastrous consequences, for now that even though the class
it does both of these roles, if you don't use type annotation, it in
fact does neither of them.

> Delegates are another way around this but let's face it:
>
> 1. they're not as popular as they should be
>
> 2. they're more classes to write
>
> 3. they're harder to use

Well, yeah, but I think they're the right solution.
In a situation where I have the same name meaning multiple things, I
want to keep them as separate as possible, because DWIMs will just get
me in trouble (because the code looks perfect, so it's hard to track
down if it doesn't behave perfectly).

Delegates make your intention explicit and independent of type
annotations (which is a plus in my book). Have your class do neither
role, so that you get an error when you didn't say what you meant.

However, that still leaves the problem of code that is in the roles
and used in the classes. In the case that there is duplicate
functionality, I would say pick the one you like better and do that
one, and write an adapter for the other one.

In the case that there is functionality you want from both, I'm at a
loss. If I were forced to use standard OO tools, I'd probably do
something awful like this:

class SomeAPIPart {
does Mail::SomeAPI;
has $!body;

method header() {
# do stuff $!body.header to make it conform
}
}

# same for SomeOtherAPIPart

class TheOneTrueWay {
has $!some_api = SomeAPIPart.new(body => self);
has $!some_other_api = SomeOtherAPIPart.new(body => self);

# use stuff from $!some_api and $!some_other_api
}

Where SomeAPIPart and SomeOtherAPIPart serve as two-way adapters to
those interfaces.

That is a horrible amount of structure accomplishing relatively
little. However, I think in practical situations a lot of that
structure would be mechanical, so one could write a module to handle
it.

I agree with the problem you are addressing; there's no good way in
module-free Perl to solve it. However, having thought a great deal
about objects with "views" when I was constructing theory.pod, I just
don't think they're a good idea---ever. An automated adapter class
solution is our best bet.

Oh, and hello everyone. Long time no see :-)

Luke

Yuval Kogman

unread,
Aug 8, 2006, 7:21:14 AM8/8/06
to Luke Palmer, perl6-l...@perl.org
On Tue, Aug 08, 2006 at 11:14:40 +0000, Luke Palmer wrote:

> Mail::TheOneTrueWay.new.header; # which one?

Either neither, or the "default" one (which is declared default by
the class).

> A related question: if Mail::SomeAPI and Mail::SomeOtherAPI both
> exported a method, say "send", and you mentioned it outside the weak
> role blocks, which one does it call.

If "mentioned" means used, then that's a conflict. If it gets that
far you *really* need a delegate.

If "mentioned" means declared than neither - it's the same one for
both.

> Well, yeah, but I think they're the right solution.

They may be right, but it's hard to get an organically growing
community to design their code for them.

I personally prefer delegates for almost any design dillema, but
most CPAN modules aren't that way.

<snipped stuff about adaptors>

Yes, this is necessary for anything heavyweight and probably better
design, but again, hard to encourage on a wide scale.

> Oh, and hello everyone. Long time no see :-)

Welcome back =)

Luke Palmer

unread,
Aug 8, 2006, 7:35:30 AM8/8/06
to Luke Palmer, perl6-l...@perl.org
On 8/8/06, Yuval Kogman <nothi...@woobling.org> wrote:
> I personally prefer delegates for almost any design dillema, but
> most CPAN modules aren't that way.

Well, what way are they? How else has this problem been solved?

> <snipped stuff about adaptors>
>
> Yes, this is necessary for anything heavyweight and probably better
> design, but again, hard to encourage on a wide scale.

Yeah, thinking in terms of delegates takes a little brain training.
I wish there were a simpler way. Every time I tried to add views to
the object model, I found immense amounts of complexity lurking. As
an example, say you declared the default header method as the one in
SomeOtherAPI as you mentioned above. If a function declares a
parameter as a SomeAPI and then passes it on to a helper function,
which leaves it untyped, which then calls header, what happens?

Since your proposal is lexical, SomeOtherAPI's is called, which is
probably not what was intended. So now to refactor, you have to
restate your types. Also, any time you use objects through role
interfaces, you have to type those parameters lest somebody weak-role
their object in a way disfavorable to your code.

If delegates are the right answer but not approachable, presumably
we're looking for a *simpler* solution to this problem. I don't think
this is it.

Luke

Yuval Kogman

unread,
Aug 8, 2006, 7:40:59 AM8/8/06
to Luke Palmer, perl6-l...@perl.org
On Tue, Aug 08, 2006 at 11:35:30 +0000, Luke Palmer wrote:
> On 8/8/06, Yuval Kogman <nothi...@woobling.org> wrote:
> >I personally prefer delegates for almost any design dillema, but
> >most CPAN modules aren't that way.
>
> Well, what way are they?

Usually not polymorphic at all.

We have the capabilities to do very flexible polymorphism with
delegation today. Roles aren't necessary from a purist perspective,
they just make it much easier to do very flexible polymorphism more
often since it'll be easier.

> How else has this problem been solved?

I dunno...

> If a function declares a
> parameter as a SomeAPI and then passes it on to a helper function,
> which leaves it untyped, which then calls header, what happens?
>
> Since your proposal is lexical, SomeOtherAPI's is called, which is
> probably not what was intended.

My original idea was that this is again an ambiguity error - without
the helper function stating which role it's using it cannot dispatch
the "header" method at all, since it's in a role-conflicted state.

Luke Palmer

unread,
Aug 8, 2006, 8:00:03 AM8/8/06
to Luke Palmer, perl6-l...@perl.org
On 8/8/06, Yuval Kogman <nothi...@woobling.org> wrote:
> My original idea was that this is again an ambiguity error - without
> the helper function stating which role it's using it cannot dispatch
> the "header" method at all, since it's in a role-conflicted state.

The difference, though, is that this ambiguity error happens at
call-time, whereas most ambiguity errors happen at composition time.

It strikes me that there is a happy medium.

class Mail::TheOneTrueWay {
has $.some_api adapts Mail::SomeAPI {


method header;
# this method has different semantics for
# each role. It would normally conflict.
}

has $.some_other_api adapts Mail::SomeOtherAPI {
method header;
}

method body; # this method is shared between the two roles
}

Which would pull all that multiple delegation bullshit in my earlier
post. Creating the class is just as easy, and using it involves
putting .some_api and .some_other_api in appropriate places like a
delegation interface. It's not quite as DWIMmy, but the class
doesn't do either role so errors will be caught quickly. It is also
annotation-agnostic.

Luke

Yuval Kogman

unread,
Aug 8, 2006, 9:00:40 AM8/8/06
to Luke Palmer, perl6-l...@perl.org
On Tue, Aug 08, 2006 at 12:00:03 +0000, Luke Palmer wrote:

> The difference, though, is that this ambiguity error happens at
> call-time, whereas most ambiguity errors happen at composition time.

As I saw it it's not really a role, it's more of a no-op coercion.

Instead of actually writing an adaptor class, you could get off
cheaply if the adaptor doesn't need any more instance data than is
already provided by the "main" object.

So yes, it's runtime, but it doesn't matter.

> It strikes me that there is a happy medium.
>
> class Mail::TheOneTrueWay {
> has $.some_api adapts Mail::SomeAPI {
> method header;
> # this method has different semantics for
> # each role. It would normally conflict.
> }
>
> has $.some_other_api adapts Mail::SomeOtherAPI {
> method header;
> }
>
> method body; # this method is shared between the two roles
> }
>
> Which would pull all that multiple delegation bullshit in my earlier
> post. Creating the class is just as easy, and using it involves
> putting .some_api and .some_other_api in appropriate places like a
> delegation interface. It's not quite as DWIMmy, but the class
> doesn't do either role so errors will be caught quickly. It is also
> annotation-agnostic.

Hmm... Are the 'adapts' things actual class bodies? Like an inner
class?

0 new messages