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

[Feature Request] Contravariant generics (if feasible, but maybe infeasible) #187

Open
rtbs-dev opened this issue Nov 17, 2022 · 13 comments

Comments

@rtbs-dev
Copy link

So there I was, happily humming a tune while beartype slays the typing demons of type-tetris' past... and, suddenly:

import beartype.door.TypeHint as th
import beartype.typing as bt

E_ID = bt.Union[int,str]  # Entity ID's 
DataType = bt.TypeVar('DataType', bound=bt.Optional[bt.Iterable], contravariant=True)
Entity = bt.Tuple[E_ID, DataType]  # So (valid_id, (some_data1, some_data2, ...))

OK, now add some spice...

class Fake(bt.NamedTuple):
    name:str
    kind:str
    velocity:float

MyFakeEntity = Entity[Fake]

and test them all:

(th(Entity).is_bearable(('hi',(2,3,4))),  # this should work
 th(Entity).is_bearable((1.20,(2,3,4))),  # 1.2 is not an E_ID
 th(Entity).is_bearable((0,None)))        # Entity data is optional
 th(MyFakeEntity).is_bearable((0,Fake('n1', 'bob',2.5))),  # is a "fake"
 th(MyFakeEntity).is_bearable((0,('n1', 'bob',2.5)))       # technically not a "fake"
)

(True, False, True, False, False)

... wait a minute...?

Obviously without the contravariant=True I get a True, False, as expected. But in my case, entities are just consumers of their datatype, like "tagging protocols". So if data iterable type B is a subclass of iterable type A, and I can tag arbitrary iterables with EntityA, and only special Fake iterables with EntityB, EntityA should be allowed anywhere EntityB is expected (EntityA is a subtype of EntityB).

All this is to say, I did a quick ctrl+F on that glorious readme of yours, and lo-and-behold, contravariant is mentioned... in the table as "not supported"! 😅 And... I didn't see any open issues for the feature request. If this isn't a good place for that, let me know!

@leycec
Copy link
Member

leycec commented Nov 18, 2022

Glorious request is glorious! Yes, contravariant type variables is absolutely in @beartype's wheelhouse, which increasingly resembles an API pigsty. Let's add this to the back burner with firm and resolute promises that we will do something about this, we solemnly swear it, even as we wring our hands in supplication towards the increasingly unruly unwashed masses yearning to type free userbase.

Oh – and the fourth item of your tuple of is_bearable() return booleans should be True, right? At least under Python 3.11, this is what I get:

(True, False, True, True, False)

Is that right?

Hidden Horrors Lurk off the Long-Forgotten Shores

Lastly, we need to discuss the disgustingly huge alien megastructure hiding in the room.

Thanks to the fact that PEP 484 and thus PEP 484-compliant type variables were never intended to actually be used by anyone at runtime, this may not be feasible. Specifically, subscripting a type hint subscripted by one or more type variables destructively replaces those type variables with their substituted type hints at runtime. The original type variables cannot be recovered from those type hints at runtime, because those type variables have been replaced by the typing module (or low-level C machinery) by the eleventh hour that @beartype finally sees those type hints.

That is to say, the type hint MyFakeEntity = Entity[Fake] destructively replaced the original type variable DataType that subscripted the original Entity type hint with the named tuple Fake. When you pass MyFakeEntity to the beartype.door.TypeHint constructor, that constructor only sees the new MyFakeEntity type hint with no reference to either the original Entity type hint or the original type variable DataType that subscripted the original Entity type hint. For example, given the above definitions:

>>> MyFakeEntity.__args__
(typing.Union[int, str], <class '__main__.Fake'>)  # <-- uhohs. "DataType" is gone, bro. EVERYTHING. IS. LOST.
>>> MyFakeEntity.__parameters__
()  # <-- ...oh my Gods.

I consider this a typing bug. The current typing implementation is preventing runtime type-checkers from implementing support for contravariant generics, because typing is destroying essential metadata runtime type-checkers require to implement that support.

