Re: Union Class Types (was Union Type (singular) straw man proposal)

From: Date: Sun, 08 Sep 2019 18:19:49 +0000
Subject: Re: Union Class Types (was Union Type (singular) straw man proposal)
References: 1 2 3 4 5 6 7 8 9  Groups: php.internals 
Request: Send a blank email to internals+get-106906@lists.php.net to get a copy of this message
> On 9 Sep 2019, at 00:44, Mike Schinkel <mike@newclarity.net> wrote: > > Hi Stephen, > > Thank you for the follow up. > > I am heading out for a week-long conference later today and not sure if I will have time to > participate on the list for a while so I wanted to get a quick reply to you before I leave. > >> In terms of how I would see it working - > > In hindsight I should have been more clear what I meant when I said "working." > > A critical aspect of the proposal is the syntax for creating anonymous unions, e.g.: > process(Guid|string $guid) vs. always requiring GuidUnion to be > predefined before using it in a context like process(GuidUnion $guid). That is the part where I > cannot see interfaces working, unless they are created magically too. But it there really any point > to that? > >> For classes, I don’t know what the solution would be - and I’m honestly less clear on >> how much demand there is for that level of predictable, developer-controlled automatic casting >> anyway. > > I am getting that impression, at least from your response. > > That seems weird to me though given how many people on the list want a stricter language. To me > this seemed like the best of both worlds, but I do get it. > >> Are you suggesting ... where a function to handle a date/time may for example accept an >> integer or an instance of \DateTimeInterface and thus be given a variable of either type and need to >> do whatever appropriate checks to use it, instead it would set a type hint of e.g >> DateOrTimestamp which is a class defining that it’s either an int >> or \DateTimeInterface via the types keyword, and the original >> function would get an instance of that object, regardless of whichever type is passed, and the >> engine would automatically wrap/box the passed value in that class? > > Yes, you got it, except you did not mention the ability to create an anonymous union with > int|\ DateTimeInterface, i.e. using the vertical bar where a type > specifier can otherwise be used. > > So per the proposal this: > > function processGuid(string|Guid $guid) { > ... > } > > Would be equivalent to this, except the above union would not have an actual name that could be > reused elsewhere, just like anonymous classes don't: > > class GuidUnion { > types string|Guid; > } > function processGuid(GuidUnion $guid) { > ... > } > > >> The example you provided doesn’t quite make sense to me even in the above scenario, >> because it shows accepting a string scalar as a type, but then somehow that value is an object with >> methods, not a scalar? Is that a further misunderstanding on my point, or is that example meant to >> include a hypothetical GuidLike ‘union class’ which has >> types Guid|string in it?. > > If I understand your confusion I think you are confused because I was showing the use of an > anonymous union and the proposal proposes that a scalar passed to a function type hinted to be a > union would receive the scalar "boxed" into a union object. This means in the function you > access the desired scalar via accessor methods of the union object and not a scalar. > > Does that clarify? > >> Looking back at your original proposal’s examples, it becomes clearer with the above >> understanding (if that’s what you meant), but it also seems even _less_ intuitive to me now, even >> though I understand (well, I *think* I do) what you’re actually suggesting, and I still don’t >> really see the benefits you’re proposing. > > I am appreciating that it comes across as less intuitive. It is probably intuitive to me > because what I am proposing is (almost) exactly how *empty* interfaces work in GoLang and I have > been programming in my spare time in Go for a year now alongside my paid PHP duties. > > The question is, is this concept so unintuitive as to make it a non-starter for PHP, or is it > like many concepts in programming; unintuitive when you first see them but once you learn them > completely intuitive? > > That is a rhetorical question I cannot answer as a single individual; the arrive at an answer > requires the feedback of many others. > >> If a method accepts a parameter with a union type of >> GUID|string, yes you have to do some work to verify that the string >> is in fact a guid - possibly converting it into an instance of GUID along the way. > > Which is part of what the proposal is trying to standard and streamline as there is no way to > cast an object to a class in PHP, and thus no way to get a type-safe value as a known object. You > can do this: > > $local_guid = $guid instanceof \Guid ? $guid : null; > > But it seems ro me so much more elegant — and readable — if you could just do this: > > $local_guid = $guid->toGuid(); > >> If a method has a signature using a ‘Union Class type’ which has a >> types GUID|string keyword, you _still_ have to verify that the >> result of value() (or toString()) is in fact a valid GUID > > Yes, and no. In the proposal $guid->toGuid() would return null, so no need to have to > check if you can use the presence of null as an indicator. > > But if you did need to check that is why the proposal includes the $guid->type() method, > that works well with switch statements; something we do not > otherwise have in PHP because of the distinction between gettype() > vs. get_class(). The proposal's approach means less > potentially confusing boilerplate logic. > > As an aside, turning classes into first-class language elements would be super powerful and I > would really like to see that, but that feels like too big of an ask for the PHP community so I have > been thinking it would not be worth the effort to create such a proposal. > >> because the calling site has just given you a string, and the ‘magic’ has boxed that up >> into an object.. so you can call a method to get back the same value, right? > > Not sure I am following that question unless you are just confirming that when $guid->type() > === 'string' that is_string( $guid->toString()) is true? > > One benefit to this approach I realize my proposal did not emphasize as that of a union > becoming a first-class entity, with the ability to extend the base union when not using anonymous > unions, and to add to or modify its methods. This is something we won't get if we simply use > type aliases in Nikitia's proposal. > > But in hindsight maybe I am trying to propose types as first-class objects after all, using a > use-case where there would be real tangible benefits vs. the general case? > > What I think I am recognizing is that while this functionality works brilliantly in Go it may > not be of the nature that makes sense for the PHP world. > > Anyway, thank you again for following up. If I am unable to reply to any responses for the > next week+ it will be because I am focusing on the conference I will be attending. > > -Mike > > >> - because the calling site has just given you a string, and the ‘magic’ has boxed that >> up into an object.. so you can call a method to get back the same value, right? > > >> On Sep 8, 2019, at 9:56 AM, Stephen Reay <php-lists@koalephant.com> wrote: >> >> Hi Mike, >> >> Sorry for the delay responding to this. >> >> So I would agree that magic methods are generally a less-obvious solution, and interfaces >> are generally a better alternatives for new solutions. >> >> In terms of how I would see it working - the same way that implementing the >> Iterator (or IteratorAggregate) >> interfaces allows a class to be iterated using foreach, my thought (and im pretty sure I’ve seen a >> similar concept suggested by others on internals before too) was that e.g. when passing an object >> that implements the stringable interface to a type that expects a >> string, it would convert it to such, even in strict mode, without warning or error. The same could >> be used for any built in basic type (scalars and arrays). >> >> Heck, a Money class could implement >> stringable (return a formatted string), >> intable (return the amount in the minor currency unit - i.e. cents >> for most dollar currencies) and floatable (return an approximation >> of the amount as major.minor. >> >> Again - I know those names are not fantastic, but you get the idea. >> >> >> For classes, I don’t know what the solution would be - and I’m honestly less clear on >> how much demand there is for that level of predictable, developer-controlled automatic casting >> anyway. >> >> I’m almost certain I’ve thus-far missed some point of what you were trying to convey, >> but I _think_ I understand now… sort of. >> >> Are you suggesting that rather than (or as well as?), as per Nikita’s proposal, where a >> function to handle a date/time may for example accept an integer or an instance of >> \DateTimeInterface and thus be given a variable of either type and need to do whatever appropriate >> checks to use it, instead it would set a type hint of e.g >> DateOrTimestamp which is a class defining that it’s either an int >> or \DateTimeInterface via the types keyword, and the original >> function would get an instance of that object, regardless of whichever type is passed, and the >> engine would automatically wrap/box the passed value in that class? >> >> >> The example you provided doesn’t quite make sense to me even in the above scenario, >> because it shows accepting a string scalar as a type, but then somehow that value is an object with >> methods, not a scalar? Is that a further misunderstanding on my point, or is that example meant to >> include a hypothetical GuidLike ‘union class’ which has >> types Guid|string in it?. >> >> Looking back at your original proposal’s examples, it becomes clearer with the above >> understanding (if that’s what you meant), but it also seems even _less_ intuitive to me now, even >> though I understand (well, I *think* I do) what you’re actually suggesting, and I still don’t >> really see the benefits you’re proposing. >> >> If a method accepts a parameter with a union type of >> GUID|string, yes you have to do some work to verify that the string >> is in fact a guid - possibly converting it into an instance of GUID along the way. >> >> If a method has a signature using a ‘Union Class type’ which has a >> types GUID|string keyword, you _still_ have to verify that the >> result of value() (or toString()) is in fact a valid GUID - because the calling site has just given >> you a string, and the ‘magic’ has boxed that up into an object.. so you can call a method to get >> back the same value, right? >> >> >> >> Cheers >> >> Stephen >> > Hi Mike, Thanks for the confirmation/clarification. So let me start out by clarifying that what *I* was suggesting with unions is quite a different concept than you’re talking about. I was talking about allowing for ‘automatic’, ‘casting’ (in a way that isn't possible now except for strings) of objects when past to into a context where a simple (scalar|array) type is dictated. The end result would be the type specified (irrespective of it being a union or not in fact), and yes, I’m aware there’s potential ambiguous cases - what if the destination accepts string|int and the object supports both interfaces? Ok - the example given makes “sense” with the understanding that it’s alternative ‘short’ syntax - but that makes it yet another step further from ‘intuitive’ to me, compared with current type hints, and the extension to allow unions Nikita introduced - and if Im not mistaken it makes it incompatible too (i.e. you can’t have the behaviour Nikita’s RFC describes and what you describe with the ‘anonymous unions’ - they’re too very different approaches using the same syntax. I think the intuitiveness factor is easier to explain if you consider the case of the Number example - plenty of things will work exactly as you expect if you use either an int or a float. But then with your proposal suddenly you don’t have a scalar variable any more - yes I know you can call value() - part of the point of type hints is to reduce boilerplate with type checking arguments within the function body - and now you have to call a method to get a usable value anyway? The GUID example still confuses me in terms of how your example ‘helps’. If $guid->getGuid returns null (signifying it’s a string?) presumably I still want to do something with that string - otherwise I wouldn’t have accepted it as a valid type - at which point I might as well just do a instanceof or is_string call - but now I have to check if the value I was given is null, even if I didn’t specify the parameter as being nullable? I think (or I hope so at least) we’re at least both clear what the other is talking about now. Enjoy your conference! Cheers Stephen

« previous php.internals (#106906) next »