"""Functions
.. autosummary::
:toctree:
Function
FunctionMonoid
.. autosummary::
:toctree:
function
compose
"""
import attr
import functools
import inspect
from hypothesis import strategies as st
from haskpy.typeclasses._monad import Monad
from haskpy.typeclasses._monoid import Monoid
from haskpy.typeclasses._cartesian import Cartesian
from haskpy.typeclasses._cocartesian import Cocartesian
from haskpy.typeclasses._semigroup import Semigroup
from haskpy.internal import (
immutable,
class_property,
class_function,
)
from haskpy.testing import eq_test
from haskpy import testing
@immutable
class _Code():
co_argcount = attr.ib()
co_flags = attr.ib()
[docs]def FunctionMonoid(monoid):
"""Create a function type that has a Monoid instance"""
@immutable
class _FunctionMonoid(Monoid, Function):
"""Function type with Monoid instance added"""
@class_property
def empty(cls):
return _FunctionMonoid(lambda _: monoid.empty)
@class_function
def sample_monoid_type(cls):
t = monoid.sample_monoid_type()
return t.map(lambda b: cls.sample_value(None, b))
return _FunctionMonoid
[docs]@immutable
class Function(Monad, Cartesian, Cocartesian, Semigroup):
"""Monad instance for curried functions
Note the resulting ``Function`` object accepts only positional arguments.
The number of those positional arguments is exactly the same as the number
of required positional arguments in the underlying function. Any optional
positional or keyword arguments become unusable. The reason for this is
that it must be unambiguous at which point, for instance, ``map`` is
applied. Also, ``f(a)(b)`` should always be equal to ``f(a, b)`` with
curried functions. This might not be the case if there are optional
arguments.
Use similar wrapping as functools.wraps does for some attributes. See
CPython: https://github.com/python/cpython/blob/master/Lib/functools.py#L30
.. note::
Monoid instance of Function requires the knowledge of the contained
monoid type in order to be able to create ``empty``. The contained type
is not known because Function class can be used to create functions of
any type. This is just convenience and simpler user interface. If you
need Monoid instance of Function, use FunctionMonoid function to create
such a class. Note though that the Semigroup instance is available in
this Function without needing to use FunctionMonoid.
"""
# NOTE: Currying functions is a bit slow (mainly because of
# functools.wraps). So don't use converter=curry here. Instead provide a
# decorator ``function`` which combines Function and curry.
__f = attr.ib()
__args = attr.ib(default=(), converter=tuple)
[docs] def __attrs_post_init__(self):
object.__setattr__(self, "__qualname__", self.__f.__qualname__)
object.__setattr__(self, "__module__", self.__f.__module__)
object.__setattr__(self, "__doc__", self.__f.__doc__)
object.__setattr__(self, "__name__", self.__f.__name__)
object.__setattr__(self, "__annotations__", self.__f.__annotations__)
object.__setattr__(self, "__defaults__", None)
object.__setattr__(self, "__kwdefaults__", None)
return
@property
def __code__(self):
# We use __code__.co_argcount attribute in this Function class, so
# let's add this attribute to Function objects too so that we can wrap
# Function objects with Function class.
return _Code(
co_argcount=self.__f.__code__.co_argcount - len(self.__args),
# co_flags is needed by Sphinx for some reason..
co_flags=self.__f.__code__.co_flags,
)
@property
def __signature__(self):
return inspect.signature(self.__f)
[docs] @__f.validator
def check_f(self, attribute, value):
if not callable(value):
raise ValueError("The function must a callable")
nargs = value.__code__.co_argcount
if nargs == 0:
raise ValueError(
"The function must have at least one required positional "
"argument"
)
return
[docs] @__args.validator
def check_args(self, attribute, value):
if len(value) >= self.__f.__code__.co_argcount:
raise ValueError()
return
# TODO: Add __annotations__
[docs] @class_function
def pure(cls, x):
return cls(lambda _: x)
[docs] def dimap(f, g, h):
"""(b -> c) -> (a -> b) -> (c -> d) -> (a -> d)"""
return Function(lambda x: h(f(g(x))))
[docs] def map(f, g):
"""(a -> b) -> (b -> c) -> (a -> c)"""
return Function(lambda x: g(f(x)))
[docs] def contramap(f, g):
"""(b -> c) -> (a -> b) -> (a -> c)"""
return Function(lambda a: f(g(a)))
[docs] def apply(f, g):
"""(a -> b) -> (a -> b -> c) -> a -> c"""
return Function(lambda x: g(x)(f(x)))
[docs] def bind(f, g):
"""(a -> b) -> (b -> a -> c) -> a -> c"""
return Function(lambda x: g(f(x))(x))
[docs] def first(f):
"""(a -> b) -> (a, c) -> (b, c)"""
from haskpy.utils import identity
return _cross(f, identity)
[docs] def second(f):
"""(a -> b) -> (c, a) -> (c, b)"""
from haskpy.utils import identity
return _cross(identity, f)
[docs] def left(f):
"""(a -> b) -> Either a c -> Either b c"""
from haskpy.utils import identity
return _plus(f, identity)
[docs] def right(f):
"""(a -> b) -> Either c a -> Either c b"""
from haskpy.utils import identity
return _plus(identity, f)
[docs] def append(f, g):
"""(a -> b) -> (a -> b) -> (a -> b)"""
return f.map(lambda x: lambda y: x.append(y)).apply_to(g)
[docs] def __call__(self, *args):
# Don't add docstring here because it shows up a bit stupidly in help
# texts.
args = self.__args + args
n = self.__f.__code__.co_argcount
m = len(args)
if m < n:
# Function partially applied.
return attr.evolve(self, Function__args=args)
elif m == n:
# Function fully applied
return self.__f(*args)
else:
# Function fully applied and some arguments left over
return self.__f(*args[:n])(*args[n:])
def __repr__(self):
return repr(self.__f)
[docs] def __pow__(self, x):
# We need to implement __pow__ for Function objects because otherwise
# composing Function objects wouldn't be possible: If f and g are
# Function objects in f ** g, Python will try f.__pow__(g) and if it
# fails, it won't try g.__rpow__(f) because it already concluded that
# Function object doesn't support pow operation with a Function object.
return x.__rpow__(self)
[docs] def __get__(self, obj, objtype):
"""Support instance methods.
See: https://stackoverflow.com/a/3296318
"""
if obj is not None:
# Instance method, bind the first argument
fp = functools.partial(self, obj)
# Keep the docstring untouched
fp.__doc__ = self.__doc__
return Function(fp)
else:
# Class method
return self
[docs] def __eq_test__(self, g, data, input_strategy=st.integers()):
# NOTE: This is used only in tests when the function input doesn't
# really matter so any hashable type here is ok. The type doesn't
# matter because the functions are either _TestFunction or created with
# pure.
x = data.draw(input_strategy)
return eq_test(self(x), g(x), data)
@class_function
def sample_value(cls, _, b):
return testing.sample_function(b).map(cls)
sample_type = testing.sample_type_from_value(
testing.sample_hashable_type(),
testing.sample_type(),
)
sample_semigroup_type = testing.sample_type_from_value(
testing.sample_hashable_type(),
testing.sample_semigroup_type(),
)
sample_monoid_type = testing.sample_type_from_value(
testing.sample_hashable_type(),
testing.sample_monoid_type(),
)
sample_functor_type = testing.sample_type_from_value(
testing.sample_hashable_type(),
)
sample_applicative_type = sample_functor_type
sample_monad_type = sample_functor_type
@class_function
def sample_contravariant_type(cls, a):
return st.tuples(a, testing.sample_type()).map(
lambda args: cls.sample_value(*args)
)
sample_profunctor_type = testing.sample_type_from_value()
[docs]def function(f):
"""Decorator for currying and transforming functions into monads"""
# Don't wrap twice
return f if isinstance(f, Function) else Function(f)
[docs]@function
def compose(f, g):
return function(g).map(f)
@function
def _cross(f, g, ab):
"""(a -> c) -> (b -> d) -> (a, b) -> (c, d)"""
return (f(ab[0]), g(ab[1]))
@function
def _plus(f, g, eab):
"""(a -> c) -> (b -> d) -> Either a b -> Either c d"""
# FIXME: Once Bifunctor has been implemented, just use:
# eab.bimap(f, g)
from haskpy.types.either import Left, Right
return eab.match(Left=lambda a: Left(f(a)), Right=lambda b: Right(g(b)))