Ejemplo n.º 1
0
class Identity(Monad, Eq):

    x = attr.ib()

    @class_function
    def pure(cls, x):
        return cls(x)

    def bind(self, f):
        """Identity a -> (a -> Identity b) -> Identity b"""
        return f(self.x)

    def __eq__(self, other):
        """Identity a -> Identity a -> bool"""
        return self.x == other.x

    def __repr__(self):
        return "Identity({})".format(repr(self.x))

    def __eq_test__(self, other, data):
        return eq_test(self.x, other.x, data)

    @class_function
    def sample_value(cls, a):
        return a.map(Identity)

    sample_type = testing.create_type_sampler(testing.sample_type(), )

    sample_functor_type_constructor = testing.create_type_constructor_sampler()

    # Some typeclass instances have constraints for the types inside

    sample_eq_type = testing.create_type_sampler(testing.sample_eq_type(), )
Ejemplo n.º 2
0
class All(Commutative, Monoid, Hashable):
    """All monoid"""

    boolean = attr.ib()

    @class_property
    def empty(cls):
        return cls(True)

    def append(self, x):
        return All(self.boolean and x.boolean)

    def __eq__(self, other):
        return self.boolean == other.boolean

    def __hash__(self):
        return hash(self.boolean)

    def __repr__(self):
        return "All({})".format(repr(self.boolean))

    @class_function
    def sample_value(cls):
        return st.booleans().map(All)

    sample_type = testing.create_type_sampler()
    sample_eq_type = sample_type
    sample_semigroup_type = sample_type
    sample_commutative_type = sample_type
    sample_monoid_type = sample_type
    sample_hashable_type = sample_type
Ejemplo n.º 3
0
class Sum(Commutative, Monoid, Hashable):
    """Sum monoid"""

    number = attr.ib()

    @class_property
    def empty(cls):
        return cls(0)

    def append(self, x):
        return Sum(self.number + x.number)

    def __eq__(self, other):
        return self.number == other.number

    def __hash__(self):
        return hash(self.number)

    def __repr__(self):
        return "Sum({})".format(repr(self.number))

    @class_function
    def sample_value(cls):
        return st.integers().map(Sum)

    sample_type = testing.create_type_sampler()
    sample_eq_type = sample_type
    sample_semigroup_type = sample_type
    sample_commutative_type = sample_type
    sample_monoid_type = sample_type
    sample_hashable_type = sample_type
Ejemplo n.º 4
0
class Endo(Monoid):
    """Endofunction monoid (a -> a)"""

    app_endo = attr.ib()

    @class_property
    def empty(cls):
        return cls(identity)

    def append(self, f):
        # Append by composing
        return Endo(lambda a: self.app_endo(f.app_endo(a)))

    def __repr__(self):
        return "Endo({})".format(self.app_endo)

    def __eq_test__(self, other, data, input_strategy=st.integers()):
        x = data.draw(input_strategy)
        return eq_test(self.app_endo(x), other.app_endo(x), data)

    @class_function
    def sample_value(cls, a):
        return testing.sample_function(a).map(lambda f: Endo(f))

    sample_type = testing.create_type_sampler(
        testing.sample_eq_type(),
    )
    sample_semigroup_type = sample_type
    sample_monoid_type = sample_type
Ejemplo n.º 5
0
class String(Monoid, Hashable):
    """String monoid"""

    string = attr.ib(converter=str)

    @class_property
    def empty(cls):
        return cls("")

    def append(self, s):
        return String(self.string + s.string)

    def __eq__(self, other):
        return self.string == other.string

    def __hash__(self):
        return hash(self.string)

    def __str__(self):
        return self.string

    def __repr__(self):
        return "String({})".format(repr(self.string))

    @class_function
    def sample_value(cls):
        return st.text().map(lambda s: String(s))

    sample_type = testing.create_type_sampler()
    sample_eq_type = sample_type
    sample_semigroup_type = sample_type
    sample_monoid_type = sample_type
    sample_hashable_type = sample_type
