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
mypycannot not infer what arguments are there inside thismy_argsvariableBecause
currycannot know when to stop accepting*argsand**kwargsAnd 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.partialwrapper with better typing support.We use a custom
mypyplugin 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.curryis a bit smarter and will do partial application until enough arguments passed.If wrong arguments are passed,
TypeErrorwill be raised immediately.We use a custom
mypyplugin 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 otherCdefined callables*argsand**kwargsare not supported and we useAnyas a fallbackSupport 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
mypyplugin to make types correct, otherwise, it is currently impossibleIt might not work as expected with curried
Klass().method, it might generate invalid method signature (looks like a bug inmypy)It is probably a bad idea to
currya 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 typecheckCurrying of
__init__does not work because of the bug inmypy: https://github.com/python/mypy/issues/8801
We expect people to use this tool responsibly when they know that they are doing.
See also: - https://en.wikipedia.org/wiki/Currying - https://stackoverflow.com/questions/218025/
- Parameters:
function (
Callable[...,TypeVar(_ReturnType)])- Return type:
Callable[...,TypeVar(_ReturnType)]