dbrattli / Expression

Pragmatic functional programming for Python inspired by F#
https://expression.readthedocs.io
MIT License
421 stars 30 forks source link

_TSource in Option should be covariant #171

Open denghz opened 10 months ago

denghz commented 10 months ago

Describe the bug _TSource type in Option should be covariant.

class A:
  pass
class B(A):
  pass

x: Option[A] = Some(B())

Now since the _TSource is invariant, pyright will complain that B is not the same as A, but actually Option type should be covariant. Is Some[Dog] a Some[Animal]? I think you'll agree that the answer is yes. Also Option type is not actually mutable, so there is no way to assign a Some[Dog] to a variable typed Some[Aninmal] and then make it become Some[Cat]

Additional context Add any other context about the problem here.

brendanmaguire commented 9 months ago

I didn't see the above failing the type check in either mypy or pyright but the following did fail:

from expression import Some, Option

class A:
    pass

class B(A):
    pass

def test_covariance() -> None:
    x: Option[B] = Some(B())
    y: Option[A] = x
brendanmaguire commented 9 months ago

This isn't so easy to fix because covariant types can't be passed as parameters to other functions.

phi-friday commented 8 months ago

It also looks good to use infer_variance, which eliminates the need to consider covariant. ex:

from typing_extensions import TypeVar

_T = TypeVar("_T", infer_variance=True)

This parameter is supported by typing-extensions>=4.4.0 and is the default behavior in python>=3.12.

dbrattli commented 7 months ago

This looks really interesting. Will try. Look forward to when we can eventually be >= Python 3.12 😀

brendanmaguire commented 7 months ago

My understanding of infer_variance is that it only aids in not having to specify the variance of the TypeVar. See the related spec docs:

type checker should use inference to determine whether the type variable is invariant, covariant or contravariant

So I don't think it helps here. The problem still exists that covariant types can't be passed as parameters to other functions.

brendanmaguire commented 7 months ago

I locally updated typing-extensions to 4.8.0 and tried with infer_variance. My above code example still fails the type checker.

dbrattli commented 5 months ago

I've tried to make some progress on this one by making methods that takes _TSource static methods and use another type when generating e.g:

    @staticmethod
    def Some(value: _T1) -> Option[_T1]:
        """Create a Some option."""
        return Option(some=value)

Then for some more tricky methods like default_value I return a union with the default arg:

def default_value(self, value: _T1) -> _TSource | _T1:    

I'm just unsure if the rest is ok, or false negatives. E.g:

def default_with(self, getter: Callable[[], _TSource]) -> _TSource:

It type checks, but is it ok to use _TSource in an argument that is a callable that returns _TSource? I could do the same trick as with default_value but then there's all the other methods like map, bind, but they are probably ok since they return a differnet type in the callable?