I acknowledge that this a tall ask. But... would you mind opening a discussion on Python's official Typing-sig mailing list about this? That's where all high-level discussion on typing improvements happens. I would do this for you, except I am lazy and currently role-playing as the benevolent Star Trek Federation in Stellaris. The future of the Federation isn't looking well, friends. The Ferengi trade empire hounds my tissue-paper-thin borders and now a Klingon warship just uncloaked outside my fully unprotected Starbase at a planet I foolishly dubbed "Memory Alpha."

Weep for me! My citizenry already do.

@leycec leycec changed the title DOOR support for Contravariant Generics [Feature Request] Contravariant generics (if feasible, but maybe infeasible) Nov 18, 2022
@rtbs-dev
Copy link
Author

this may not be feasible. Specifically, subscripting a type hint subscripted by one or more type variables destructively replaces those type variables with their substituted type hints at runtime.

great scott

Damnit, python, ya done me dirty again.

Wellp. I wouldn't blame you to ignore this entirely, then! Not going to lie, the list of reasons to abandon the "lets me do anything" python land, which began with a lovely debate about HKTs, is growing ever longer.

if I see something python says "no" to for some damn reason...one more time...

But hey, I guess life isn't as fun without Sisyphusian kicking against the proverbial goads. (unless, wait, is that just called masochism?

So, as per my usual, is there another way...?

I'm starting to think that user-derived types, similar to how you are starting to support NamedTuple, dataclass, etc, are a possibility to double down on. Perhaps there is room to "bless" certain types, especially user-created ones, with enhanced capabilities like contravariance, tagged unions, etc.

You already have the machinery, right? These beautiful TypeHint objects. With a proper decorator, we can permanently attach metadata that informs beartype hey, this thing is an X, trust me.

This means beartype can know or easily infer at run time the type of pretty much any "thing" that is or was derived from these special "blessed" types. And boom, we've kinda invented Algebraic BearTypes.

I know this is sort've sounding like the tack that pydantic took, but rather than rely on a basemodel and focus on validation, we can maybe use relatively straight-fwd protocols to cleanly endow types with BearTypeable (or something). An outer beartype check makes sure that if we're trying to do something advanced, it at least passes an is_bearable beartypable check. Plus, the protocol approach means we can provide machinery for folks to add beartyp-ability to whatever-they-want™, and then just let them deal with the fallout™ of such decisions!

Also, I'm pretty sure any types we can "assign" a priori to get a "TypeHintBadge" as metadata wouldn't need anything more than a subhint check at runtime, which is...faster? maybe? than full beartype type checking of a complicated nested type?

So then the questions are 1) am I needlessly reinventing, what, GHC as a rebellious fiefdom in the kingdom of python, and should pack up my toys and go to a new sandbox? 😆
2) should I add specific examples to the mailing list and/or how do I make this sound less like ramblings of a type-obsessed heretic?
3) should I start my first game as humans, or can I jump right into play my new obsession the Scyldari, and...can I quickly scale this learning curve so I can recapture the feeling of Sins of a Solar Empire or M2TW without wasting 50 hours playing wrong?

No joke, I booted up Stellaris for the first time from an old humble Bundle yesterday, after getting my steam deck in the mail. So glad you brought it up, and now I'm going to give it another go instead of losing myself to Factorio again for the 10th time.

@leycec
Copy link
Member

leycec commented Nov 19, 2022

heh heh heh

i see your shades and raise you more shades

more shades

Perhaps there is room to "bless" certain types...

Perl 5 intensifies. Inb4 @tbsexton publishes a bless package providing a single public bless() function dynamically promoting standard unholy profane types into "blessed" types. Guido bless you, bless.

You already have the machinery, right? These beautiful TypeHint objects. With a proper decorator, we can permanently attach metadata that informs beartype hey, this thing is an X, trust me.

You walk the dark plugin roads – and you walk them alone. Currently, @beartype lacks a plugin API. This is a thing we routinely think about until remembering that we lack this precious thing called time other sentient beings claim without evidence they have.

beartype.vale.IsParametrization: Because We Can

That said, in the absence of official typing support for exposing metadata on contravariant and covariant generics to runtime type-checkers, @beartype desperately cries out for an official @beartype-specific solution. As always, the optimal solution may be for us to officially add a new beartype validator enabling typonistas like us to attach this metadata to existing type hints.

I contemplate a dread API resembling:

# Old stuff copy-pasted from above. Boring!
from beartype.door import TypeHint as th
from beartype import typing as bt

E_ID = bt.Union[int,str]  # Entity ID's 
DataType = bt.TypeVar('DataType', bound=bt.Optional[bt.Iterable], contravariant=True)
Entity = bt.Tuple[E_ID, DataType]  # So (valid_id, (some_data1, some_data2, ...))

class Fake(bt.NamedTuple):
    name:str
    kind:str
    velocity:float

# NEW STUFF IS NEW. Excitement ensues as madness prevails.
from beartype.typing import Annotated
from beartype.vale import IsParametrization

# PEP-compliant type hint notifying @beartype that "Entity[Fake]" is
# the result of substituting one or more type variables subscripting
# the "Entity" type hint with concrete types satisfying those variables,
# while preserving compatibility with static type-checkers and IDEs.
#
# @beartype will then dynamically do the rest. Specifically, by the
# dark power of introspection, @beartype will iteratively do a diff
# between "Entity[Fake]" and "Entity" to find the type variables in the
# latter replaced by types in the former. Doing so then enables
# @beartype to validate that these types either do or do not satisfy
# the contravariance or covariance of these type variables.
#
# Chortle with glee as @beartype does something.
MyUsableFakeEntity = Annotated[Entity[Fake], IsParametrization[Entity]]

BearTypeable: We Go Where We Shouldn't

In the absence of collective sanity, a hypothetical class BearTypeable(typing.Protocol) superclass also intrigues even as it disturbs me. When you squint until your forehead crinkles, that looks like an extensible plugin API. Weekend permitting, would you mind speccing out what a proposed beartype.door.Beartypeable API might look like for Old Man @leycec over here?

If your first-draft stab in the darkness successfully results in a wet, meaty sound indicating that something unmentionable was hit and is now pathetically bleeding out on the dungeon floor, perhaps we could begin running the PR gauntlet with... something. Since your galactic-tier intelligence clearly exceeds that of my own, I leave everything in your capable clutches.

Treat @beartype gently. It is tired and getting old.

should I add specific examples to the mailing list and/or how do I make this sound less like ramblings of a type-obsessed heretic?

...actually, your wise words make me reconsider that risky ploy. Let's perhaps ignore Typing-sig for the moment. Official Python devs frighten me. It's a rough crowd and I'm kinda too sleepy to defend myself in a heated scrum of fifty brawling typing purists.

They usually see runtime as an afterthought. That's an uphill dogpile. Cue Sisyphus and his rolling rock.

should I start my first game as humans

Yes, yes, yes! By all the ancient leviathans that plumb the empty depths of space, yes!

There are white-knight humans and there are bad-seed humans. The white-knight humans replicate the plucky happy-go-lucky Star Trek Federation. The bad-seed humans replicate the disgustingly "so bad they're good" Terran Empire from the Star Trek mirror universe. Either way, you really can't go wrong with any species.

It's Stellaris, after all – the impossibly complex 4X you never quite understand where mechanical victory is secondary only to your having nebulous fun while role-playing out the vicious childhood fantasies of galactic domination you always knew was your rightful inheritance.

can I quickly scale this learning curve

