在 Python 中输入柯里化函数

som*_*bra 5 python functional-programming

有这样一个柯里化函数。问题是我不\xe2\x80\x99不知道如何使这个函数返回具有正确类型的修饰函数。求助,我在任何地方都没有找到解决方案。

\n\n
import functools\nimport typing as ty\nfrom typing import TypeVar, Callable, Any, Optional\n\nF = TypeVar("F", bound=Callable[..., Any])\n\ndef curry(func: F, max_argc: Optional[int] = None):\n    if max_argc is None:\n        max_argc = func.__code__.co_argcount\n\n    @functools.wraps(func)\n    def wrapped(*args): \n        argc = len(args)\n        if argc < max_argc:\n            return curry(functools.partial(func, *args), max_argc - argc)\n        else:\n            return func(*args)\n    return ty.cast(F, wrapped)\n\n@curry\ndef foo(x: int, y: int) -> int:\n    return x + y\n\nfoo("df")(5)  # mypy error: Too few arguments for "foo"\n              # mypy error: "int" not callable\n              # mypy error: Argument 1 to "foo" has incompatible type "str"; expected "int"  # True\n
Run Code Online (Sandbox Code Playgroud)\n\n

如何修复 1、2 mypy 错误?

\n

ale*_*580 9

有趣的是,我尝试了完全相同的事情,编写一个装饰器,它将返回任何函数的柯里化版本,包括通用的高阶函数。我尝试建立一个curry允许对输入参数进行任何分区的模型。

否认

然而,据我所知,由于 python 类型系统的一些限制,这是不可能的。我现在正在努力寻找一种通用的类型安全方法。我的意思是 Haskell 做到了这一点,C++ 采用了它,Typescript 也做到了,那么为什么 python 就不能呢?

我寻求mypy替代方案,例如pyright,它有很棒的维护者,但仍然受到 PEP 的约束,PEP 声明有些事情是不可能的。

愤怒

当提交关于我的咖喱链中最后一个缺失部分的问题时pyright,我将问题归结为以下内容(如本期所示:https ://github.com/microsoft/pyright/issues/1720 )

from typing import TypeVar

from typing_extensions import Protocol

R = TypeVar("R", covariant=True)
S = TypeVar("S", contravariant=True)


class ArityOne(Protocol[S, R]):
    def __call__(self, __s: S) -> R:
        ...


def id_f(x: ArityOne[S, R]) -> ArityOne[S, R]:
    return x


X = TypeVar("X")


def identity(x: X) -> X:
    return x


i: int = id_f(identity)(4) # Does not type check, expected type `X`, got type `Literal[4]`
Run Code Online (Sandbox Code Playgroud)

请注意间隙,这是缺失链接的最小可重复示例。

我最初尝试做的事情如下(跳过实际curry实现,相比之下,这就像在公园散步):

  1. 编写一个curry装饰器(不带类型)
  2. 定义一元、二元和三元(等)Protocol,这是函数类型的更现代版本Callable。巧合的是,Protocols可以指定类型@overloads为其__call__方法指定类型,这让我想到了下一点。
  3. 使用edProtocol类型的 s定义 CurriedBinary、CurriedTernary(等)@overload__call__
  4. @overload为函数定义 type s curry,例如Binary -> CurriedBinaryTernary -> CurriedTernary

有了这个,一切就位了,并且它对于固定类型函数(即int -> intstr -> int -> bool效果非常好。不过,我现在手头没有尝试的实施。

然而,当柯里化诸如map, 或 之类的函数时filter的函数时,它无法将函数的柯里化泛型版本与实际类型相匹配。

讨价还价

发生这种情况是由于类型变量的作用域如何工作的。有关更多详细信息,您可以查看 GitHub 问题。那里有更详细的解释。

本质上发生的情况是,要柯里化的通用函数的类型变量不能部分地受到要传递的数据的实际类型的影响,因为有一个class Protocol自己的类型范围的变量变量。

尝试包装或重组东西并没有产生丰硕的结果。

沮丧

Protocol在那里使用 s 来表示柯里化函数的类型,这对于 eg 来说是不可能的Callable,尽管pyright显示重载函数的类型,因为Overload[contract1, contract2, ...]没有这样的符号,仅@overload

所以无论哪种方式,都会有一些东西阻止你表达你想要的类型。

由于 python 类型系统的限制,目前不可能表示完全通用的类型安全柯里函数。

验收

但是,可能会在某些功能上做出妥协,例如泛型或输入参数的任意分区。

以下curry工程的实现在pyright 1.1.128.

from typing import TypeVar, Callable, List, Optional, Union


R = TypeVar("R", covariant=True)
S = TypeVar("S", contravariant=True)
T = TypeVar("T", contravariant=True)


def curry(f: Callable[[T, S], R]) -> Callable[[T], Callable[[S], R]]:
    raise Exception()


X = TypeVar("X")
Y = TypeVar("Y")


def function(x: X, y: X) -> X:
    raise Exception()

def to_optional(x: X) -> Optional[X]:
    raise Exception()

def map(f: Callable[[X], Y], xs: List[X]) -> List[Y]:
    raise Exception()


i: int = curry(function)(4)(5)
s: List[Optional[Union[str, int]]] = curry(map)(to_optional)(["dennis", 4])
Run Code Online (Sandbox Code Playgroud)


Joe*_*ley 1

首先,我不会将其作为装饰器,而是将其包装为curry(foo). 我发现查看修饰函数签名与其初始定义不同的 API 会很混乱。

关于类型的主题,如果使用 Python 类型提示可以实现一般情况,我会印象非常深刻。我不确定如何在 Scala 中做到这一点。您可以执行有限数量的情况,使用overload两个参数的函数作为

T1 = TypeVar("T1")
T2 = TypeVar("T2")
U = TypeVar("U")

@overload
def curry(
    func: Callable[[T1, T2], U],
    max_argc: Optional[int]
) -> Callable[[T1], Callable[[T2], U]]:
    ...
Run Code Online (Sandbox Code Playgroud)

添加一个、三个、四个参数等的版本。无论如何,具有大量参数的函数都是代码味道,除了可变参数,我不确定它是否值得柯里化。