Another point that I forgot to mention is the changing of classes like
Pow to container types will break a lot of things.  I think it would
be safer to leave Pow(a, b) as calling a**b (with the dispatch), and
have the dispatch call some class method constructor that just creates
the container class (more or less _new_rawargs).  It's pretty clear to
me from the changes in your branch so far that attempting to change
the classes directly would be a mess, and would break tons of
non-library code.  I think it would also break the obj.func(*args)
expectations of functions like expand().

On Sat, Jul 28, 2012 at 2:35 PM, Ronan Lamy <[email protected]> wrote:
> Le vendredi 27 juillet 2012 à 15:07 -0600, Aaron Meurer a écrit :
>> On Fri, Jul 27, 2012 at 2:55 PM, Ronan Lamy <[email protected]> wrote:
>> > I'm experimenting with a system implementing a perfectly sound
>> > double-dispatch, even in the face of multiple inheritance, bypassing
>> > Python's byzantine and subtly incorrect rules. It is based on a callable
>> > binary_operation object that stores implementations for pairs of types.
>> > When it is called, it finds the implementation corresponding to the most
>> > derived pair of types or raises an error in case of ambiguity.
>> >
>> > Concretely, with the following definitions:
>> >
>> > power = binary_operation()
>> >
>> > @power.define(Expr, Expr) # same as power[Expr, Expr] = _pow_base
>> > def _pow_base(x, y):
>> >     return Pow(x, y)
>> >
>> > @power.define(Expr, Zero)
>> > def _pow_Expr_Zero(x, zero):
>> >     return S.One
>> >
>> > @power.define(Zero, Expr)
>> > def _pow_Zero_Expr(zero, x):
>> >     return S.Zero
>> >
>> > then:
>> >         * power(Symbol('x'), Integer(2)) calls Pow.
>> >         * power(Symbol('x'), S.Zero) returns S.one directly.
>> >         * power(S.Zero, S.Zero) raises an error, because there are two
>> >         conflicting definitions. You can fix this with
>> >         power[Zero, Zero] = power[Expr, Zero].
>>
>> I'd say that's actually correct behavior.  0**0 is defined to be 1,
>> but it's actually an ambiguous definition.
>
> Yes, that's correct. The example was actually meant to demonstrate what
> happens when you make a coding mistake (forgetting that the value of
> 0**0 is a matter of convention).
>
>> >
>> > Now, if we plug this function into Expr.__pow__, we'll be able to define
>> > the behaviour of '**' in a modular way, provided we also turn Pow into
>> > an inert object (i.e. with the behaviour currently corresponding to
>> > evaluate=False). I've started coding in that direction in
>> > https://github.com/rlamy/sympy/tree/binop It works (except for some
>> > problems with integral transforms) but it's a bit slow as the overhead
>> > of double-dispatch is significant.
>>
>> I was going to ask if this was the case.  Is there any way to make it faster?
>
> Caching the dispatch would be an obvious way, though the cache could
> grow quite big, given that we have hundreds of classes.

It doesn't seem to me that that would create that much overhead.

>
> The algorithm could also be improved, as it simply tries all possible
> combinations of superclasses ATM.

This is an interesting problem.  Perhaps the monotonicity of the
Python MRO algorithm can be used to make the algorithm more efficient.

>
>> >
>> > What do you think?
>>
>>
>> So what does this do that the regular dispatch doesn't do?
>
> It guarantees that you don't need to alter existing code if you add a
> subclass. The way '**' works currently is that it always calls Pow which
> handles dispatch on the second argument as a bunch of special cases and
> then dispatches on the first argument. So every time the second argument
> needs to influence the dispatch, the code for this must go in Pow.
>
>> Also, how does it work if I have a custom subclass that I want to
>> define my own Pow behavior with?
>
> You just define your implementations in the place where you define the
> subclass. For instance:
>
> @power.define(MatrixExpr, NegativeOne)
> def _inverse_MatrixExpr(mat, n):
>     return Inverse(mat)
>
> The big advantage of this is if you want two unrelated subclasses to
> interact. Suppose you have matrices and random variables, and then you
> want to implement random matrices, so that Matrix**RandomVariable
> returns a RandomMatrix. With this, you can just define the
> implementation at the same time as RandomMatrix. With the existing
> systems, you'd have to modify Matrix and/or RandomVariable.

So we've talked a little bit about under defined interfaces, but what
about over defined ones?  If two classes try to define
Matrix**RandomVariable, what happens?  Does whichever one happened to
run the last win?  Does it raise an exception?  Does it attempt to
break the tie if one of the classes happens to be Matrix or
RandomVariable?

>
>> Is there any issue with the fact that __pow__ will still be using the
>> normal dispatch?  I guess we could make it never return
>> NotImplemented.
>
> There are several possibilities here, and I don't know which is best. It
> depends how we want to handle the interaction with non-Expr and
> non-Basic classes. Do we handle sympification inside or outside of
> power? Do we let other classes' __rpow__ run or not?

I'm a little wary of this, because it would mean quadruple dispatch.

Aaron Meurer

-- 
You received this message because you are subscribed to the Google Groups 
"sympy" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/sympy?hl=en.

Reply via email to