Curry

This module is dedicated to partial application.

We support two types of partial application: @curry and partial.

@curry is a new concept for most Python developers, but Python already has a great tool to use partial application: functools.partial

The only problem with it is the lack of typing. Let’s see what problems do we solve with this module.

Warning

This module requires our mypy plugin to be present. Without it we will fallback to the original behaviour.

Partial

Here’s how typing works there:

from functools import partial

def some_function(first: int, second: int) -> float:
    return first / second

reveal_type(partial(some_function, 1))
# => functools.partial[builtins.float*]
# => Which is really: `def (*Any, **Any) -> builtins.float`

And compare it with our solution:

from returns.curry import partial

def some_function(first: int, second: int) -> float:
    return first / second

reveal_type(partial(some_function, 1))
# => def (second: builtins.int) -> builtins.float*
# => Which is fair!

Note

We still use functools.partial inside. We just improve the typings.

Generics

One more problem is generics support in functools.partial. Here’s the comparison:

from functools import partial
from typing import List, TypeVar

T = TypeVar('T')

x: List[int]

def some_function(first: List[T], second: int) -> T:
    return first[second]

reveal_type(partial(some_function, x))
# => functools.partial[T`-1]
# => Which is broken!

And our solution works fine:

from returns.curry import partial

reveal_type(partial(some_function, x))
# => def (second: builtins.int) -> builtins.int*

We also work with complex generic with multiple arguments or with multiple generics.

The only known problem is that passing explicit generic like [1, 2, 3] will resolve in List[Any]. Because mypy won’t be able to infer this type for some reason.

The reasonable work-around is to pass annotated variables like in the example above.

Types and Instances

We can also work with types and instances. Because they are callable too!

from returns.curry import partial

class Test(object):
    def __init__(self, arg: int) -> None:
        self.arg = arg

    def __call__(self, other: int) -> int:
        return self.arg + other

reveal_type(partial(Test, 1))  # N: Revealed type is 'def () -> ex.Test'
reveal_type(partial(Test(1), 1))  # N: Revealed type is 'def () -> builtins.int'

No differences with regular callables at all.

Overloads

We also support working with @overload definitions. It also looks the same way:

from typing import overload
from returns.curry import partial

@overload
def test(a: int, b: str) -> str:
    ...

@overload
def test(a: int) -> int:
    ...

@overload
def test(a: str) -> None:  # won't match!
    ...

def test(a, b=None):
    ...

reveal_type(partial(test, 1))  # N: Revealed type is 'Overload(def (b: builtins.str) -> builtins.str, def () -> builtins.int)'

From this return type you can see that we work with all matching cases and discriminate unmatching ones.

@curry

curry allows to provide only a subset of arguments to a function. And it won’t be called until all the required arguments are provided.

In contrast to partial which works on the calling stage, @curry works best when defining a new function.

>>> from returns.curry import curry

>>> @curry
... def function(first: int, second: str) -> bool:
...     return len(second) > first

>>> assert function(1)('a') is False
>>> assert function(1, 'a') is False
>>> assert function(2)('abc') is True
>>> assert function(2, 'abc') is True

Take a note, that providing invalid arguments will raise TypeError:

>>> function(1, 2, 3)
Traceback (most recent call last):
  ...
TypeError: too many positional arguments

>>> function(a=1)
Traceback (most recent call last):
  ...
TypeError: got an unexpected keyword argument 'a'

This is really helpful when working with .apply() method of containers.

Warning

We recommend using partial instead of @curry when possible because it’s much faster.

Typing

@curry functions are also fully typed with our custom mypy plugin.

Let’s see how types do look like for a curried function:

>>> from returns.curry import curry

>>> @curry
... def zero(a: int, b: float, *, kw: bool) -> str:
...     return str(a - b) if kw else ''

>>> assert zero(1)(0.3)(kw=True) == '0.7'
>>> assert zero(1)(0.3, kw=False) == ''

# If we will reveal the type it would be quite big:

reveal_type(zero)

# Overload(
#   def (a: builtins.int) -> Overload(
#     def (b: builtins.float, *, kw: builtins.bool) -> builtins.str,
#     def (b: builtins.float) -> def (*, kw: builtins.bool) -> builtins.str
#   ),
#   def (a: builtins.int, b: builtins.float) -> def (*, kw: builtins.bool)
#     -> builtins.str,
#   def (a: builtins.int, b: builtins.float, *, kw: builtins.bool)
#     -> builtins.str
# )

It reveals to us that there are 4 possible way to call this function. And we type all of them with overload type.

When you provide any arguments, you discriminate some overloads and choose more specific path:

reveal_type(zero(1, 2.0))
# By providing this set of arguments we have chosen this path:
#
#   def (a: builtins.int, b: builtins.float) -> def (*, kw: builtins.bool)
#     -> builtins.str,
#
# And the revealed type would be:
#
#   def (*, kw: builtins.bool) -> builtins.str
#

It works with functions, instance, class, and static methods, including generics. See Limitations in the API Reference.

FAQ

Why don’t you support * and ** arguments?

When you use partial(some, *my_args) or partial(some, **my_args) or both of them at the same time, we fallback to the default return type. The same happens with curry. Why?

There are several problems:

  • Because mypy cannot not infer what arguments are there inside this my_args variable

  • Because curry cannot know when to stop accepting *args and **kwargs

  • And there are possibly other problems!

Our advice is not to use *args and *kwargs with partial and curry.

But, it is still possible, but in this case we will fallback to Any.

Further reading

API Reference

partial(func, *args, **kwargs)[source]

Typed partial application.

It is just a functools.partial wrapper with better typing support.

We use a custom mypy plugin to make sure types are correct. Otherwise, it is currently impossible to properly type this function.

>>> from returns.curry import partial

>>> def sum_two_numbers(first: int, second: int) -> int:
...     return first + second

>>> sum_with_ten = partial(sum_two_numbers, 10)
>>> assert sum_with_ten(2) == 12
>>> assert sum_with_ten(-5) == 5
Parameters:
  • func (Callable[..., TypeVar(_ReturnType)]) –

  • args (Any) –

  • kwargs (Any) –

Return type:

Callable[..., TypeVar(_ReturnType)]

curry(function)[source]

Typed currying decorator.

Currying is a conception from functional languages that does partial applying. That means that if we pass one argument in a function that gets 2 or more arguments, we’ll get a new function that remembers all previously passed arguments. Then we can pass remaining arguments, and the function will be executed.

partial() function does a similar thing, but it does partial application exactly once. curry is a bit smarter and will do partial application until enough arguments passed.

If wrong arguments are passed, TypeError will be raised immediately.

We use a custom mypy plugin to make sure types are correct. Otherwise, it is currently impossible to properly type this function.

>>> from returns.curry import curry

>>> @curry
... def divide(number: int, by: int) -> float:
...     return number / by

>>> divide(1)  # doesn't call the func and remembers arguments
<function divide at ...>
>>> assert divide(1)(by=10) == 0.1  # calls the func when possible
>>> assert divide(1)(10) == 0.1  # calls the func when possible
>>> assert divide(1, by=10) == 0.1  # or call the func like always

Here are several examples with wrong arguments:

>>> divide(1, 2, 3)
Traceback (most recent call last):
  ...
TypeError: too many positional arguments

>>> divide(a=1)
Traceback (most recent call last):
  ...
TypeError: got an unexpected keyword argument 'a'

Limitations:

  • It is kinda slow. Like 100 times slower than a regular function call.

  • It does not work with several builtins like str, int, and possibly other C defined callables

  • *args and **kwargs are not supported and we use Any as a fallback

  • Support of arguments with default values is very limited, because we cannot be totally sure which case we are using: with the default value or without it, be careful

  • We use a custom mypy plugin to make types correct, otherwise, it is currently impossible

  • It might not work as expected with curried Klass().method, it might generate invalid method signature (looks like a bug in mypy)

  • It is probably a bad idea to curry a function with lots of arguments, because you will end up with lots of overload functions, that you won’t be able to understand. It might also be slow during the typecheck

  • Currying of __init__ does not work because of the bug in mypy: https://github.com/python/mypy/issues/8801

We expect people to use this tool responsibly when they know that they are doing.

Parameters:

function (Callable[..., TypeVar(_ReturnType)]) –

Return type:

Callable[..., TypeVar(_ReturnType)]