Curring in Python and why it sucks
Tags: python, functional
Published: 2022-05-24 12:55:08

## What is curring?

Curring is a functional technic (or feature) that allows developers to pass arguments to the functions partially. Let's look at the example.

### Example

Imagine we have function `decode` that converts `str` to `bytes` with some encoding.

> Yep, I know that there is built-in `str.decode` method, but for simplicity and some non-imaginary `add` example let's look at this one

```python
def decode(encoding: str, s: str) -> bytes:
    return s.decode(encoding)
```

This function is executed like `decode("UTF-8", some_str)` all the time. I'm sure that you would mostly use one encoding ("UTF-8" in particular) so this `"UTF-8"` argument would be everywhere. One day tired of writing it all the time you decide to refactor that and write another function:

```python
def decode_utf_8(s: str) -> bytes:
    return decode("UTF-8", s)
```

It is actually just a shortcut, but unknowingly you used curring (but not in the way real functional languages do)! Curring is about passing some of the arguments to the function to get another function that just waits when you pass left arguments.

So if function has 3 arguments and you pass only 2, than instead of raising exception you actually get function that accepts only one left argument.

## What if Python supported curring?

If Python would support this feature natively than you could've written something like:

```python
# this does not work!!!
decode_utf_8 = decode("UTF-8")  # type: Callable[[str], bytes]
```

Looks great to me, but to be honest I don't see any possibility this might actually become real in Python3.X (maybe in Python4 some day, but who knows?) due to backward compatibility issues.

But there are a few legal ways of curring functions in Python right now and right there.

## Option 1: `functools.partial`

With `functools.partial` I could've written `decode_utf_8` like so:

```python
from functools import partial

decode_utf_8 = partial(decode, "UTF-8")

decode_utf_8("hello")  # b"hello"
```

## Option 2: `toolz.curry` (better one)

Another option is provided by great package [`toolz`](https://toolz.readthedocs.io/en/latest/api.html)

```python
from toolz import curry

@curry
def decode(encoding: str, s: str) -> bytes:
    return s.decode(encoding)

decode_utf_8 = decode("UTF-8")
```

This option looks much-much better as it seems like curring is natively supported with just one decorator.

## But why it sucks?

Python is dynamically typed language and after using `partial` or `@curry` you can't really get type hint for your code editor. All your attempts to keep code clean and typed via writing type hints and using static type checkers like `mypy` are lost in vain. VS Code for example tells that `decode_utf_8` has type `partial` or `curry` instead of actual `Callable[[str], bytes]`.

This makes it hard to use, because you need to remember by yourself what arguments you need to pass to actually execute function. Also it's not always clear what to do with optional arguments. Functions with arbitrary arguments can't be obviously curried too.

## Is there any hack?

Well, I've been looking for one and made this simple workaround:

```python
from typing import (
    Callable,
    Concatenate,
    ParamSpec,
    TypeVar,
)

P = ParamSpec("P")
A1 = TypeVar("A1")
AResult = TypeVar("AResult")


def hof1(func: Callable[Concatenate[A1, P], AResult]):
    """Separate first argument from other."""

    def _wrapper(arg_1: A1) -> Callable[P, AResult]:
        def _func(*args: P.args, **kwargs: P.kwargs) -> AResult:
            return func(arg_1, *args, **kwargs)

        return _func

    return _wrapper

# also hof2 and hof3 decorator for separating first 2 and 3
# arguments correspondingly

@hof1
def decode(encoding: str, s: str) -> bytes:
    return s.decode(encoding)

decode_utf_8 = decode("UTF-8")
# but you can't do this:
decode("UTF-8", "hello")  # raises error
```

With this decorator you must know beforehand what arguments you might want to pass first and what arguments you gonna pass to actually execute function. This is a serious limit, but it helps me save my type hints which I consider more important than easier syntax.

More extensive example:

```python
from toolz import pipe


@hof1
def encode(encoding: str, b: bytes) -> str:
    return b.encode(encoding)


@hof1
def split(sep: str, s: str) -> Iterable[str]:
    return s.split(sep)

@hof1
def cmap(
   mapper: Callable[[X], Y], iterable: Iterable[X]
) -> Iterable[Y]:
    return map(mapper, iterable)

query = pipe(
    b"name=John&age=32",
    encode("UTF-8"),
    split("&"),
    cmap(split("=")),
    dict,
)

print(query)  # {"name": "John", "age": "32"}

# same without hof1
query = pipe(
    b"name=John&age=32",
    lambda b: b.encode("UTF-8"),
    lambda s: s.split("&"),
    lambda ss: map(lambda s: s.split("="), ss),
    dict,
)
```

What do you think about curring and such workaround? Share your thoughts and questions in the comments!