Ejemplo n.º 6
0
class Dictionary(Apply, Eq, Monoid, Traversable):
    """Dictionary type

    .. todo::

        For method ideas, refer to:
        `<https://pursuit.purescript.org/packages/purescript-unordered-collections/0.2.0/docs/Data.HashMap>`_

    """

    __dict = attr.ib()

    def __init__(self, *args, **kwargs):
        object.__setattr__(self, "_Dictionary__dict", dict(*args, **kwargs))
        return

    @class_property
    def empty(cls):
        """Empty dictionary"""
        return cls()

    def append(self, other):
        """Combine two dictionaries

        ::

            Semigroup a => Dictionary a -> Dictionary a -> Dictionary a

        .. note::

            If a key is in both dictionaries, the values are expected to be
            semigroup instances, so that they can be combined. Alternative
            solutions would be to prefer either the first or the second
            dictionary value as done in Haskell and PureScript, so that there
            would be no need to constrain the contained type to be an instance
            of Semigroup. This class provides separate methods
            :py:meth:`.Dictionary.append_first` and
            :py:meth:`.Dictionary.append_second` for those purposes.

        """
        self_keys = set(self.__dict.keys())
        other_keys = set(other.__dict.keys())

        self_only_keys = self_keys.difference(other_keys)
        other_only_keys = other_keys.difference(self_keys)
        both_keys = self_keys.intersection(other_keys)

        return Dictionary({
            key: (self.__dict[key] if c == 1 else other.__dict[key]
                  if c == 2 else self.__dict[key].append(other.__dict[key]))
            for (c, key) in itertools.chain(
                zip(itertools.repeat(1), self_only_keys),
                zip(itertools.repeat(2), other_only_keys),
                zip(itertools.repeat(3), both_keys),
            )
        })

    def append_first(self, other):
        """Combine two dictionaries preferring the elements of the first"""
        def get(key):
            try:
                return self.__dict[key]
            except KeyError:
                return other.__dict[key]

        return Dictionary({
            key: get(key)
            for key in set(self.__dict.keys()).union(set(other.__dict.keys()))
        })

    def append_second(self, other):
        """Combine two dictionaries preferring the elements of the second"""
        def get(key):
            try:
                return other.__dict[key]
            except KeyError:
                return self.__dict[key]

        return Dictionary({
            key: get(key)
            for key in set(self.__dict.keys()).union(set(other.__dict.keys()))
        })

    def map(self, f):
        """Apply a function to each value in the dictionary

        ::

            Dictionary k a -> (a -> b) -> Dictionary k b

        """
        return Dictionary(
            {key: f(value)
             for (key, value) in self.__dict.items()})

    def apply(self, f):
        """Apply a dictionary of functions to a dictionary of values

        ::

            Dictionary k a -> Dictionary k (a -> b) -> Dictionary k b

        .. note::

            The resulting dictionary will have only such keys that are in both
            of the input dictionaries.

        """
        self_keys = set(self.__dict.keys())
        f_keys = set(f.__dict.keys())
        keys = self_keys.intersection(f_keys)
        return Dictionary(
            {key: f.__dict[key](self.__dict[key])
             for key in keys})

    # def bind(self, f):
    #     """Dictionary k a -> (a -> Dictionary k b) -> Dictionary k b

    #     When joining values for identical keys, the first (left) dictionary
    #     value is preferred. However, note that it can be random in which order
    #     the dictionaries are joined.

    #     """
    #     return self.map(f).foldl(
    #         append_first,
    #         Dictionary()
    #     )

    def fold_map(self, monoid, f):
        """Monoid m => Dictionary k a -> Monoid -> (a -> m) -> m"""
        xs = builtins.map(f, self.__dict.values())
        return functools.reduce(
            monoid.append,
            xs,
            monoid.empty,
        )

    def foldl(self, combine, initial):
        """Dictionary k a -> (b -> a -> b) -> b -> b"""
        return functools.reduce(
            lambda acc, x: combine(acc)(x),
            self.__dict.values(),
            initial,
        )

    def foldr(self, combine, initial):
        """Dictionary k a -> (a -> b -> b) -> b -> b"""
        return functools.reduce(
            lambda acc, x: combine(x)(acc),
            reversed(self.__dict.values()),
            initial,
        )

    def foldl_with_index(self, combine, initial):
        """Dictionary k a -> (k -> b -> a -> b) -> b -> b"""
        return functools.reduce(
            lambda acc, key_value: combine(key_value[0])(acc)(key_value[1]),
            self.__dict.items(),
            initial,
        )

    def sequence(self, applicative):
        """Applicative f => Dictionary k (f a) -> f (Dictionary k a)"""
        return self.foldl_with_index(
            lambda key: lift2(lambda xs: lambda x: xs.append(
                Dictionary({key: x}))),
            applicative.pure(Dictionary()),
        )

    def lookup(self, key):
        try:
            x = self.__dict[key]
        except KeyError:
            return Nothing
        else:
            return Just(x)

    @class_function
    def singleton(cls, key, value):
        return cls({key: value})

    def insert(self, key, value):
        raise NotImplementedError()

    def delete(self, key):
        raise NotImplementedError()

    def update(self, f, key):
        raise NotImplementedError()

    def alter(self, f, key):
        raise NotImplementedError()

    def keys(self):
        raise NotImplementedError()

    def values(self):
        raise NotImplementedError()

    def __getitem__(self, key):
        return self.lookup(key)

    def __repr__(self):
        return "Dictionary({})".format(repr(self.__dict))

    def __eq__(self, other):
        return self.__dict == other.__dict

    def __eq_test__(self, other, data=None):
        return (self.__dict.keys() == other.__dict.keys() and all(
            testing.eq_test(self.__dict[key], other.__dict[key], data)
            for key in self.__dict.keys()))

    @class_function
    def sample_value(cls, k, a):
        return st.dictionaries(k, a, max_size=3).map(Dictionary)

    sample_type = testing.create_type_sampler(
        testing.sample_hashable_type(),
        testing.sample_type(),
    )

    sample_functor_type_constructor = testing.create_type_constructor_sampler(
        testing.sample_hashable_type(), )

    sample_foldable_type_constructor = sample_functor_type_constructor

    sample_eq_type = testing.create_type_sampler(
        testing.sample_hashable_type(),
        testing.sample_eq_type(),
    )

    sample_semigroup_type = testing.create_type_sampler(
        testing.sample_hashable_type(),
        testing.sample_semigroup_type(),
    )
