Where Normal Coercion Rules May Fail

The Problem

There is a not-so-uncommon cornercase in which the class-oriented operator concept yields unwanted results. The situation is as follows:

  • Class A is array-like and implements the operation with subclasses of A. Operands which are not an instance of A are assumed to be scalar-like. Hence they are applied to each element of the A instance.
  • Class B is array-like too, but cannot be derived from A because it is not an A. B implements the array coercion operation with A, both in forward as well as in reverse mode.
  • B also implements forward- and reverse operation with scalars (that is, objects that are neither A instances nor B instances).

Now, consider the following expression:

A() + B()

The following happens:

  1. A() checks if it can operate with B().

    1. B() is not A-like. So it’s scalar-like.
    2. B() is hence applied to each element of A().
  2. The elements of A() cannot deal with B(), since B() is in fact an array.

  3. So the reverse operation on B() is invoked, with each of the elements of A().

  4. B() applies the scalar elements of A() to each of its elements in turn.

  5. One ends up with the following situation:

    .      A()      +      B()                                                         =                            A() + B()
    
                                        +---------------------+--------------------+       +----------------------------+-----------------------------+
                                        |                     |                    |       |                            |                             |
                        +---+---+       |        +---+---+    |        +---+---+   |       |   +---------+---------+    |    +---------+---------+    |
        +---+---+       | a | b |       |        | a | b |    |        | a | b |   |       |   |  1 + a  |  1 + b  |    |    |  2 + a  |  2 + b  |    |
        | 1 | 2 |   +   +---+---+   =   |  1  +  +---+---+    |  2  +  +---+---+   |   =   |   +---------+---------+    |    +---------+---------+    |
        +---+---+       | c | d |       |        | c | d |    |        | c | d |   |       |   |  1 + c  |  1 + d  |    |    |  2 + d  |  2 + d  |    |
                        +---+---+       |        +---+---+    |        +---+---+   |       |   +---------+---------+    |    +---------+---------+    |
                                        |                     |                    |       |                            |                             |
                                        +---------------------+--------------------+       +----------------------------+-----------------------------+

Expected (wanted) is the broadcast of A() to the shape of B() and an element-wise operation + on each pair. This is what happens on B() + A():

.      B()     +       A()                                      =         B() + A()

    +---+---+                       +---+---+       +---+---+       +---------+---------+
    | a | b |       +---+---+       | a | b |       | 1 | 1 |       |  a + 1  |  b + 1  |
    +---+---+   +   | 1 | 2 |   =   +---+---+   +   +---+---+   =   +---------+---------+
    | c | d |       +---+---+       | c | d |       | 2 | 2 |       |  c + 2  |  d + 2  |
    +---+---+                       +---+---+       +---+---+       +---------+---------+

Thus the ignorance of A on the probably later introduced class B leads to an unexcepted result.

Fixing this problem is not easy. When one has usually no access anymore to the implementation of A, it seems even impossible. If one still has access to some hooks in the implementation of A, one can monkey-patch A, but this is error-prone and little portable to scenarios where many packages want to access A‘s hooks. Especially for the numpy case, this monkey-patching is possible, although it lets the application currently crash on exit (April 2011, numpy 1.5.1).

The priops Approach

priops encourages all packages involved (here those of A and B) to give their power of deciding what to do away, to the Priop defined in the priops package. There are as many Priops for as many types of operation as needed. Priops consists of a description of the possible operations, optionally together with some cost measure (which might be boolean, for simplicity).

When one wants to use the priop, one retrieves a kind of handle for the priop, defining the search criteria. This handle’s function is to find a path of operations using the information stored in the Priop once the specification of the operation are known (classes of objects, output type ...).

There exists a common data format for operator information exchange between concatenated operations used to carry out the request, and there exists a framework implementing the graph notions used.

The priops approach can be localised to a non-global usecase by instantiating the classes provided by priops in the namespace where they are needed.

Table Of Contents

Previous topic

Concepts

Next topic

API

This Page