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

Consider adding generics support #8

Closed
sobolevn opened this issue Nov 25, 2019 · 10 comments · Fixed by #207
Closed

Consider adding generics support #8

sobolevn opened this issue Nov 25, 2019 · 10 comments · Fixed by #207
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@sobolevn
Copy link
Member

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.

@anatoly-scherbakov
Copy link

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]?

@sobolevn
Copy link
Member Author

sobolevn commented Nov 27, 2019

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])
...

@anatoly-scherbakov
Copy link

anatoly-scherbakov commented Nov 28, 2019

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.

@sobolevn
Copy link
Member Author

@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.

@anatoly-scherbakov
Copy link

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?

@MadcowD
Copy link

MadcowD commented Jan 6, 2020

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.

@sobolevn
Copy link
Member Author

sobolevn commented Jan 7, 2020

@MadcowD thanks! 👍

@MadcowD
Copy link

MadcowD commented Jan 16, 2020

(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 :)

@anatoly-scherbakov
Copy link

@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!

@sobolevn
Copy link
Member Author

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

Successfully merging a pull request may close this issue.

3 participants