Ejemplo n.º 7
0
class List(Monad, Monoid, Traversable, Eq):

    __xs = attr.ib(converter=tuple)

    def __init__(self, *xs):
        object.__setattr__(self, "_List__xs", tuple(xs))
        return

    def map(self, f):
        """List a -> (a -> b) -> List b"""
        return List(*(f(x) for x in self.__xs))

    @class_function
    def pure(cls, x):
        """a -> List a"""
        return cls(x)

    def apply(self, fs):
        """List a -> List (a -> b) -> List b"""
        return List(*(y for f in fs.__xs for y in self.map(f).__xs))

    def bind(self, f):
        """List a -> (a -> List b) -> List b"""
        return List(*(y for ys in self.map(f).__xs for y in ys.__xs))

    def __eq__(self, other):
        """List a -> List a -> bool"""
        return self.__xs == other.__xs

    @class_property
    def empty(cls):
        """Empty list, type ``List a``"""
        return cls()

    def append(self, xs):
        """List a -> List a -> List a"""
        return List(*self.__xs, *xs.__xs)

    def to_iter(self):
        yield from self.__xs

    @class_function
    def from_iter(cls, xs):
        """Iterable f => f a -> List a"""
        return cls(*xs)

    def length(self):
        return len(self.__xs)

    def elem(self, e):
        return e in self.__xs

    def last(self):
        try:
            return Just(self.__xs[-1])
        except IndexError:
            return Nothing

    def foldl(self, combine, initial):
        """List a -> (b -> a -> b) -> b -> b

        ``combine`` function is assumed to be curried

        """
        # TODO: We could implement also fold_map to make fold_map and fold to
        # use parallelized implementation because they use monoids. Now, the
        # default implementations use foldl/foldr which both are sequential.
        return functools.reduce(
            lambda a, b: combine(a)(b),
            self.__xs,
            initial,
        )

    def foldr(self, combine, initial):
        """List a -> (a -> b -> b) -> b -> b

        ``combine`` function is assumed to be curried

        """
        # TODO: We could implement also fold_map to make fold_map and fold to
        # use parallelized implementation because they use monoids. Now, the
        # default implementations use foldl/foldr which both are sequential.
        return functools.reduce(
            lambda b, a: combine(a)(b),
            self.__xs[::-1],
            initial,
        )

    def sequence(self, applicative):
        """Applicative f => List (f a) -> f (List a)"""
        return self.foldl(
            lift2(lambda xs: lambda x: List(*xs.__xs, x)),
            applicative.pure(List()),
        )

    def __repr__(self):
        return "List{}".format(repr(self.__xs))

    #
    # Sampling methods for property tests
    #

    @class_function
    def sample_value(cls, a):
        # Use very short lists because otherwise some tests like Traversable
        # sequence/traverse might explode to huuuuge computations.
        return st.lists(a, max_size=3).map(lambda xs: cls(*xs))

    sample_type = testing.create_type_sampler(testing.sample_type(), )

    sample_functor_type_constructor = testing.create_type_constructor_sampler()
    sample_apply_type_constructor = sample_functor_type_constructor
    sample_applicative_type_constructor = sample_functor_type_constructor
    sample_monad_type_constructor = sample_functor_type_constructor

    sample_semigroup_type = testing.create_type_sampler(
        testing.sample_type(), )
    sample_monoid_type = sample_semigroup_type

    sample_eq_type = testing.create_type_sampler(testing.sample_eq_type(), )

    sample_foldable_type_constructor = testing.create_type_constructor_sampler(
    )
    sample_foldable_functor_type_constructor = sample_foldable_type_constructor

    def __eq_test__(self, other, data=None):
        return (False if len(self.__xs) != len(other.__xs) else all(
            eq_test(x, y, data) for (x, y) in zip(self.__xs, other.__xs)))
