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(), )
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
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
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
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
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(), )
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)))
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(), )
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(), )
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
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(), )