Source code for discopy.cat

# -*- coding: utf-8 -*-

"""
Implements free dagger categories and functors.

We can create boxes with objects as domain and codomain:

>>> x, y, z = Ob('x'), Ob('y'), Ob('z')
>>> f, g, h = Box('f', x, y), Box('g', y, z), Box('h', z, x)

We can create arbitrary arrows with composition:

>>> arrow = Arrow(x, x, [f, g, h])
>>> assert arrow == f >> g >> h == h << g << f

We can create dagger functors from the free category to itself:

>>> ob = {x: z, y: y, z: x}
>>> ar = {f: g[::-1], g: f[::-1], h: h[::-1]}
>>> F = Functor(ob, ar)
>>> assert F(arrow) == (h >> f >> g)[::-1]
"""

from functools import total_ordering
from collections.abc import Mapping, Iterable

import numpy as np

from discopy import messages
from discopy.utils import factory_name, from_tree, rsubs, unbiased


[docs]@total_ordering class Ob: """ Defines an object in a free category, only distinguished by its name. Parameters ---------- name : any Name of the object. Note ---- When printing an object, we only print its name. >>> x = Ob('x') >>> print(x) x Objects are equal only to objects with equal names. >>> x = Ob('x') >>> assert x == Ob('x') and x != 'x' and x != Ob('y') Objects are hashable whenever their name is. >>> d = {Ob(['x', 'y']): 42} Traceback (most recent call last): ... TypeError: unhashable type: 'list' """ def __init__(self, name): self._name = name @property def name(self): """ The name of an object is immutable, it cannot be empty. >>> x = Ob('x') >>> x.name 'x' >>> x.name = 'y' Traceback (most recent call last): ... AttributeError: can't set attribute... """ return self._name def __repr__(self): return "Ob({})".format(repr(self.name)) def __str__(self): return str(self.name) def __eq__(self, other): if not isinstance(other, Ob): return False return self.name == other.name def __hash__(self): return hash(self.name) def __lt__(self, other): return self.name < other.name def to_tree(self): return {'factory': factory_name(self), 'name': self.name} @classmethod def from_tree(cls, tree): return cls(tree['name'])
[docs]class Arrow: """ Defines an arrow in a free dagger category. Parameters ---------- dom : cat.Ob Domain of the arrow. cod : cat.Ob Codomain of the arrow. boxes : list of :class:`Arrow` Boxes of the arrow. Raises ------ :class:`cat.AxiomError` Whenever the boxes do not compose. Examples -------- >>> x, y, z = Ob('x'), Ob('y'), Ob('z') >>> f, g, h = Box('f', x, y), Box('g', y, z), Box('h', z, x) >>> arrow = Arrow(x, x, [f, g, h]) >>> print(arrow[::-1]) h[::-1] >> g[::-1] >> f[::-1] """ def __init__(self, dom, cod, boxes, _scan=True): if not isinstance(dom, Ob): raise TypeError(messages.type_err(Ob, dom)) if not isinstance(cod, Ob): raise TypeError(messages.type_err(Ob, cod)) if _scan: scan = dom for depth, box in enumerate(boxes): if not isinstance(box, Arrow): raise TypeError(messages.type_err(Arrow, box)) if box.dom != scan: raise AxiomError(messages.does_not_compose( boxes[depth - 1] if depth else Id(dom), box)) scan = box.cod if scan != cod: raise AxiomError(messages.does_not_compose( boxes[-1] if boxes else Id(dom), Id(cod))) self._dom, self._cod, self._boxes = dom, cod, boxes
[docs] @staticmethod def upgrade(old): """ Allows class inheritance. """ return old
@property def dom(self): """ The domain of an arrow is immutable. >>> arrow = Arrow(Ob('x'), Ob('x'), []) >>> assert arrow.dom == Ob('x') >>> arrow.dom = Ob('y') # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: can't set attribute... """ return self._dom @property def cod(self): """ The codomain of an arrow is immutable. >>> arrow = Arrow(Ob('x'), Ob('x'), []) >>> assert arrow.cod == Ob('x') >>> arrow.cod = Ob('y') # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: can't set attribute... """ return self._cod @property def boxes(self): """ The list of boxes in an arrow is immutable. Use composition instead. >>> f = Box('f', Ob('x'), Ob('y')) >>> arrow = Arrow(Ob('x'), Ob('x'), []) >>> arrow.boxes.append(f) # This does nothing. >>> assert f not in arrow.boxes """ return list(self._boxes) def __iter__(self): for box in self.boxes: yield box def __getitem__(self, key): if isinstance(key, slice): if key.step == -1: boxes = [box[::-1] for box in self.boxes[key]] return self.upgrade( Arrow(self.cod, self.dom, boxes, _scan=False)) if (key.step or 1) != 1: raise IndexError boxes = self.boxes[key] if not boxes: if (key.start or 0) >= len(self): return Id(self.cod) if (key.start or 0) <= -len(self): return Id(self.dom) return Id(self.boxes[key.start or 0].dom) return self.upgrade( Arrow(boxes[0].dom, boxes[-1].cod, boxes, _scan=False)) return self.boxes[key] def __len__(self): return len(self.boxes) def __repr__(self): if not self.boxes: # i.e. self is identity. return repr(Id(self.dom)) if len(self.boxes) == 1: # i.e. self is a box. return repr(self.boxes[0]) return "Arrow(dom={}, cod={}, boxes={})".format( repr(self.dom), repr(self.cod), repr(self.boxes)) def __str__(self): return ' >> '.join(map(str, self)) or str(self.id(self.dom)) def __eq__(self, other): if not isinstance(other, Arrow): return False return all(getattr(self, a) == getattr(other, a) for a in ["dom", "cod", "boxes"]) def __hash__(self): return hash(repr(self)) def __add__(self, other): return self.sum([self]) + other def __radd__(self, other): return self + other
[docs] def then(self, *others): """ Returns the composition of `self` with arrows `others`. This method is called using the binary operators `>>` and `<<`: >>> x, y, z = Ob('x'), Ob('y'), Ob('z') >>> f, g, h = Box('f', x, y), Box('g', y, z), Box('h', z, x) >>> assert f.then(g) == f >> g == g << f Parameters ---------- others : cat.Arrow such that `self.cod == others[0].dom` and `all(x.cod == y.dom for x, y in zip(others, others[1:])`. Returns ------- arrow : cat.Arrow such that :code:`arrow.boxes == self.boxes + sum(other.boxes for other in others, [])`. Raises ------ :class:`cat.AxiomError` whenever `self` and `others` do not compose. Notes ----- We can check the axioms of categories (i.e. composition is unital and associative): >>> assert f >> Id(y) == f == Id(x) >> f >>> assert (f >> g) >> h == f >> (g >> h) """ if not others: return self if any(isinstance(other, Sum) for other in others): return self.sum([self]).then(*others) for i, other in enumerate(others): if not isinstance(other, Arrow): raise TypeError(messages.type_err(Arrow, other)) for x, y in zip((self, ) + others, others): if x.cod != y.dom: raise AxiomError(messages.does_not_compose(x, y)) boxes = self.boxes + sum([other.boxes for other in others], []) return self.upgrade(Arrow(self.dom, others[-1].cod, boxes))
def __rshift__(self, other): return self.then(other) def __lshift__(self, other): return other.then(self)
[docs] def dagger(self): """ Returns the dagger of `self`, this method is called using the unary operator :code:`[::-1]`, i.e. :code:`self[::-1] == self.dagger()`. Returns ------- arrow : cat.Arrow Such that :code:`arrow.boxes == [box[::-1] for box in self[::-1]]`. Notes ----- We can check the axioms of dagger (i.e. a contravariant involutive identity-on-objects endofunctor): >>> x, y, z = Ob('x'), Ob('y'), Ob('z') >>> f, g = Box('f', x, y), Box('g', y, z) >>> assert f[::-1][::-1] == f >>> assert Id(x)[::-1] == Id(x) >>> assert (f >> g)[::-1] == g[::-1] >> f[::-1] """ return self[::-1]
[docs] @staticmethod def id(dom): """ Returns the identity arrow on `dom`. >>> x = Ob('x') >>> assert Arrow.id(x) == Id(x) == Arrow(x, x, []) Parameters ---------- dom : cat.Ob Any object. Returns ------- cat.Id """ return Id(dom)
@property def free_symbols(self): """ Free symbols in a :class:`Arrow`. >>> from sympy.abc import phi, psi >>> x, y = Ob('x'), Ob('y') >>> f = Box('f', x, y, data={"Alice": [phi + 1]}) >>> g = Box('g', y, x, data={"Bob": [psi / 2]}) >>> assert (f >> g).free_symbols == {phi, psi} """ return {x for box in self.boxes for x in box.free_symbols}
[docs] def subs(self, *args): """ Substitute a variable by an expression. Parameters ---------- var : sympy.Symbol Subtituted variable. expr : sympy.Expr Substituting expression. Returns ------- arrow : Arrow Note ---- You can give a list of (var, expr) pairs for multiple substitution. Examples -------- >>> from sympy.abc import phi, psi >>> x, y = Ob('x'), Ob('y') >>> f = Box('f', x, y, data={"Alice": [phi + 1]}) >>> g = Box('g', y, x, data={"Bob": [psi / 2]}) >>> assert (f >> g).subs(phi, phi + 1) == f.subs(phi, phi + 1) >> g >>> assert (f >> g).subs(phi, 1) == f.subs(phi, 1) >> g >>> assert (f >> g).subs(psi, 1) == f >> g.subs(psi, 1) """ return self.upgrade( Functor(ob=lambda x: x, ar=lambda f: f.subs(*args))(self))
[docs] def lambdify(self, *symbols, **kwargs): """ Turns a symbolic diagram into a function from parameters to diagram. Parameters ---------- symbols : list of sympy.Symbol Inputs of the lambda. kwargs : any Passed to sympy.lambdify Returns ------- lambda : callable Takes concrete values returns concrete diagrams. Examples -------- >>> from sympy.abc import phi, psi >>> x, y, z = Ob('x'), Ob('y'), Ob('z') >>> f, g = Box('f', x, y, data=phi), Box('g', y, z, data=psi) >>> assert f.lambdify(psi)(42) == f >>> assert (f >> g).lambdify(phi, psi)(42, 43)\\ ... == Box('f', x, y, data=42) >> Box('g', y, z, data=43) """ return lambda *xs: self.id(self.dom).then(*( box.lambdify(*symbols, **kwargs)(*xs) for box in self.boxes))
[docs] def bubble(self, **params): """ Returns a :class:`cat.Bubble` with the diagram inside. """ return self.bubble_factory(self, **params)
def fmap(self, func): return func(self)
[docs] def to_tree(self): """ Encodes an arrow as a tree. """ return { 'factory': factory_name(self), 'dom': self.dom.to_tree(), 'cod': self.cod.to_tree(), 'boxes': [box.to_tree() for box in self.boxes]}
[docs] @classmethod def from_tree(cls, tree): """ Decodes a tree as an arrow. """ dom, cod = map(from_tree, (tree['dom'], tree['cod'])) boxes = list(map(from_tree, tree['boxes'])) return cls(dom, cod, boxes, _scan=False)
[docs]class Id(Arrow): """ Defines the identity arrow on `dom`, i.e. with an empty list of boxes. Parameters ---------- dom : cat.Ob Any object. Examples -------- >>> x = Ob('x') >>> assert Id(x) == Arrow(x, x, []) See also -------- cat.Arrow.id """ def __init__(self, dom): Arrow.__init__(self, dom, dom, [], _scan=False) def __repr__(self): return "Id({})".format(repr(self.dom)) def __str__(self): return "Id({})".format(str(self.dom)) from_tree = Arrow.from_tree
[docs]class AxiomError(Exception): """ This is raised whenever we try to build an invalid arrow. """
[docs]@total_ordering class Box(Arrow): """ Defines a box as an arrow with the list of only itself as boxes. Parameters ---------- name : any Name of the box. dom : cat.Ob Domain. cod : cat.Ob Codomain. data : any Extra data in the box, default is `None`. Examples -------- >>> x, y = Ob('x'), Ob('y') >>> f = Box('f', x, y, data=[42]) >>> assert f == Arrow(x, y, [f]) >>> assert f.boxes == [f] >>> assert f[:0] == Id(f.dom) and f[1:] == Id(f.cod) """ def __init__(self, name, dom, cod, **params): def recursive_free_symbols(data): if isinstance(data, np.ndarray): data = data.tolist() if isinstance(data, Mapping): data = data.values() if isinstance(data, Iterable): # Handles numpy 0-d arrays, which are actually not iterable. if not hasattr(data, "shape") or data.shape != (): return set().union(*map(recursive_free_symbols, data)) return data.free_symbols if hasattr(data, "free_symbols") else {} data, _dagger = params.get("data", None), params.get("_dagger", False) self._free_symbols = recursive_free_symbols(data) self._name, self._dom, self._cod = name, dom, cod self._boxes, self._dagger, self._data = [self], _dagger, data Arrow.__init__(self, dom, cod, [self], _scan=False) @property def name(self): """ The name of a box is immutable. >>> f = Box('f', Ob('x'), Ob('y'), data=[42, {0: 1}]) >>> assert f.name == 'f' >>> f.name = 'g' # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: can't set attribute... """ return self._name @property def data(self): """ The attribute `data` is immutable, but it can hold a mutable object. >>> f = Box('f', Ob('x'), Ob('y'), data=[42, {0: 1}]) >>> assert f.data == [42, {0: 1}] >>> f.data = [42, {0: 2}] # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: can't set attribute... >>> f.data[1][0] = 2 >>> assert f.data == [42, {0: 2}] """ return self._data @property def free_symbols(self): return self._free_symbols def subs(self, *args): if not any(var in self.free_symbols for var in ( {var for var, _ in args[0]} if len(args) == 1 else {args[0]})): return self return type(self)( self.name, self.dom, self.cod, _dagger=self._dagger, data=rsubs(self.data, *args)) def lambdify(self, *symbols, **kwargs): if not any(x in self.free_symbols for x in symbols): return lambda *xs: self from sympy import lambdify return lambda *xs: type(self)( self.name, self.dom, self.cod, _dagger=self._dagger, data=lambdify(symbols, self.data, **kwargs)(*xs)) @property def is_dagger(self): """ Whether the box is dagger. """ return self._dagger def dagger(self): return type(self)( name=self.name, dom=self.cod, cod=self.dom, data=self.data, _dagger=not self._dagger) def __getitem__(self, key): if key == slice(None, None, -1): return self.dagger() return super().__getitem__(key) def __repr__(self): if self._dagger: return repr(self.dagger()) + ".dagger()" return "Box({}, {}, {}{})".format( *map(repr, [self.name, self.dom, self.cod]), '' if self.data is None else ", data=" + repr(self.data)) def __str__(self): return str(self.name) + ("[::-1]" if self._dagger else '') def __hash__(self): return hash(super().__repr__()) def __eq__(self, other): if isinstance(other, Box): attributes = ['_name', '_dom', '_cod', '_data', '_dagger'] return all( getattr(self, x) == getattr(other, x) for x in attributes) if isinstance(other, Arrow): return len(other) == 1 and other[0] == self return False def __lt__(self, other): return self.name < other.name def __call__(self, *args, **kwargs): if hasattr(self, "_apply"): return self._apply(self, *args, **kwargs) raise TypeError("Box is not callable, try drawing.diagramize.") def to_tree(self): tree = { 'factory': factory_name(self), 'name': self.name, 'dom': self.dom.to_tree(), 'cod': self.cod.to_tree()} if self.is_dagger: tree['is_dagger'] = True if self.data is not None: tree['data'] = self.data return tree @classmethod def from_tree(cls, tree): name = tree['name'] dom, cod = map(from_tree, (tree['dom'], tree['cod'])) data, _dagger = tree.get('data', None), 'is_dagger' in tree return cls(name=name, dom=dom, cod=cod, data=data, _dagger=_dagger)
[docs]class Sum(Box): """ Implements enrichment over monoids, i.e. formal sums of diagrams. Parameters ---------- terms : list of :class:`Arrow` Terms of the formal sum. dom : :class:`Ob`, optional Domain of the formal sum, optional if :code:`diagrams` is non-empty. cod : :class:`Ob`, optional Codomain of the formal sum, optional if :code:`diagrams` is non-empty. Examples -------- >>> x, y = Ob('x'), Ob('y') >>> f, g = Box('f', x, y), Box('g', x, y) >>> f + g Sum([Box('f', Ob('x'), Ob('y')), Box('g', Ob('x'), Ob('y'))]) >>> unit = Sum([], x, y) >>> assert (f + unit) == Sum([f]) == (unit + f) >>> print((f + g) >> (f + g)[::-1]) (f >> f[::-1]) + (f >> g[::-1]) + (g >> f[::-1]) + (g >> g[::-1]) Note ---- The sum is non-commutative, i.e. :code:`Sum([f, g]) != Sum([g, f])`. A diagram is different from the sum of itself, i.e. :code:`Sum([f]) != f` """ @staticmethod def upgrade(old): return old def __init__(self, terms, dom=None, cod=None): self.terms = list(terms) if not terms: if dom is None or cod is None: raise ValueError(messages.missing_types_for_empty_sum()) else: dom = terms[0].dom if dom is None else dom cod = terms[0].cod if cod is None else cod if (dom, cod) != (terms[0].dom, terms[0].cod): raise AxiomError( messages.cannot_add(Sum([], dom, cod), terms[0])) for arrow in terms: if (arrow.dom, arrow.cod) != (dom, cod): raise AxiomError(messages.cannot_add(terms[0], arrow)) name = "Sum({})".format(repr(terms)) if terms\ else "Sum([], dom={}, cod={})".format(repr(dom), repr(cod)) super().__init__(name, dom, cod) def __eq__(self, other): if not isinstance(other, Sum): return False return (self.dom, self.cod, self.terms)\ == (other.dom, other.cod, other.terms) def __hash__(self): return hash(repr(self)) def __repr__(self): return self.name def __str__(self): if not self.terms: return "Sum([], {}, {})".format(self.dom, self.cod) return " + ".join("({})".format(arrow) for arrow in self.terms) def __add__(self, other): if other == 0: return self other = other if isinstance(other, Sum) else Sum([other]) return self.sum(self.terms + other.terms, self.dom, self.cod) def __radd__(self, other): return self.__add__(other) def __iter__(self): for arrow in self.terms: yield arrow def __len__(self): return len(self.terms) @unbiased def then(self, other): other = other if isinstance(other, Sum) else Sum((other, )) unit = Sum([], self.dom, other.cod) terms = [f.then(g) for f in self.terms for g in other.terms] return self.upgrade(sum(terms, unit)) def dagger(self): unit = Sum([], self.cod, self.dom) return self.upgrade(sum([f.dagger() for f in self.terms], unit)) @property def free_symbols(self): return {x for box in self.terms for x in box.free_symbols} def subs(self, *args): unit = Sum([], self.dom, self.cod) return self.upgrade(sum([f.subs(*args) for f in self.terms], unit)) def lambdify(self, *symbols, **kwargs): return lambda *xs: self.sum( [box.lambdify(*symbols, **kwargs)(*xs) for box in self.terms], dom=self.dom, cod=self.cod) @staticmethod def fmap(func): def sum_func(diagram): return type(diagram)([func(term) for term in diagram.terms]) return sum_func def to_tree(self): return { 'factory': factory_name(self), 'terms': [t.to_tree() for t in self.terms], 'dom': self.dom.to_tree(), 'cod': self.cod.to_tree()} @classmethod def from_tree(cls, tree): dom, cod = map(from_tree, (tree['dom'], tree['cod'])) terms = list(map(from_tree, tree['terms'])) return cls(terms=terms, dom=dom, cod=cod)
[docs]class Bubble(Box): """ A unary operator on homsets. """ def __init__(self, inside, dom=None, cod=None): dom = inside.dom if dom is None else dom cod = inside.cod if cod is None else cod self._inside = inside Box.__init__(self, "Bubble", dom, cod) @property def inside(self): """ The diagram inside a bubble. """ return self._inside def __str__(self): return "({}).bubble({})".format( self.inside, "" if (self.dom, self.cod) == (self.inside.dom, self.inside.cod) else "dom={}, cod={}".format(self.dom, self.cod)) def __repr__(self): return "Bubble({}{})".format( repr(self.inside), "" if (self.dom, self.cod) == (self.inside.dom, self.inside.cod) else ", dom={}, cod={})".format(repr(self.dom), repr(self.cod))) def to_tree(self): return { 'factory': factory_name(self), 'inside': self.inside.to_tree(), 'dom': self.dom.to_tree(), 'cod': self.cod.to_tree()} @classmethod def from_tree(cls, tree): dom, cod, inside = map(from_tree, ( tree['dom'], tree['cod'], tree['inside'])) return cls(dom=dom, cod=cod, inside=inside)
Arrow.sum = Sum Arrow.bubble_factory = Bubble
[docs]class Functor: """ Defines a dagger functor which can be applied to objects and arrows. By default, `Functor` defines an endofunctor from the free dagger category to itself. The codomain can be changed with the optional parameters `ob_factory` and `ar_factory`. Parameters ---------- ob : dict_like Mapping from :class:`cat.Ob` to `ob_factory`. ar : dict_like Mapping from :class:`cat.Box` to `ar_factory`. Other Parameters ---------------- ob_factory : type, optional Class to be used as objects for the codomain of the functor. If None, this will be set to :class:`cat.Ob`. ar_factory : type, optional Class to be used as arrows for the codomain of the functor. If None, this will be set to :class:`cat.Arrow`. Examples -------- >>> x, y, z = Ob('x'), Ob('y'), Ob('z') >>> f, g = Box('f', x, y), Box('g', y, z) >>> ob, ar = {x: y, y: z, z: y}, {f: g, g: g[::-1]} >>> F = Functor(ob, ar) >>> assert F(x) == y and F(f) == g Notes ----- We can check the axioms of dagger functors. >>> assert F(Id(x)) == Id(F(x)) >>> assert F(f >> g) == F(f) >> F(g) >>> assert F(f[::-1]) == F(f)[::-1] >>> assert F(f.dom) == F(f).dom and F(f.cod) == F(f).cod Functors are bubble-preserving. >>> assert F(f.bubble()) == F(f).bubble() See Also -------- Quiver : For functors from infinitely-generated categories, use quivers to create dict-like objects from functions. """ def __init__(self, ob, ar, ob_factory=None, ar_factory=None): if ob_factory is None: ob_factory = Ob if ar_factory is None: ar_factory = Arrow self.ob_factory, self.ar_factory = ob_factory, ar_factory self._ob, self._ar = ob, ar @property def ob(self): """ Mapping on objects. >>> F = Functor({Ob('x'): Ob('y')}, {}) >>> assert F.ob == {Ob('x'): Ob('y')} """ return self._ob if isinstance(self._ob, Mapping) else Quiver(self._ob) @property def ar(self): """ Mapping on arrows. >>> f, g = Box('f', Ob('x'), Ob('y')), Box('g', Ob('y'), Ob('z')) >>> F = Functor({}, {f: g}) >>> assert F.ar == {f: g} """ return self._ar\ if hasattr(self._ar, "__getitem__") else Quiver(self._ar) def __eq__(self, other): return self.ob == other.ob and self.ar == other.ar def __repr__(self): return "Functor(ob={}, ar={})".format(repr(self.ob), repr(self.ar)) def __call__(self, arrow): if isinstance(arrow, Sum): return self.ar_factory.sum( list(map(self, arrow)), self(arrow.dom), self(arrow.cod)) if isinstance(arrow, Bubble): return self(arrow.inside).bubble( dom=self(arrow.dom), cod=self(arrow.cod)) if isinstance(arrow, Ob): return self.ob[arrow] if isinstance(arrow, Box): if arrow.is_dagger: return self.ar[arrow.dagger()].dagger() return self.ar[arrow] if isinstance(arrow, Arrow): return self.ar_factory.id(self(arrow.dom)).then(*map(self, arrow)) raise TypeError(messages.type_err(Arrow, arrow))
class Quiver(Mapping): """ Wraps a function into an immutable dict-like object, used as input for a :class:`Functor`. Parameters ---------- func : callable Any callable Python object. Examples -------- >>> ob, ar = Quiver(lambda x: x), Quiver(lambda f: f) >>> F = Functor(ob, ar) >>> x, y, z = Ob('x'), Ob('y'), Ob('z') >>> f, g = Box('f', x, y), Box('g', y, z) >>> assert F(x) == x and F(f >> g) == f >> g Notes ----- In conjunction with :attr:`Box.data`, this can be used to create a :class:`Functor` from a free category with infinitely many generators. >>> h = Box('h', x, x, data=42) >>> def ar_func(box): ... return Box(box.name, box.dom, box.cod, data=box.data + 1) >>> F = Functor(ob, Quiver(ar_func)) >>> assert F(h).data == 43 and F(F(h)).data == 44 If :attr:`Box.data` is a mutable object, then so can be the image of a :class:`Functor` on it. >>> ar = Quiver(lambda f: f if all(f.data) else f[::-1]) >>> F = Functor(ob, ar) >>> m = Box('m', x, x, data=[True]) >>> assert F(m) == m >>> m.data.append(False) >>> assert F(m) == m[::-1] """ def __init__(self, func): self._func = func def __getitem__(self, box): return self._func(box) def __repr__(self): return "Quiver({})".format(repr(self._func)) def __len__(self): """ >>> dict(Quiver(lambda x: x)) # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: Quivers have no length, you can't iterate them. """ raise TypeError("Quivers have no length, you can't iterate them.") __iter__ = __len__