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

Split Result type into Ok and Err class #18

Closed
wants to merge 2 commits into from

Conversation

Emerentius
Copy link
Contributor

and make Result[T, E] a type alias for typing.Union[Ok[T], Err[E]].
This allows type safe access to the result members via isinstance
checks.

Implementation of #17 for discussion.

This is of course a big breaking change. I've switched the order of the T and E parameters to be in line with Rust. I haven't added the Ok() fallback for Ok[bool], yet. I'm a bit confused on why that was added, especially with a bool member but it can of course be added again.

The big upside of this way is type safety. A downside is that you can no longer do checks like isinstance(res, Result) because Result is no longer a real type. Consequently, if you want to have a Union[Result[T, E], U], it will be less ergonomic to go back to Result[T, E].

and make Result[T, E] a type alias for typing.Union[Ok[T], Err[E]].
This allows type safe access to the result members via isinstance
checks.
@dbrgn
Copy link
Member

dbrgn commented Oct 5, 2019

Nice, thanks!

Regarding Ok(), I think that was added to simplify the use case where a function without return value can fail.

However, I think that in the next version we should try to avoid magic, and be explicit in all decisions. Removing the default value (True right now) might be part of that.

A downside is that you can no longer do checks like isinstance(res, Result) because Result is no longer a real type.

Could we maybe add an empty BaseResult type or something like that, which the two result variants extend?

@dbrgn
Copy link
Member

dbrgn commented Oct 5, 2019

Btw, we should probably add a note to the README that this library is best enjoyed together with mypy and type annotations.

@Emerentius
Copy link
Contributor Author

Could we maybe add an empty BaseResult type or something like that, which the two result variants extend?

Yes, but you can already use a tuple to check for multiple types at once. isinstance(res, (Ok, Err)) would be the replacement for isinstance(res, Result).
A baseclass couldn't replace the Result alias because both Ok and Err are generic over one type and Result is generic over two. As far as I can see, it would be useful solely for isinstance, but could confuse people elsewhere. Someone might think you were supposed to use it for type annotations.
We could export an alias for (Ok, Err), like idk, Res or Result_. That may be useful, although it could also cause some confusion. If you tried to use it for type annotations it would throw an error at least. At least if and until PEP 585 is accepted.

@Emerentius
Copy link
Contributor Author

Emerentius commented Oct 6, 2019

Ok and Err should also probably be covariant so that Ok[Cat] is a subtype of Ok[Animal] if Cat is a subtype of Animal. I haven't found out how you can do that, yet.

As far as I can see, it isn't possible currently due to the restrictive way mypy treats variance. If you have a class covariant over T, you must never use T as an argument. python/mypy#734 is just about forbidding this exact use case.

@Emerentius
Copy link
Contributor Author

I believe the error is from an outdated mypy. I couldn't install a development environment from requirements-dev.txt, so I set up a fresh poetry project and used that to setup my dev environment with up-to-date versions. With mypy 0.730 there are no errors.

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling 6e72211 on Emerentius:type_safe_result into 0778597 on dbrgn:master.

@Emerentius
Copy link
Contributor Author

I think the most faithful way to model sum types is to create classes for each enum member like I've done here and then to wrap them into a class with just one attribute member. The wrapper class can then contain all the methods like unwrap_or, you can use isinstance(var, Result) and the equivalent of matching through isinstance(var.member, Ok).

This structure also allows nested enums. If you just had a union type alias for the enum Union[Every, Single, Member] (like in this PR currently), then nested enum types would always be flattened into one mega-enum: Union[Member, Union[SubEnumMember, SubEnumMember2]] == Union[Member, SubEnumMember, SubEnumMember2]

Example (using dataclass for brevity):

@dataclass
class Ok(Generic[T]):
    value: Final[T]

@dataclass
class Err(Generic[E]):
    value: Final[E]

@dataclass
class Result(Generic[T, E]):
    member: Union[Ok[T], Err[E]]

    def unwrap_or(self, value: T) -> T:
        if isinstance(self.member, Ok):
            return self.member.value
        else:
            return value

Downside: It's even more verbose

@dbrgn
Copy link
Member

dbrgn commented Mar 9, 2020

Replaced by #27.

Thanks @Emerentius for getting this started. If possible, I'll try to retain your co-ownership of the initial commit when merging the other PR 🙂

@dbrgn dbrgn closed this Mar 9, 2020
dbrgn added a commit that referenced this pull request Apr 15, 2020
This allows for much better type safety in code that uses results, since code can now use `isinstance` as a replacement for matching on the type of a result. See #17 for details. A migration guide has been provided.

Fixes #17, replaces #18.

Co-authored-by: Emerentius <emerentius@arcor.de>
Co-authored-by: francium <francium@users.noreply.github.com>
Co-authored-by: Danilo Bargen <mail@dbrgn.ch>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants