Skip to content
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

Enable Supports[] to refer to multiple typeclasses #206

Closed
eganjs opened this issue Jun 2, 2021 · 9 comments
Closed

Enable Supports[] to refer to multiple typeclasses #206

eganjs opened this issue Jun 2, 2021 · 9 comments
Labels
enhancement New feature or request

Comments

@eganjs
Copy link

eganjs commented Jun 2, 2021

class Foo:
    def __call__(self, instance) -> A:
        ...

foo = typeclass(Foo)

...

class Bar:
    def __call__(self, instance) -> B:
        ...

bar = typeclass(Bar)

...

def fn(instance: Supports[Foo, Bar]) -> C:
    ...

This is similar to #83 and python/typing#213, but offers the advantages of composability and conciseness.

I'd be willing to have a go at implementing this but I'm not very familiar with mypy plugins (where I suspect most of the work would be required).

@sobolevn
Copy link
Member

sobolevn commented Jun 2, 2021

@eganjs great idea! I will do this in several days, I am preparing a new release right now! 👍

@eganjs
Copy link
Author

eganjs commented Jun 2, 2021

Awesome @sobolevn I look forward to using it in action!

@sobolevn
Copy link
Member

sobolevn commented Jun 2, 2021

Sneak peak (I am working on Generic typeclasses):
Снимок экрана 2021-05-31 в 11 30 58

@sobolevn sobolevn added the enhancement New feature or request label Jun 13, 2021
@sobolevn
Copy link
Member

So! I'm on it!

Let's see what we can possibly do here.

  1. Supports[] type can be variadic (we already have this for AssociatedType: https://github.com/dry-python/classes/blob/master/classes/contrib/mypy/classes_plugin.py#L56-L62) to receive any amount of associated types
  2. Supports[] uses nominal inheritance with mro tweaks, probably we can use this feature to model intersenctions

Let's say that we have this code:

from classes import typeclass, AssociatedType, Supports


class A(AssociatedType):
    ...

@typeclass(A)
def a(instance) -> str:
    ...

@a.instance(int)
def _a_int(instance: int) -> str:
    ...

@a.instance(str)
def _a_str(instance: str) -> str:
    ...

# =====

class B(AssociatedType):
    ...

@typeclass(B)
def b(instance) -> str:
    ...

@b.instance(int)
def _b_int(instance: int) -> str:
    ...

@b.instance(list)
def _b_list(instance: lost) -> str:
    ...

Next thing, we want a type that Supports[A, B], in our case it is just int, because str and list supports just A and B respectively.

This must work:

number: int
a: Supports[A] = number
b: Supports[B] = number
ab: Supports[A, B] = number

a = ab
b = ab

And this should be an error, because a and b does not support all ab features:

ab = a
ab = b

So, it feels like this should work:

class A: ...

class B: ...

class AB(A, B): ...

But, then we have a problem. Because we would have to track all types and their subtypes and inject these new intersection types everywhere.

I will need to think about other approaches.

@sobolevn
Copy link
Member

Looks like this might be a way to go:

from typing_extensions import Protocol

class A(Protocol):
    _classes_supports_A: 'A'


class B(Protocol):
    _classes_supports_B: 'B'


class AB(Protocol, A, B):
    ...


# This is an emulation of `@some.instance(Number)`
# Consider `Number` as the instance type we are going to work with.
class Number(object):
    _classes_supports_A: 'A'
    _classes_supports_B: 'B'

n: Number
a: A = n
b: B = n
ab: AB = n

a1: A = AB
# Incompatible types in assignment (expression has type "Type[AB]", variable has type "A")
b1: B = AB
# Incompatible types in assignment (expression has type "Type[AB]", variable has type "B")

@sobolevn
Copy link
Member

Ok, this is hard. I am going to release it later, not in the closest version.

@sobolevn
Copy link
Member

sobolevn commented Jun 18, 2021

Here's my attempt:

from typing_extensions import Protocol
from typing import Union, TypeVar
from classes import AssociatedType

_T = TypeVar('_T')

class Supports(Protocol[_T]):
    __classes_supports: _T

class A(AssociatedType):
    ...

class B(AssociatedType):
    ...

class C(AssociatedType):
    ...

# Real data types:
# It will be used in `@some.instance(HasA)`

class HasA(object):
    __classes_supports: A

class HasB(object):
    __classes_supports: B

class HasAB(object):
    __classes_supports: Union[A, B]

class HasABC(object):
    __classes_supports: Union[A, B, C]


a: HasA
b: HasB
ab: HasAB

sup_a: Supports[A]
sup_b: Supports[B]
sup_ab: Supports[Union[A, B]]

sup_a = a  # ok
sup_b = b  # ok
sup_ab = ab  # ok

sup_ab = a  # fail
sup_ab = b  # fail

sup_abc: Supports[Union[A, B, C]]
sup_ab = sup_abc  # fails, is it correct?

The only problem for me right now:

sup_abc: Supports[Union[A, B, C]]
sup_ab = sup_abc  # fails, is it correct?

If some typeclass supports A, B and C, it should be assignable to Supports[A, B] type.

I think that to achieve this I need to fallback to individual props 😞

@sobolevn
Copy link
Member

Ok, seems that this works:

from typing import Union, Generic, TypeVar

T = TypeVar('T', contravariant=True)

class Supports(Generic[T]):
    ...

class A:
    ...

class B:
    ...

def some_a(instance: Supports[A]):
    ...

def some_b(instance: Supports[B]):
    ...

def some_ab(instance: Supports[Union[A, B]]):
    ...

a: Supports[A]
b: Supports[B]
ab: Supports[Union[A, B]]

some_a(a)    # ok
some_a(b)    # incompatible type "Supports[B]"; expected "Supports[A]"
some_a(ab)   # ok

some_b(a)    # incompatible type "Supports[A]"; expected "Supports[B]"
some_b(b)    # ok
some_b(ab)   # ok

some_ab(a)   # incompatible type "Supports[A]"; expected "Supports[Union[A, B]]"
some_ab(b)   # incompatible type "Supports[B]"; expected "Supports[Union[A, B]]"
some_ab(ab)  # ok

a = ab       # ok
b = ab       # ok

ab = a       # error
ab = b       # error

My plan:

  1. Transform Supports[A, B] into Supports[Union[A, B]]
  2. When adding new Supports[] base type into mro, I will need to find existing instances and unify them into a single one
  3. Test all the things

@sobolevn
Copy link
Member

See #250

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Development

No branches or pull requests

2 participants