Ejemplo n.º 8
0
class Either(Monad, Eq):

    match = attr.ib()

    @class_function
    def pure(cls, x):
        return Right(x)

    def map(self, f):
        return self.match(
            Left=lambda _: self,
            Right=lambda x: Right(f(x)),
        )

    def apply_to(self, x):
        return self.match(
            Left=lambda _: self,
            Right=lambda f: x.map(f),
        )

    def bind(self, f):
        return self.match(
            Left=lambda _: self,
            Right=lambda x: f(x),
        )

    def __eq__(self, other):
        return self.match(
            Left=lambda x: other.match(
                Left=lambda y: x == y,
                Right=lambda _: False,
            ),
            Right=lambda x: other.match(
                Left=lambda _: False,
                Right=lambda y: x == y,
            ),
        )

    def __eq_test__(self, other, data):
        return self.match(
            Left=lambda x: other.match(
                Left=lambda y: eq_test(x, y, data=data),
                Right=lambda _: False,
            ),
            Right=lambda x: other.match(
                Left=lambda _: False,
                Right=lambda y: eq_test(x, y, data=data),
            ),
        )

    def __repr__(self):
        return self.match(
            Left=lambda x: "Left({0})".format(repr(x)),
            Right=lambda x: "Right({0})".format(repr(x)),
        )

    @class_function
    def sample_value(cls, a, b):
        return st.one_of(a.map(Left), b.map(Right))

    sample_type = testing.create_type_sampler(
        testing.sample_type(),
        testing.sample_type(),
    )

    sample_functor_type_constructor = testing.create_type_constructor_sampler(
        testing.sample_type(), )

    # Some typeclass instances have constraints for the types inside

    sample_eq_type = testing.create_type_sampler(
        testing.sample_eq_type(),
        testing.sample_eq_type(),
    )