That... may not happen. Thankfully, the joy of Stellaris is learning the joy of Stellaris while failing your homeworld's citizenry in a nuclear conflagration of forced assimilation at the underhanded probes of the nearby machine culture you thought you could trust. You were wrong.

Specifically, I advise while stroking my suspicious goatee:

  • Play the tutorial. Seriously. It's pretty good! It also starts you off as the white-knight humans, with whom you can just quietly proceed after the tutorial ends. Let's go, utopian Federation of culturally diverse and socially appropriate good guys!
  • Enable the StarTech or StarNet AI mods, both by the same mod dev. StarTech for a more flavourful cerebral experience; StarNet for more gut-wrenching militancy and bloated corpses spilling out of breached airlocks. The only core feature that Stellaris still lacks in 2022 is cutthroat AI. Enter StarTech and StarNet, stage left. (Reportedly, the upcoming Stellaris 3.6 bump also significantly enhances AI. I for one welcome our new AI neighbours with open blasters.)
  • Enable Ironman mode. Chuckles as quiet screaming fills the void.
  • Increase difficulty to at least Captain. More screaming. More chuckling.

cry about it

@langfield
Copy link

langfield commented Nov 19, 2022

@tbsexton Are you running a prerelease version of @beartype? I read this thread and was surprised to see this bt.NamedTuple use because I thought these weren't deeply checked yet. But then, I see that the readme says 0.12.0 for deep type checking of NamedTuple.

I assume you've installed from main or something?

I'm just curious because I currently use dataclasses whenever I need a typechecked algebraic data type, and I am hoping that using NamedTuples will be faster and require less boilerplate (you can use tuple unpacking and you don't need to add an @dataclass(frozen=True, eq=True) on every definition.

Somewhat along the lines of covariant v. contravariant generics, I'm curious if there will ever be a flag-type-thing for strictly checking not-necessarily-generics, i.e. something that behaves like type(x) == str instead of isinstance(x, str). I've found myself doing a lot of work with Lark lately, and the raw output of the parser gives you values of type Token(str), and quite often I find myself thinking "this function really ought to take as an argument some str that is not a Token."

Edit. Actually, now that I think about it, there's already a pretty good way to do this. Namely to define something like:

class Str(str):
    """A parsed string value."""

and then box the parsed str values in Str, type hint all strings in the post-parse-processor with Str, and then cast back to str at the end, when all the postprocessing is finished and you're guaranteed not to have to deal with any more Token arguments.

@rtbs-dev
Copy link
Author

@langfield So you're right, they aren't (yet) deeply typechecked in the released version (though this info is becoming out of date rapidly, I belive). BUT I discovered that the DOOR api actually does deeply typecheck comparable TypeHint objects, really well. Ergo this issue, where I want to proxy the checking of nested types to checking nested TypeHints in cases that such knowledge is avaliable to us a priori! 😄

As for your question about NamedTuple, my understanding is based from this nice answer which you should absolutely check out!:

When your data structure needs to/can be immutable, hashable, iterable, unpackable, comparable then you can use NamedTuple. If you need something more complicated, for example, a possibility of inheritance for your data structure then use Dataclass.

Other great reading for the kinds of things you'd be interested is from a fantastic dev of the bidict package, where they share lessons learned with us:

To get the performance benefits, intrinsic sortability, etc. of namedtuple() while customizing behavior, state, API, etc., you can subclass a namedtuple() class. (Make sure to include slots = (), if you want to keep the associated performance benefits – see the section about slots above.)

As an aside here, I love to see a Lark person in the wild! I spent a lot of time digging into the parsley library recently, but I almost went the Lark route and read a lot of the documentation 😅. If you're looking for something like NamedTuples but catered toward ADTs and immutability/memory usage, please please please check out coconut-lang! I love coconut so much. The documentation mentions their custom data keyword

used to create immutable, algebraic data types, including built-in support for destructuring pattern-matching and fmap

I had way more fun writing my parser in coconut with pattern matching and ADTs than I did with plain python!

