Skip to content

Consider adding generics support #8

Description

@sobolevn

We need to be able to work with generic like List[str] or Result[str, Exception] and Maybe[int]

I propose the following API:

@typeclass
def test(instance) -> int:
    ...

@test.instance(
    Some[int], 
    predicate=lambda instance: is_successful(instance) and isinstance(instance.unwrap(), int),
)
def _test_success_int(instance: Some[int]) -> int:
     return instance.unwrap()

It should be supported on both side: types and runtime.

Activity

  1. anatoly-scherbakov commented on Nov 27, 2019

    @anatoly-scherbakov

    I am not certain the use case is entirely clear. Does Some container not represent separate types for a value versus no value inside so that your example from #126 can't be used?

    Regarding List[str], the predicate will have to check isinstance(instance[0]) - or how would it distingiuish them since, as far as I understand, instance.__args__ will be empty due to type erasure even if one of value's parent classes was List[str]?

  2. sobolevn commented on Nov 27, 2019

    @sobolevn
    MemberAuthor

    List[str] can be checked as all(isinstance(item, str) for item in some_list)

    The usecase I have in mind is:

    @some.instance(Some[FirstType])
    ...
    
    @some.instance(Some[SecondType])
    ...
    
    @some.instance(Some[ThirdType])
    ...
  3. anatoly-scherbakov commented on Nov 28, 2019

    @anatoly-scherbakov

    Understood. I am all for Some[TypeA] vs Some[TypeB] selectors. However, I wonder if it would be possible to write them just like you just did, without unrwap() in a predicate, which exposes the internals of the container and ruins the charm of declarativity.

    I could propose an implementation for that as follows.

    First of all, Some, Result and any other containers which expect to be used this way should define __class_getitem__, which on invocation subclasses the container class. Like this:

    class Success(...):
        def __class_getitem__(cls, value):
            if blablabla:
                return type(
                    f'{cls.__name__}[{value.__name__}]',
                    (cls, ),
                    {
                        '_contained_type': value
                    }
                )

    (I know Success is not a class, it's just an example). This is actually preventing type erasure. Of course there should be a cache of subclass instances created like that. See python/typing#400 and python/typing#79 - one of them says creating a new subclass makes a piece of code 250x slower; let's minimize instances of this.

    However,

    >>> Success[str]._contained_type
    <type 'str'>

    Now, let's define an instance method:

    class Success(...):
        def wrapped_type(self) -> type:
            return type(self._inner_value)

    This means that:

    >>> Success(5).wrapped_type()
    <type 'int'>

    Why is all this needed anyway? That's why.

    class Success(...):
        def _instancecheck_(cls, instance):
            return isinstance(instance, Success) and (
                instance.wrapped_type() == self._contained_type
                or self._contained_type is None
            )
    
    >>> isintance(Success(5), Success[int])
    True
    
    >>> isintance(Success(5), Success[str])
    False
    
    >>> isinstance(Success(5), Success)
    True

    This probably means the implementation for containers in classes would be no different from implementation for other classes. This is of course not generalizeable - you need to heavily modify your class for it to work. For the classes you cannot modify probably the predicate will have to be used.

    P. S. all(isinstance(item, str) for item in some_list), while theoretically making sure the list indeed contains only strings, is to my opinion definitely intolerable in any production code due to its O(N) time complexity.

  4. sobolevn commented on Nov 28, 2019

    @sobolevn
    MemberAuthor

    @anatoly-scherbakov great idea! So, we can provide a protocol for other generics to follow.
    And modify our own types to support this protocol.

    And possibly leave predicate= for people who need generics without this protocol support.

  5. anatoly-scherbakov commented on Nov 28, 2019

    @anatoly-scherbakov

    Thanks.

    I have stumbled across __class_getitem__ override because I'd like to use generic type arguments at runtime; I ended up with https://github.com/anatoly-scherbakov/platonic/blob/master/platonic/platonic/model.py#L44

    I've come to believe that fixing runtime type erasure is a valid and sufficiently generic use case to be implemented as an independent component useful in many contexts. As per my links above, however, calls like

    def very_slow_stuff():
        v = Mapping[int](5)

    ... would be quite undesirable and should be avoided. But, if used in inheritance context - they would only slow down the startup of the application and provide a neat interface to type specification which is also well supported by static analysis. Example:

    class PeopleGraph(Graph[Person, Friendship]):
        ...

    looks more pythonic and native than

    class PeopleGraph(Graph):
        node_type = Person
        edge_type = Friendship

    Thoughts?

  6. MadcowD commented on Jan 6, 2020

    @MadcowD

    Hey! Don't know how I ended up here, but actually Mapping[int]() shouldn't be slow any more given that there is type caching.

  7. sobolevn commented on Jan 7, 2020

    @sobolevn
    MemberAuthor

    @MadcowD thanks! 👍

  8. MadcowD commented on Jan 16, 2020

    @MadcowD

    (Be careful with isinstance calls as well). Lastly, you might consider implementing you're own generic's system which doesn't do class instantiation, but rather acts as a partial function on an object constructor:

    class Meta(type):
    
        def __getitem__(cls, typin):
              def partial(*args, **kwargs):
                  return cls(typin, *args, **kwargs)
              partial.__doc__ = cls.__doc__ 
              # Do other things here
              return partial
    
    class TypeSpecificClass(metaclass=Meta):
        def __init__(self, mytype, someotherarg, somekwarg=0):
            assert isinstance(mytype, type), "Must instantiate with a type."
            self._type = mytype
    
    # Usage
    intlabeledobj = TypeSpecificClass[int]("hi", somekwarg=1)

    This is honestly so much faster than using the typing library (mainly for isinstance resolution). You can also extend this __getitem__ override to work with isinstance.

    Good luck :)

  9. anatoly-scherbakov commented on Jan 17, 2020

    @anatoly-scherbakov

    @MadcowD it seems to me like SomeClass[something] can improve expressiveness and readability of code in some cases, not only to assist type system.

    I am using a cache to avoid re-creating new subclasses every time, but this idea of yours is an interesting alternative. Thanks!

  10. added a commit that references this issue on Jun 3, 2021
    3b0885c
  11. added a commit that references this issue on Jun 11, 2021
    f1ac13b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions