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.
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 out 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.
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.
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.
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
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.
@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.
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
.
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
func (Callable
[…, ~_ReturnType]) –
args (Any
) –
kwargs (Any
) –
Callable
[…, ~_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.
function (Callable
[…, ~_ReturnType]) –
Callable
[…, ~_ReturnType]