Lastly, you said

I'm curious if there will ever be a flag-type-thing for strictly checking not-necessarily-generics, i.e. something that behaves like type(x) == str instead of isinstance(x, str)

And go on to do a wrapper class. There's a couple options here, but the core is to make use of predicates on that same wrapped class you show.

  • phantom-types, which was pretty much purpose-built for this exact use-case. Note their first example is a str wrap like you have, but the wrap is gone at runtime (type is still str, but isinstance goes to some custom wrapper). @leycec I'm realizing is implementing pretty much exactly what I was poorly approximating in the BearTyped plugin discussion, and I will probably explore what exactly the overlap is in that discussion, since the phantom-types readme even mentions beartype interop 👀
  • This is also, sort of, the point of typing.Annotated, since you get the same type, but can use arbitrary metadata at runtime. This is how the beartype validator API works, which tbh is another way you could go here. Read through that and maybe the PEP for annotated types, which hopefully helps, but e.g. if you want something that acts like type(x)==str but is a Token, then you could use beartype like this: TokenHint = Annotated[str, IsInstance[Token]], I think...? The downside is that static checkers completely ignore these annotations, which is another benefit of phantom-types that have the same predicates like beartype.vale.Is but magically give mypy that info too. (@leycec another reason we should maybe dig into that project's source code... a bit...?)

@langfield
Copy link

As for your question about NamedTuple, my understanding is based from this nice answer which you should absolutely check out!:

When your data structure needs to/can be immutable, hashable, iterable, unpackable, comparable then you can use NamedTuple. If you need something more complicated, for example, a possibility of inheritance for your data structure then use Dataclass.

This is super helpful to know! I bet @dataclass(frozen=True, eq=True) is the most common line of code across all my projects, so I really ought to be using NamedTuples. I can't wait until @beartype can deeply check them in 0.12.0!

phantom-types, which was pretty much purpose-built for this exact use-case. Note their first example is a str wrap like you have, but the wrap is gone at runtime (type is still str, but isinstance goes to some custom wrapper). @leycec I'm realizing is implementing pretty much exactly what I was poorly approximating in the BearTyped plugin discussion, and I will probably explore what exactly the overlap is in that discussion, since the phantom-types readme even mentions beartype interop eyes

Wow, this looks sweet! I'll be trying this out soon.

As an aside here, I love to see a Lark person in the wild! I spent a lot of time digging into the parsley library recently, but I almost went the Lark route and read a lot of the documentation sweat_smile. If you're looking for something like NamedTuples but catered toward ADTs and immutability/memory usage, please please please check out coconut-lang! I love coconut so much. The documentation mentions their custom data keyword

used to create immutable, algebraic data types, including built-in support for destructuring pattern-matching and fmap

Neat stuff! Yes I suppose I have become a Lark person, but under duress. I'd really rather be known as an attoparsec person, lol. I've been looking into trying to get a lot of what I'm missing from Haskell in Python. Unfortunately, I think something that compiles to python is just a bit too far away for me. I've been looking at using fn.py in my current project, though. I use so many damn lambdas and partial() calls that the underscore syntax and currying is starting to look really desirable.

@leycec
Copy link
Member

leycec commented Nov 24, 2022

@tbsexton Are you running a prerelease version of @beartype

...ohnoes. So sorry about that, @langfield! Everything's mostly ready to go for beartype 0.12.0 – including deep type-checking support for typing.NamedTuple that will surely shatter our understanding of QA as we know it. I just need to actually sit still for two whole minutes and pull the trigger on the changelog.

Sweat is beading my bald head over here. 😓

I spent a lot of time digging into the parsley library recently...

This is the way, because Parser Expression Grammars (PEGs) are the way. I'm a simple parsing man. I see PEGs. I star on GitHub.

Technically, there is one well-known CFG that has no equivalent PEG. Pragmatically, PEGs can do everything and more (and more importantly, much faster) than CFGs. There is a reason Guido transitioned Python to a PEG-based parser. Packrat parsing for life! Flex that determinism, folks. 💪

please please please check out coconut-lang!

What is this sanity-shredding oblivion that I see: "Coconut provides a recursive decorator to perform tail recursion optimization on a function written in a tail-recursive style..."

wut

It's tail recursion in Python, everybody! The prophesied Golden Age has finally arrived.

The downside is that static checkers completely ignore these annotations, which is another benefit of phantom-types that have the same predicates like beartype.vale.Is but magically give mypy that info too.

Gah! Please someone: laboriously crawl over the phantom-types codebase and educate me. I am ignorant and couldn't decipher the relevant hieroglyphics out of @antonagestam's phenomenal work there. I descry suspicious generics like the phantom.sized.NonEmpty protocol – but then I'm lost. This must explain why brain shrinkage is detrimental to coding.

Thanks for the thrilling discourse that is opening my eyes to a whole new typing world, @tbsexton and @langfield. @beartype bros the best bros. 🫂

@antonagestam
Copy link

👋

The requirements you mention in this thread, ie compatibility with both static and runtime checkers is exactly what cornered me into the design of phantom-types. I've written a blog post explaining how the core of phantom-types work, if you want to dig into the details: https://www.agest.am/phantom-types-in-python

The real implementation is just a generalization of the technique described there.

@langfield
Copy link

langfield commented Nov 25, 2022

Hey @antonagestam! Incredible project! I've really never seen anything like it.

I've got a question on an example:

By introducing a phantom type we can define a pre-condition for a function argument.

from phantom import Phantom
from phantom.predicates.collection import contained


class Name(str, Phantom, predicate=contained({"Jane", "Joe"})):
    ...


def greet(name: Name):
    print(f"Hello {name}!")

Now this will be a valid call.

greet(Name.parse("Jane"))

... and so will this.

joe = "Joe"
assert isinstance(joe, Name)
greet(joe)

But this will yield a static type checking error.

greet("bird")

So I imagine that if we call greet("Joe"), it will also yield a static type checking error. Will this yield a runtime type checking error? I am curious when the predicate gets evaluated other than when we call parse().

@antonagestam
Copy link

@langfield The phantom type doesn't do anything magical to the function signature that it is used in, so it won't make any runtime errors happen for calls to the greet function. Just like this won't produce a runtime error ...

def fn(v: int) -> None: ...
fn("foo")

... neither will this.

def fn(name: Name) -> None: ...
fn("not a name")

However, if you combine it with a runtime type checker, like beartype, you would see a runtime error, i.e. like the invocation in this example below.

greet = beartype(greet)
greet("not a name")  # <-- runtime error
greet("Joe")  # <-- no runtime error

Just like you mention though, calling greet("Joe") would give a static type checker error, because even though the value is valid, we haven't proved that it is valid. Perhaps the README example could be more clear about that. With that same greet function from the README, you get static type errors in the first invocation in the example below, but not the second.

name = "Joe"

# Even though "Joe" is a valid value, the type checker hasn't
# been presented with proof to back that claim yet, and so infers
# just `str` for name at this point. This call gives a static type
# checker error.
greet(name)

# Now we're presenting proof. mypy understands that the program
# cannot possibly continue unless name is a Name, and so happily
# narrows name from str to Name for any line of code following
# this assertion.
assert isinstance(name, Name)

# And so, with the presented proof, the type checker finds this
# call valid.
greet(name)

So that's the trade-off really, the burden to prove validity is pushed to the call site. However, because Python also has support for Literal types, there are things we can do to make the Name type even more user friendly, as long as you're willing to spend even more time crafting the type (of course you are, you're static type vigilantes, right?!).

So we have a set of values that the input must adhere to, we can change so that we define those as a Literal instead. This, you guessed it, will be used to recognize literal values in the code-base.

from typing import Literal
LiteralName = Literal["Joe", "Jane"]

Now we can update the definition of Name so that its source of truth is the arguments of this literal type. We change the name of the phantom type, and make Name the union of the phantom and the literal type. That's a mouthful. Let's try it.

from typing import get_args, Literal, TypeAlias
from phantom import Phantom
from phantom.predicates.collection import contained

LiteralName = Literal["Joe", "Jane"]

class ParsedName(
    str,
    Phantom,
    predicate=contained(get_args(LiteralName)),
):
    ...

Name: TypeAlias = LiteralName | ParsedName

What do we end up? There are good things and slightly less good things. The cool thing is, mypy now understands which literals are valid Names, so it allows this:

def greet(name: Name) -> ...: ...

greet("Joe")  # valid!
greet("Jane")  # valid!
greet("who?")  # not valid!

The slightly less bad thing is that Name now isn't compatible with isinstance() any longer, so to call greet with a non-literal value, we need to use the underlying phantom type, e.g.:

def greet(name: Name) -> ...: ...

val = read_from_somewhere()
assert isinstance(val, ParsedName)
# or
val = ParsedName.parse(read_from_somewhere())

# this would pass with one of the two proofs above
greet(val)

I do believe that Pydantic would be able to figure out usage of the union though, to be honest I'm not sure if it'd work with beartype, but I think it should? So if your source of parsed values is a typing-compatible validation framework like Pydantic, you'd likely not ever have to use explicitly use the underlying phantom type.

In the library, the CountryCode type uses this elaborate union-of-a-literal-and-a-phantom trick so you can play around with that if you'd like.

I guess this was sort of off-topic, hope it's at least tangentially relevant for this discussion ;)

@langfield
Copy link

The phantom type doesn't do anything magical to the function signature that it is used in, so it won't make any runtime errors happen for calls to the greet function.

Apologies, yes! Makes sense. I meant with the use of @beartype. My bad entirely.

The slightly less bad thing is that Name now isn't compatible with isinstance() any longer, so to call greet with a non-literal value, we need to use the underlying phantom type

It will work with beartype.door.is_bearable, though! If only static type checkers could use is_bearable for narrowing, haha!

I do believe that Pydantic would be able to figure out usage of the union though, to be honest I'm not sure if it'd work with beartype, but I think it should?

I do think @leycec supports this currently.

I guess this was sort of off-topic, hope it's at least tangentially relevant for this discussion ;)

No, no, I think it's fair to say we're all pretty deeply interested in this stuff! Thanks for the exposition! 😁

@leycec
Copy link
Member

leycec commented Nov 26, 2022

Fascinating discussion continues fascinating. Thankfully, @beartype has everyone covered here; we deeply type-check arbitrary combinations of both typing.Literal[...] and typing.Union[...], which | syntactically reduces to. @beartype implicitly supports everything above with no batteries required.

I'm also crossing my age-wizened fingers behind my back, which now itches suspiciously.

It will work with beartype.door.is_bearable, though! If only static type checkers could use is_bearable for narrowing, haha!

unbeatable

@rtbs-dev
Copy link
Author

This is very cool. @leycec I feel like everyone's use of typing.Literal is the closest thing in python we have to runtime tagged-unions, further evidenced by the fact that mypy's own documentation (and pydantic's) shows tagged/discriminated unions via Literal tags.

Which got me thinking...

If I'm understanding the beartype signs correctly, every typehint has a unique sign that can be represented as a unique str.

so?

Well...could we hack together a true sum type that MyPy, phantom-types, etc. could understand, by proxying a MySum = Sum[T1, T2, T3] as having an __instance_check__ against Literal[sign(T1), sign(T2), sign(T3)]??? This should theoretically mean that a type annotated with x: MySum, but at runtime has a sign of sign(T2), should automatically get narrowed as type(x) is T2 right?? That would be... awesome.

muahaha

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

No branches or pull requests

4 participants