Re: Union Class Types (was Union Type (singular) straw man proposal)
| From: | Stephen Reay | 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