Ejemplo n.º 9
0
class Maybe(
        Monad,
        Commutative,
        Monoid,
        Hashable,
        Traversable,
):
    """Type ``Maybe a`` for a value that might be present or not

    :py:class:`.Maybe` monad is one of the simplest yet very useful monads. It
    represents a case when you might have a value or not. If you have a value,
    it's wrapped with :py:func:`.Just`:

    .. code-block:: python

    >>> x = hp.Just(42)
    >>> x
    Just(42)
    >>> type(x)
    Maybe

    If you don't have a value, it's represented with :py:data:`.Nothing`:

    .. code-block:: python

    >>> y = hp.Nothing
    >>> y
    Nothing
    >>> type(y)
    Maybe

    Quite often Python programmers handle this case by using ``None`` to
    represent "no value" and then just the plain value otherwise. However,
    whenever you want to do something with the value, you need to first check
    if it's ``None`` or not, and handle both cases somehow. And, more
    importantly, you need to remember to do this every time you use a value
    that might be ``None``.

    With HaskPy, it is explicit that the value might exist or not, so you are
    forced to handle both cases. Or, more interestingly, you can just focus on
    the value and let HaskPy take care of the special case. Let's see what this
    means. Say you have a function that you'd like to apply to the value:

    .. code-block:: python

    >>> f = lambda v: v + 1

    You can use :py:meth:`.Functor.map` method to apply it to the value:

    .. code-block:: python

    >>> x.map(f)
    Just(43)

    Or, equivalently, use a function:

    .. code-block:: python

    >>> hp.map(f, x)
    Just(43)

    Quite often there are corresponding functions for the methods and it may
    depend on the context which one is more convenient to use. The order of the
    arguments might be slightly different in the function than in the method
    though.

    But what would've happened if we had ``Nothing`` instead?

    .. code-block:: python

    >>> hp.map(f, y)
    Nothing

    So, nothing was done to ``Nothing``. But the important thing is that you
    didn't need to worry about whether ``x`` or ``y`` was ``Nothing`` or
    contained a real value, :py:class:`Maybe` took care of that under the hood.
    If in some cases you need to handle both cases explicitly, you can use
    :py:func:`.match` function:

    .. code-block:: python

    >>> g = hp.match(Just=lambda v: 2*v, Nothing=lambda: 666)
    >>> g(x)
    84
    >>> g(y)
    666

    With :py:func:`.match` you need to explicitly handle all possible cases or
    you will get an error even if your variable wasn't ``Nothing``. Therefore,
    you'll never forget to take into account ``Nothing`` case as might happen
    with the classic ``None`` approach.

    Alright, this was just a very tiny starter about :py:class:`.Maybe`.

    See also
    --------

    haskpy.types.either.Either

    """

    match = attr.ib()

    @class_property
    def empty(cls):
        return Nothing

    @class_function
    def pure(cls, x):
        return Just(x)

    def map(self, f):
        return self.match(
            Nothing=lambda: Nothing,
            Just=lambda x: Just(f(x)),
        )

    def apply_to(self, x):
        return self.match(
            Nothing=lambda: Nothing,
            Just=lambda f: x.map(f),
        )

    def bind(self, f):
        return self.match(
            Nothing=lambda: Nothing,
            Just=lambda x: f(x),
        )

    def append(self, m):
        return self.match(
            Nothing=lambda: m,
            Just=lambda x: m.match(
                Nothing=lambda: self,
                Just=lambda y: Just(x.append(y)),
            ),
        )

    def fold_map(self, monoid, f):
        return self.match(
            Nothing=lambda: monoid.empty,
            Just=lambda x: f(x),
        )

    def foldl(self, combine, initial):
        return self.match(
            Nothing=lambda: initial,
            Just=lambda x: combine(initial)(x),
        )

    def foldr(self, combine, initial):
        return self.match(
            Nothing=lambda: initial,
            Just=lambda x: combine(x)(initial),
        )

    def length(self):
        return self.match(
            Nothing=lambda: 0,
            Just=lambda _: 1,
        )

    def to_iter(self):
        yield from self.match(
            Nothing=lambda: (),
            Just=lambda x: (x, ),
        )

    def sequence(self, applicative):
        return self.match(
            Nothing=lambda: applicative.pure(Nothing),
            # Instead of x.map(Just), access the method via the class so that
            # if the given applicative class is inconsistent with the contained
            # value an error would be raised. This helps making sure that if
            # sequence works for case Just, it'll be consistent with case
            # Nothing. "If it runs, it probably works."
            Just=lambda x: applicative.map(x, Just),
        )

    def __repr__(self):
        return self.match(
            Nothing=lambda: "Nothing",
            Just=lambda x: "Just({0})".format(repr(x)),
        )

    def __hash__(self):
        return self.match(
            # Use some random strings in hashing
            Nothing=lambda: hash("hfauwovnohuwehrofasdlnvlspwfoij"),
            Just=lambda x: hash(("fwaoivaoiejfaowiefijafsduhasdo", x)))

    def __eq__(self, other):
        return self.match(
            Nothing=lambda: other.match(
                Nothing=lambda: True,
                Just=lambda _: False,
            ),
            Just=lambda x: other.match(
                Nothing=lambda: False,
                Just=lambda y: x == y,
            ),
        )

    def __eq_test__(self, other, data):
        return self.match(
            Nothing=lambda: other.match(
                Nothing=lambda: True,
                Just=lambda _: False,
            ),
            Just=lambda x: other.match(
                Nothing=lambda: False,
                Just=lambda y: eq_test(x, y, data),
            ),
        )

    #
    # Sampling methods for property tests
    #

    @class_function
    def sample_value(cls, a):
        return st.one_of(st.just(Nothing), a.map(Just))

    sample_type = testing.create_type_sampler(testing.sample_type(), )

    sample_functor_type_constructor = testing.create_type_constructor_sampler()
    sample_foldable_type_constructor = testing.create_type_constructor_sampler(
    )

    # Some typeclass instances have constraints for the type inside Maybe

    sample_hashable_type = testing.create_type_sampler(
        testing.sample_hashable_type(), )

    sample_semigroup_type = testing.create_type_sampler(
        testing.sample_semigroup_type(), )

    sample_commutative_type = testing.create_type_sampler(
        testing.sample_commutative_type(), )

    sample_eq_type = testing.create_type_sampler(testing.sample_eq_type(), )
