Source code for haskpy.types.compose

"""Compose two applicative functors into one

.. autosummary::
   :toctree:

   Compose
   decompose

"""

import attr
import functools

from haskpy.typeclasses import Applicative, Eq, map, apply
from haskpy.internal import class_function, immutable
from haskpy.testing import eq_test
from haskpy import testing


[docs]def Compose(X, Y): """Compose two type constructors X and Y into a single type constructor Kind: .. code-block:: Compose :: (* -> *) -> (* -> *) -> * -> * That is, it takes two type constructors and one concrete type to create a concrete type. Another way to see it is that it takes two one-argument type constructors and returns a one-argument type constructor: .. code-block:: Compose :: (* -> *) -> (* -> *) -> (* -> *) That's how we can see it here. Two nested Applicative structures are merged into one layer: .. code-block:: Composed :: f1 (f2 a) -> f f1 f2 a Note that ``f`` is a type constructor of three arguments as shown by its kind above. Some motivation for the current implementation: - Why not just a class that takes a value/object? Why do we need this function wrapper Compose that takes classes as arguments? Because we need to be able to implement the class method ``pure``. Also, this solution would make it possible to decide the parent classes based on the classes of X and Y, so that composed result isn't, for instance, Foldable unless both X and Y are Foldable. - Why the class Composed doesn't have ``__init__`` that passes ``*args`` and ``**kwargs`` to the outer class constructor? It would make it slightly more convenient to create values, right? Not really. We want to be able to transform already existing non-composed values to composed values. For that we need to just take a value, not the outer class constructor arguments. Also, this allows us to decompose and compose in turns without problems. Also, it is more explicit for the user that which are the underlying types because they need to write those when creating values. So, in comparison to Haskell, the function ``Compose`` corresponds to type-level composing and ``Composed`` corresponds to value-level type converter. We need explicit type-level composing here because we need some class methods such as ``pure``. """ class MetaComposed(type(Applicative)): InnerClass = Y OuterClass = X def compress_init(cls): """Create a bit simplified constructor XY = Compose(X, Y) Instead of something like: xy = XY(X(Y(1), Y(2))) You can write use compressed constructor: XYc = XY.compress_init() And then create objects as: xy = XYc(Y(1), Y(2)) Note that the constructor of X is now missing. """ return functools.wraps(X)( lambda *args, **kwargs: cls(X(*args, **kwargs)) ) def __repr__(cls): return "Compose({0}, {1})".format( repr(cls.OuterClass), repr(cls.InnerClass), ) # It's also Foldable and Traversable if both X and Y are. @immutable class Composed(Applicative, Eq, metaclass=MetaComposed): # The attribute name may sound weird but it makes sense once you # understand that this indeed is the not-yet-composed variable and if # you want to decompose a composed variable you get it by x.decomposed. # Thus, there's no need to write a simple function to just return this # attribute, just use this directly. decomposed = attr.ib() @class_function def pure(cls, x): """a -> f a Without composition, this corresponds to: a -> f1 (f2 a) """ return cls(X.pure(Y.pure(x))) def apply(self, f): """f a -> f (a -> b) -> f b Without composition, this corresponds to: f1 (f2 a) -> (f1 (f2 (a -> b))) -> f1 (f2 b) f1 a -> f1 (a -> b) -> f1 b """ # TODO: Check this.. return attr.evolve( self, decomposed=(apply(map(apply, f.decomposed), self.decomposed)) ) def map(self, f): """(a -> b) -> f a -> f b Without composition, this corresponds to: map . map :: (a -> b) -> f1 (f2 a) -> f1 (f2 b) """ # This implementation isn't necessary because Applicative has a # default implementation. But let's just provide this simple # implementation for efficiency. return attr.evolve( self, decomposed=(map(map(f))(self.decomposed)) ) def decompose(self): return self.decomposed def __repr__(self): return "{0}({1})".format( repr(self.__class__), repr(self.decomposed), ) def __eq__(self, other): return self.decomposed == other.decomposed def __eq_test__(self, other, data): return eq_test(self.decomposed, other.decomposed, data) @class_function def sample_value(cls, a): return X.sample_value(Y.sample_value(a)).map(cls) sample_type = testing.sample_type_from_value( testing.sample_type(), ) sample_functor_type = testing.sample_type_from_value() sample_applicative_type = sample_functor_type sample_monad_type = sample_functor_type sample_eq_type = testing.sample_type_from_value( testing.sample_eq_type(), ) return Composed
[docs]def decompose(x): return x.decomposed