Skip to content

Add @curry decorator #350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a9217cf
init
orsinium Apr 22, 2020
85568d6
raise type error when too many args
orsinium Apr 22, 2020
661153e
test some cases
orsinium Apr 22, 2020
c9efac1
test more and preserve docs
orsinium Apr 22, 2020
ad4afb3
simplify
orsinium Apr 22, 2020
ab02439
test and re-write lazy_curry
orsinium Apr 23, 2020
ad2b44e
test more
orsinium Apr 23, 2020
8223169
turn lazy_curry into a func
orsinium Apr 23, 2020
7b86998
mv docs for eager_curry
orsinium Apr 23, 2020
f6a4c58
handle typing
orsinium Apr 23, 2020
93f4e30
update docs
orsinium Apr 23, 2020
0ed9354
fix regexp and better doctests
orsinium Apr 23, 2020
8241a97
a bit more doctests
orsinium Apr 23, 2020
1a4d5bc
trigger CI
orsinium Apr 23, 2020
dec9cfd
drop
orsinium Apr 25, 2020
0706beb
Merge remote-tracking branch 'dry-python/master' into curry
orsinium Apr 25, 2020
cf2bd14
+benchmarks
orsinium Apr 25, 2020
49c157a
fix WPS complaints
orsinium Apr 25, 2020
cd15485
isort
orsinium Apr 25, 2020
8cdff32
fix some wps complaints
orsinium Apr 25, 2020
a2c65a5
use closures
orsinium Apr 27, 2020
e967f1b
test TypeError raising
orsinium Apr 27, 2020
d462532
Changes implementation of @curry
sobolevn May 6, 2020
116a7a9
Fixes doctest
sobolevn May 6, 2020
8a0eac3
Initial work on typing for @curry
sobolevn May 7, 2020
5c9c9cd
Merge branch 'master' into curry
sobolevn May 7, 2020
97d6678
Fixes CI
sobolevn May 7, 2020
0b3d0ab
Adds more typetests
sobolevn May 7, 2020
7c4784f
Fixes tests for methods and classmethods
sobolevn May 12, 2020
aa4719d
Adds docs
sobolevn May 13, 2020
36af9e9
Adds more tests
sobolevn May 13, 2020
9dac064
Fixes CI
sobolevn May 14, 2020
661b69e
Adds proper MypyType annotation
sobolevn May 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 117 additions & 18 deletions docs/pages/curry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
Curry
=====

Python already has a great tool to use partial application:
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 <https://docs.python.org/3/library/functools.html#functools.partial>`_

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

.. warning::

Expand Down Expand Up @@ -145,39 +150,133 @@ From this return type you can see that we work
with all matching cases and discriminate unmatching ones.


FAQ
---
curry
-----

What is the difference between curring and partial?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``curry`` allows to provide only a subset of arguments to a function.
And it won't be called untill all the required arguments are provided.

This is a question a lot of Python developers ask.
In contrast to ``partial`` which works on the calling stage,
``@curry`` works best when defining a new function.

`Here are some great answers <https://stackoverflow.com/questions/218025/what-is-the-difference-between-currying-and-partial-application>`_.
.. code:: python

.. warning::
>>> 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``:

.. code::

>>> 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.

Typing
~~~~~~

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

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

.. code:: python

Python has a very limited support for real curring in a way like
``(x, y, z) -> t`` => ``x -> y -> z -> t``
works in languages like Haskell.
>>> from returns.curry import curry

This is actually a partial application, but that's the best we can do.
>>> @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 reaveals to us that there are 4 possible way to call this function.
And we type all of them with
`overload <https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading>`_
type.

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

.. code:: python

reveal_type(zero(1, 2.0))
# By providing this set of arguments we have choosen 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. Why?
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``.


Because ``mypy`` cannot not infer what arguments are there
inside this ``my_args`` variable.
Further reading
---------------

Our advice is not to use ``*args`` and ``*kwargs`` in ``partial`` call.
- `functools.partial <https://docs.python.org/3/library/functools.html#functools.partial>`_
- `Currying <https://en.wikipedia.org/wiki/Currying>`_
- `@curry decorator <https://stackoverflow.com/questions/9458271/currying-decorator-in-python>`_


API Reference
-------------

.. automodule:: returns.curry
:members:
3 changes: 2 additions & 1 deletion returns/context/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""
This module was quite big one, so we have split it.
This module was quite a big one, so we have split it.

isort:skip_file
"""

from returns.context.requires_context import ( # noqa: F401
Context as Context,
RequiresContext as RequiresContext,
Reader as Reader,
NoDeps as NoDeps,
)
from returns.context.requires_context_result import ( # noqa: F401
Expand Down
6 changes: 6 additions & 0 deletions returns/context/requires_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,9 @@ def ask(cls) -> RequiresContext[_EnvType, _EnvType]:

"""
return RequiresContext(identity)


# Aliases

#: Sometimes `RequiresContext` is too long to type.
Reader = RequiresContext
3 changes: 0 additions & 3 deletions returns/context/requires_context_io_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,9 +947,6 @@ def from_failure(
"""
return RequiresContextIOResult(lambda _: IOFailure(inner_value))

# TODO: support from_successful_result_context
# TODO: support from_failed_result_context


@final
class ContextIOResult(Immutable, Generic[_EnvType]):
Expand Down
Loading