Ejemplo n.º 10
0
class LinkedList(Monad, Monoid, Foldable, Eq):
    """Linked-list with "lazy" Cons

    The "lazy" Cons makes it possible to construct infinite lists. For
    instance, an infinite list of a repeated value 42 can be constructed as:

    .. code-block:: python

        >>> repeat(42)
        Cons(42, Cons(42, Cons(42, Cons(42, Cons(42, Cons(42, ...))))))

    You can use, for instance, ``scanl`` and ``map`` to create more complex
    infinite lists from a simple one:

    .. code-block:: python

        >>> xs = repeat(1).scanl(lambda acc, x: acc + x)
        >>> xs
        Cons(1, Cons(2, Cons(3, Cons(4, Cons(5, Cons(6, ...))))))
        >>> xs.map(lambda x: x ** 2)
        Cons(1, Cons(4, Cons(9, Cons(16, Cons(25, Cons(36, ...))))))

    Note that this works also for very long lists:

    .. code-block:: python

        >>> xs.drop(10000)
        Cons(10001, Cons(10002, Cons(10003, Cons(10004, Cons(10005, Cons(10006, ...))))))

    One can create infinite lists by using a recursive definition:

    .. code-block:: python

        >>> xs = Cons(42, lambda: xs)

    But beware that this kind of recursive definition doesn't always work as
    one might expect. For instance, the following construction causes huge
    recursion depths:

    .. code-block:: python

        >>> xs = Cons(1, lambda: xs.map(lambda y: y + 1))
        >>> xs
        Cons(1, Cons(2, Cons(3, Cons(4, Cons(5, Nil)))))
        >>> xs.drop(10000)
        RecursionError: maximum recursion depth exceeded while calling a Python object

    This happens because each value depends recursively on all the previous
    values

    """

    match = attr.ib()

    @class_property
    def empty(cls):
        return Nil

    @class_function
    def pure(cls, x):
        """a -> LinkedList a"""
        return Cons(x, lambda: Nil)

    def map(self, f):
        """List a -> (a -> b) -> List b"""
        return self.match(
            Nil=lambda: Nil,
            Cons=lambda x, xs: Cons(f(x), lambda: xs().map(f)),
        )

    def bind(self, f):
        """List a -> (a -> List b) -> List b"""

        def append_lazy(xs, lys):
            """LinkedList a -> (() -> LinkedList a) -> LinkedList a

            Append two linked lists. This function is "lazy" in its second
            argument, that is, ``lys`` is a lambda function that returns the
            linked list.

            """
            return xs.match(
                Nil=lambda: lys(),
                Cons=lambda x, lxs: Cons(x, lambda: append_lazy(lxs(), lys))
            )

        return self.match(
            Nil=lambda: Nil,
            Cons=lambda x, lxs: append_lazy(f(x), lambda: lxs().bind(f)),
        )

    def append(self, ys):
        """LinkedList a -> LinkedList a -> LinkedList a"""
        return self.match(
            Nil=lambda: ys,
            Cons=lambda x, lxs: Cons(x, lambda: lxs().append(ys))
        )

    def take(self, n):
        # Note that here we can use a recursive definition because the
        # recursion stops at lambda, so the list is consumed lazily.
        return self.match(
            Nil=lambda: Nil,
            Cons=lambda x, xs: (
                Nil if n <= 0 else
                Cons(x, lambda: xs().take(n-1))
            ),
        )

    def drop(self, n):
        # This is the trivial recursive implementation:
        #
        # return self.match(
        #     Nil=lambda: Nil,
        #     Cons=lambda x, xs: (
        #         Cons(x, xs) if n <= 0 else
        #         xs().drop(n-1)
        #     ),
        # )
        #
        # However, recursion causes stack overflow for long lists with large n.
        # So, let's use a loop:
        xs = self
        for i in range(n):
            (exit, xs) = xs.match(
                Nil=lambda: (True, Nil),
                Cons=lambda z, zs: (False, zs())
            )
            if exit:
                break
        return xs

    def __eq__(self, other):
        """LinkedList a -> LinkedList a -> bool"""
        # Here's a nice recursive solution:
        #
        # return self.match(
        #     Nil=lambda: other.match(
        #         Nil=lambda: True,
        #         Cons=lambda _, __: False,
        #     ),
        #     Cons=lambda x, xs: other.match(
        #         Nil=lambda: False,
        #         Cons=lambda y, ys: (x == y) and (xs() == ys()),
        #     ),
        # )
        #
        # However, it doesn't work because of Python has bad recursion support.
        # So, let's use recurse_tco which converts recursion to a loop:
        return self.recurse_tco(
            lambda acc, x: (
                acc.match(
                    # self is longer than other
                    Nil=lambda: Left(False),
                    Cons=lambda y, lys: (
                        # Elements don't match
                        Left(False) if x != y else
                        # All good thus far, continue
                        Right(lys())
                    )
                )
            ),
            lambda acc: acc.match(
                # Both lists are empty (or end at the same time)
                Nil=lambda: True,
                # other is longer than self
                Cons=lambda _, __: False,
            ),
            other,
        )

    def to_iter(self):
        lxs = lambda: self
        while True:
            (stop, x, lxs) = lxs().match(
                Nil=lambda: (True, None, None),
                Cons=lambda z, lzs: (False, z, lzs),
            )
            if stop:
                break
            yield x

    def recurse_tco(self, f, g, acc):
        """Recursion with tail-call optimization

        Type signature:

        ``LinkedList a -> (b -> a -> Either c b) -> (b -> c) -> b -> c``

        where ``a`` is the type of the elements in the linked list, ``b`` is
        the type of the accumulated value and ``c`` is the type of the result.
        Quite often, the accumulated value is also the end result, so ``b`` is
        ``c`` and ``g`` is an identity function.

        As Python supports recursion very badly, some typical recursion
        patterns are implemented as methods that convert specific recursions to
        efficients loops. This method implements the following pattern:

        .. code-block:: python

            >>> return self.match(
            ...     Nil=lambda: g(acc),
            ...     Cons=lambda x, lxs: f(acc, x).match(
            ...         Left=lambda y: y,
            ...         Right=lambda y: lxs().recurse_tco(f, g, y)
            ...     )
            ... )

        This recursion method supports short-circuiting and simple tail-call
        optimization. A value inside ``Left`` stops the recursion and returns
        the value. A value inside ``Right`` continues the recursion with the
        updated accumulated value.

        Examples
        --------

        For instance, the following recursion calculates the sum of the list
        elements until the sum exceeds one million:

        .. code-block:: python

            >>> from haskpy import Left, Right, iterate
            >>> xs = iterate(lambda x: x + 1, 1)
            >>> my_sum = lambda xs, acc: xs.match(
            ...     Nil=lambda: acc,
            ...     Cons=lambda y, ys: acc if acc > 1000000 else my_sum(xs, acc + y)
            ... )
            >>> my_sum(xs, 0)

        Unfortunately, this recursion exceeds Python maximum recursion depth
        because 1000000 is a large enough number. Note that this cannot be
        implemented with ``foldl`` because it doesn't support short-circuiting.
        Also, ``foldr`` doesn't work because it's right-associative so it
        cannot short-circuit based on the accumulator. But it can be calculated
        with this ``recurse_tco`` method which converts the recursion into a
        loop internally:

        .. code-block:: python

            >>> xs.recurse_tco(
            ...     lambda acc, x: Left(acc) if acc > 1000000 else Right(acc + x),
            ...     lambda acc: acc,
            ...     0
            ... )

        See also
        --------

        foldl
        foldr
        foldr_lazy

        """
        stop = False
        xs = self
        while not stop:
            (stop, acc, xs) = xs.match(
                Nil=lambda: (True, g(acc), Nil),
                Cons=lambda y, lys: f(acc, y).match(
                    Left=lambda z: (True, z, Nil),
                    Right=lambda z: (False, z, lys())
                )
            )
        return acc

    def foldl(self, combine, initial):
        """Foldable t => t a -> (b -> a -> b) -> b -> b

        Strict left-associative fold

        ((((a + b) + c) + d) + e)

        """
        # NOTE: The following simple recursive implementation doesn't work
        # because it can exceed Python maximum recursion depth:
        #
        # return self.match(
        #     Nil=lambda: initial,
        #     Cons=lambda x, xs: xs().foldl(combine, combine(initial, x)),
        # )
        #
        # So, let's use a for-loop based solution instead:
        return functools.reduce(lambda a, b: combine(a)(b), self, initial)

    def foldr(self, combine, initial):
        """Foldable t => t a -> (a -> b -> b) -> b -> b

        Strict right-associative fold. Note that this doesn't work for infinite
        lists because it's strict. You probably want to use ``foldr_lazy`` or
        ``foldl`` instead as this function easily exceeds Python maximum
        recursion depth (or the stack overflows).

        ..code-block:: python

            >>> xs = iterate(lambda x: x + 1, 1)
            >>> xs.foldr(lambda y, ys: Cons(2 * y, lambda: ys), Nil)
            RecursionError: maximum recursion depth exceeded while calling a Python object

        """
        return self.match(
            Nil=lambda: initial,
            Cons=lambda x, xs: combine(x)(xs().foldr(combine, initial))
        )

    def foldr_lazy(self, combine, initial):
        r"""Foldable t => t a -> (a -> (() -> b) -> (() -> b)) -> b -> b

        Nonstrict right-associative fold with support for lazy recursion,
        short-circuiting and tail-call optimization.

        HOW ABOUT [a,b,c,d,e,f,g,h,...] -> (a(b(c(d(e))))) UNTIL TOTAL STRING
        LENGTH IS X?

        Parameters
        ----------

        combine : curried function

        See also
        --------

        haskpy.typeclasses.foldable.foldr_lazy

        """

        def step(x, lxs):
            """A single recursion step

            Utilizes tail-call optimization if used.

            """
            lacc_prev = lambda: run(lxs())
            lacc_next = combine(x)(lacc_prev)

            # Special case: Tail call optimization! If the lazy accumulator
            # stays unmodified, we can just iterate as long as it's not
            # modified.
            while lacc_next is lacc_prev:
                (lxs, lacc_next) = lxs().match(
                    Nil=lambda: (Nil, lambda: initial),
                    Cons=lambda z, lzs: (lzs, combine(z)(lacc_next)),
                )

            # Just return and let the normal recursion roll
            return lacc_next()

        def run(xs):
            """Run the recursion

            This wouldn't need to be wrapped in a separate function as we could
            call foldr_lazy recursively. However, as we explicitly curry
            combine-function, let's avoid doing that at every step.

            """
            return xs.match(
                Nil=lambda: initial,
                Cons=step,
            )

        return run(self)

    def scanl(self, f):
        return self.match(
            Nil=lambda: Nil,
            Cons=lambda x, xs: Cons(x, lambda: xs()._scanl(f, x)),
        )

    def _scanl(self, f, acc):
        def create_cons(x, xs):
            z = f(acc, x)
            return Cons(z, lambda: xs()._scanl(f, z))
        return self.match(
            Nil=lambda: Nil,
            Cons=create_cons,
        )

    def __repr__(self):
        return self.__repr()

    def __repr(self, maxdepth=5):
        return self.match(
            Nil=lambda: "Nil",
            Cons=lambda x, xs: "Cons({0}, {1})".format(
                repr(x),
                "..." if maxdepth <= 0 else xs().__repr(maxdepth-1),
            ),
        )

    #
    # Sampling methods for property tests
    #

    # @class_function
    # @st.composite
    # def sample_value(draw, cls, a):
    #     return draw(
    #         st.one_of(
    #             st.just(Nil),
    #             a.map(
    #                 lambda x: Cons(
    #                     x,
    #                     lambda: draw(cls.sample_value(a))
    #                 )
    #             )
    #         )
    #     )
    #     #return st.lists(a, max_size=10).map(lambda xs: cls(*xs))

    @class_function
    def sample_value(cls, a, max_depth=3):
        # It's not possible to sample linked lists lazily because hypothesis
        # doesn't support that sampling happens at some later point (the
        # sampler gets "frozen"). So, we must sample everything at once,
        # although we then add the "lazy" lambda wrapping to the pre-sampled
        # values.
        #
        # This non-lazy sampling could be implemented recursively as follows:
        #
        return (
            st.just(Nil) if max_depth <= 0 else
            st.deferred(
                lambda: st.one_of(
                    st.just(Nil),
                    a.flatmap(
                        lambda x: cls.sample_value(a, max_depth=max_depth-1).map(
                            lambda xs: Cons(x, lambda: xs)
                        )
                    )
                )
            )
        )
        #
        # However, this can cause RecursionError in Python, so let's write it
        # as a loop instead:

    sample_type = testing.create_type_sampler(
        testing.sample_type(),
    )

    sample_functor_type_constructor = testing.create_type_constructor_sampler()
    sample_apply_type_constructor = sample_functor_type_constructor
    sample_applicative_type_constructor = sample_functor_type_constructor
    sample_monad_type_constructor = sample_functor_type_constructor

    sample_semigroup_type = testing.create_type_sampler(
        testing.sample_type(),
    )
    sample_monoid_type = sample_semigroup_type

    sample_eq_type = testing.create_type_sampler(
        testing.sample_eq_type(),
    )

    def __eq_test__(self, other, data=None):
        return self.match(
            Nil=lambda: other.match(
                Nil=lambda: True,
                Cons=lambda _, __: False,
            ),
            Cons=lambda x, lxs: other.match(
                Nil=lambda: False,
                Cons=lambda y, lys: (
                    eq_test(x, y, data) and
                    eq_test(lxs(), lys(), data)
                ),
            ),
        )

    sample_foldable_type_constructor = testing.create_type_constructor_sampler()
    sample_foldable_functor_type_constructor = sample_foldable_type_constructor
Ejemplo n.º 11
0
    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.create_type_sampler(testing.sample_type(), )

        sample_functor_type_constructor = testing.create_type_constructor_sampler(
        )
        sample_apply_type_constructor = sample_functor_type_constructor
        sample_applicative_type_constructor = sample_functor_type_constructor
        sample_monad_type_constructor = sample_functor_type_constructor

        sample_eq_type = testing.create_type_sampler(
            testing.sample_eq_type(), )