Ejemplo n.º 1
0
    def test_traversable_composition(cls, data):
        """Test composition law of traverse"""
        # Sample an applicative type constructor
        from haskpy.typeclasses.applicative import Applicative
        app_f = data.draw(testing.sample_class(Applicative))
        app_g = data.draw(testing.sample_class(Applicative))

        # Sample types
        a = data.draw(testing.sample_hashable_type())
        b = data.draw(testing.sample_hashable_type())
        c = data.draw(testing.sample_type())
        t = data.draw(cls.sample_traversable_type_constructor())

        m_f = data.draw(app_f.sample_applicative_type_constructor())
        m_g = data.draw(app_g.sample_applicative_type_constructor())

        # Sample values
        x = data.draw(t(a))
        f = data.draw(testing.sample_function(m_f(b)))
        g = data.draw(testing.sample_function(m_g(c)))

        # Check the law
        from haskpy import map
        cls.assert_traversable_traverse_composition(x,
                                                    f,
                                                    g,
                                                    app_f,
                                                    app_g,
                                                    data=data)
        cls.assert_traversable_sequence_composition(map(map(g), x.map(f)),
                                                    app_f,
                                                    app_g,
                                                    data=data)
        return
Ejemplo n.º 2
0
        def check(app_trans, app1, app2):

            a = data.draw(testing.sample_hashable_type())
            b = data.draw(testing.sample_type())

            t = data.draw(cls.sample_traversable_type_constructor())
            f = data.draw(app1.sample_applicative_type_constructor())

            x = data.draw(t(a))
            g = data.draw(testing.sample_function(f(b)))

            cls.assert_traversable_traverse_naturality(
                x,
                g,
                app_trans,
                app1,
                app2,
                data=data,
            )
            cls.assert_traversable_sequence_naturality(
                x.map(g),
                app_trans,
                app1,
                app2,
                data=data,
            )
            return
Ejemplo n.º 3
0
    def test_hashable_equality(cls, data):
        """Test that hash is equal for equal values"""

        a = data.draw(testing.sample_hashable_type())

        x = data.draw(a)
        y = data.draw(a)

        cls.assert_hashable_equality(x, y)
        return
Ejemplo n.º 4
0
    def test_traversable_traverse(cls, data):
        """Test traverse based on the default implementation

        The default implementation defines the law with respect to sequence.

        """
        # Sample an applicative type constructor
        from haskpy.typeclasses.applicative import Applicative
        app = data.draw(testing.sample_class(Applicative))

        # Sample types
        a = data.draw(testing.sample_hashable_type())
        b = data.draw(testing.sample_hashable_type())
        t = data.draw(cls.sample_traversable_type_constructor())

        f = data.draw(app.sample_applicative_type_constructor())

        # Sample values
        x = data.draw(t(a))
        g = data.draw(testing.sample_function(f(b)))

        # Check the law
        cls.assert_traversable_traverse(app, x, g, data=data)
        return
Ejemplo n.º 5
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.º 6
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(), )