import inspect
from collections.abc import Callable, Iterable
from functools import wraps
from operator import matmul
from katharos.algebra import Applicative, Semigroup
from katharos.types.list import NonEmptyList
[docs]
class F:
"""Namespace for utility functions.
All functions are static and can be called without instantiating the class.
"""
[docs]
@staticmethod
def compose[A, B, C](
f: Callable[[B], C],
) -> Callable[[Callable[[A], B]], Callable[[A], C]]:
"""Compose two functions.
Args:
f: A function from B to C.
Returns:
A function that takes a function from A to B and returns a function
from A to C.
Examples:
>>> inc = lambda x: x + 1
>>> double = lambda x: x * 2
>>> inc_then_double = F.compose(double)(inc)
>>> inc_then_double(5)
12
"""
def inner(g: Callable[[A], B]) -> Callable[[A], C]:
return lambda x: f(g(x))
return inner
[docs]
@staticmethod
def id[A](x: A) -> A:
"""Identity function.
Args:
x: Input value.
Returns:
The same value x.
Examples:
>>> F.id(42)
42
>>> F.id("hello")
'hello'
"""
return x
[docs]
@staticmethod
def foldr[A, B](
f: Callable[[A, B], B],
acc: B,
xs: Iterable[A],
) -> B:
"""Right fold a function over an iterable.
Args:
f: A function that takes an element and an accumulator and returns a
new accumulator.
acc: The initial accumulator value.
xs: An iterable of elements.
Returns:
The accumulator value after applying f to each element of xs.
Examples:
>>> F.foldr(lambda x, acc: acc + [x], [], [1, 2, 3])
[3, 2, 1]
>>> F.foldr(lambda x, acc: f"({x}+{acc})", "0", [1, 2, 3])
'(1+(2+(3+0)))'
"""
result = acc
for x in reversed(list(xs)):
result = f(x, result)
return result
[docs]
@staticmethod
def foldl[A, B](
f: Callable[[B, A], B],
acc: B,
xs: Iterable[A],
) -> B:
"""Left fold a function over an iterable.
Args:
f: A function that takes an accumulator and an element and returns a
new accumulator.
acc: The initial accumulator value.
xs: An iterable of elements.
Returns:
The accumulator value after applying f to each element of xs.
Examples:
>>> F.foldl(lambda acc, x: acc + x, 0, [1, 2, 3])
6
>>> F.foldl(lambda acc, x: f"({acc}+{x})", "0", [1, 2, 3])
'(((0+1)+2)+3)'
"""
result = acc
for x in xs:
result = f(result, x)
return result
[docs]
@staticmethod
def sigma[A: Semigroup](xs: NonEmptyList[A]) -> A:
"""Combine all elements of a non-empty list using the semigroup operation.
Args:
xs: A non-empty list of semigroup elements.
Returns:
The result of combining all elements using the semigroup's @ operator.
Examples:
>>> from katharos.types.list import NonEmptyList
>>> from katharos.types.monoid import Sum
>>> F.sigma(NonEmptyList(Sum(1), [Sum(2), Sum(3)]))
Sum(6)
"""
return F.foldl(
matmul,
xs.head,
xs.tail,
)
[docs]
@staticmethod
def curry(f: Callable) -> Callable:
"""Transform a multi-argument function into a curried version.
Currying converts a function that takes multiple arguments into a sequence
of functions, each taking a single argument. Supports both positional and
keyword arguments.
Args:
f: A function to curry.
Returns:
A curried version of the function.
Examples:
>>> def add(x: int, y: int, z: int) -> int:
... return x + y + z
>>> curried_add = F.curry(add)
>>> curried_add(1)(2)(3)
6
>>> add_one = curried_add(1)
>>> add_one(2)(3)
6
>>> curried_add(x=1)(y=2)(z=3)
6
>>> curried_add(1, y=2)(z=3)
6
"""
sig = inspect.signature(f)
params = list(sig.parameters.values())
num_params = len(params)
if num_params == 0:
return f
@wraps(f)
def curried(*args, **kwargs):
bound_args = sig.bind_partial(*args, **kwargs)
bound_args.apply_defaults()
total_bound = len(bound_args.arguments)
if total_bound >= num_params:
return f(*args, **kwargs)
def partial(*more_args, **more_kwargs):
combined_kwargs = {**kwargs, **more_kwargs}
return curried(*(args + more_args), **combined_kwargs)
return partial
return curried
[docs]
@staticmethod
def lift_a2[A, B, C, App](
f: Callable[[A, B], C],
fa: Applicative[App, A],
fb: Applicative[App, B],
) -> Applicative[App, C]:
"""Lift a binary function into an applicative context.
Applies a two-argument function to the values held inside two
applicatives of the same type, combining their contexts.
Args:
f: A function taking two arguments of types A and B.
fa: An applicative containing a value of type A.
fb: An applicative containing a value of type B.
Returns:
An applicative containing the result of applying f to the two
wrapped values.
Examples:
>>> from katharos.types.maybe import Maybe
>>> F.lift_a2(lambda x, y: x + y, Maybe.Just(2), Maybe.Just(3))
Just(5)
>>> F.lift_a2(lambda x, y: x + y, Maybe.Just(2), Maybe.Nothing())
Nothing()
"""
curried = F.curry(f)
return fb.ap(fa.ap(fa.pure(curried)))
[docs]
@staticmethod
def lift_a3[A, B, C, D, App](
f: Callable[[A, B, C], D],
fa: Applicative[App, A],
fb: Applicative[App, B],
fc: Applicative[App, C],
) -> Applicative[App, D]:
"""Lift a ternary function into an applicative context.
Applies a three-argument function to the values held inside three
applicatives of the same type, combining their contexts.
Args:
f: A function taking three arguments of types A, B and C.
fa: An applicative containing a value of type A.
fb: An applicative containing a value of type B.
fc: An applicative containing a value of type C.
Returns:
An applicative containing the result of applying f to the three
wrapped values.
Examples:
>>> from katharos.types.maybe import Maybe
>>> F.lift_a3(
... lambda x, y, z: x + y + z,
... Maybe.Just(1),
... Maybe.Just(2),
... Maybe.Just(3),
... )
Just(6)
>>> F.lift_a3(
... lambda x, y, z: x + y + z,
... Maybe.Just(1),
... Maybe.Nothing(),
... Maybe.Just(3),
... )
Nothing()
"""
curried = F.curry(f)
return fc.ap(fb.ap(fa.ap(fa.pure(curried))))