pyright icon indicating copy to clipboard operation
pyright copied to clipboard

Pyright incorrectly binds type variables in higher-order functions

Open LeeeeT opened this issue 6 months ago • 1 comments

For the following code, pyright correctly binds the type variables for the nested higher order functions call. The revealed type of map2 is correct.

from collections.abc import Callable


def curry[First, *Rest, Result](function: Callable[[First, *Rest], Result]) -> Callable[[*Rest], Callable[[First], Result]]:
    return lambda *rest: lambda first: function(first, *rest)


@curry
@curry
def map1[From, To, Arg](value: Arg, first: Callable[[Arg], From], second: Callable[[From], To]) -> To:
    return second(first(value))


@curry
@curry
def map1_copy[From, To, Arg](value: Arg, first: Callable[[Arg], From], second: Callable[[From], To]) -> To:
    return second(first(value))


# While `map1` is used to map the result of a function of one argument like this:
# (From -> To) ->     (Arg1 -> From)     ->     (Arg1 -> To)
# `map2` is meant to map the result of a function of two arguments like this:
# (From -> To) -> (Arg1 -> Arg2 -> From) -> (Arg1 -> Arg2 -> To)


map2 = map1(map1)(map1)


reveal_type(map2)  # ((From(2)@map1) -> To(2)@map1) -> ((((Arg(1)@map1) -> ((Arg(2)@map1) -> From(2)@map1))) -> ((Arg(1)@map1) -> ((Arg(2)@map1) -> To(2)@map1)))

# Negate the sum of two ints
reveal_type(map2(int.__neg__)(curry(int.__add__)))  # (int) -> ((int) -> int)

However, if I change the definition of map2 using the exact copy of map1, pyright is no longer able to successfully bind the type variables resulting in the wrong type of map2.

map2 = map1(map1_copy)(map1_copy)


reveal_type(map2)  # ((From(1)@map1_copy) -> To(1)@map1_copy) -> ((((((From(1)@map1_copy) -> To(1)@map1_copy)) -> ((Arg(1)@map1_copy) -> From(1)@map1_copy))) -> ((((From(1)@map1_copy) -> To(1)@map1_copy)) -> ((Arg(1)@map1_copy) -> To(1)@map1_copy)))

reveal_type(map2(int.__neg__)(curry(int.__add__)))  # ((int) -> int) -> ((int) -> int)

As a consequence, this also results in a false positive error.

Argument of type "(int) -> ((int) -> int)" cannot be assigned to parameter of type "((int) -> int) -> ((Arg(1)@map1_copy) -> int)"
  Type "(int) -> ((int) -> int)" is incompatible with type "((int) -> int) -> ((int) -> int)"
    Parameter 1: type "(int) -> int" is incompatible with type "int"
      "function" is incompatible with "int"

For comparison, mypy's behavior doesn't change depending on whether I use map1 or map1_copy to define map2 which is the expected behavior.

LeeeeT avatar Aug 14 '24 15:08 LeeeeT