On Sat, Sep 7, 2024, at 15:28, Larry Garfield wrote: > On Fri, Sep 6, 2024, at 7:46 PM, Davey Shafik wrote: > > > My main struggle with this is readability. As much as I want custom > > types (and type aliases is a good chunk of the way there), the main > > issue I have is understanding what the valid inputs are: > > > > function foo(Status $string): void { } > > > > How do I know that Status is a) not a class, b) that I can fulfill the > > requirement with a string, and/or maybe any object with __toString(), > > or maybe it’s ints? Or objects or enums? > > > > Even with file-local aliases (which I would definitely prefer to avoid) > > we will most likely rely on developer tooling (e.g. IDEs and static > > analyzers) to inform the developer what the right input types are. > > > > I would very much prefer to either go all in with an Enum-like (which > > means that we can hang methods on to the value) or we need to > > distinguish between type hints for class-likes and type hints for > > not-class-likes (*Bar anyone?). > > > > Expanding on type-class-likes: within the type methods, $this->value > > would refer to the original value, any operators would follow the > > _same_ rules as either the original values type (e.g. $int = 4; $string > > = “foo”; $int . $string == “4foo", or call __toString() in all the > > normal places for strings if defined). > > > > > > type Stringable: string|int { > > public function __toString(): string > > { > > return (string) $this->value; // original value > > } > > > > // Add Stringable methods here > > }. > > Methods on typedefs was the sort of "other stuff classes do" that I was > trying to avoid getting into right now. :-) Mainly because I can totally see > how it's tempting, but also have no idea what it would mean from a > type-theoretic perspective. It would only make sense if we're talking type > DEFs, not type ALIASes. I'm not against that, and it could be fun to try and > think through the type theoretical implications, but I don't think that's > what Rob was going for so I didn't want to take that side quest just yet. > (Though if he's OK with it, I'm OK with it.)
To be fair, I should probably mention that I've already explored it some (which I alluded to in another thread a couple of weeks ago). 😉 So... I guess it is 'on-topic' for no other reason than it is interesting. Here are my notes thus far: __Sugary Generics__ Since we could attach behaviors (at least at the engine level) we could use this to implement generics. Imagine we have a Box<T> and want to instantiate a Box<int|float>. To do this, when we define a Box<T>, we actually define an alias internally. This is what the definition of our generic box class in PHP might look like: class Box<T> { public T $var; } It would get compiled to something like this (though it would probably be invalid php, it perhaps illustrates my meaning): class Box { public BoxT $var; public function __construct(private alias BoxT) {} } Then, to instantiate a Box<int|float >, the engine compiles the constructor, passing the types as arguments (still probably not ever valid php) and stealing a bit from python's "self": $box = new Box<int|float>; // compiles to $box = new Box(alias int|float); The beauty of this is that it automatically becomes an error if you forget the type argument (and if we made it the last argument, would allow setting default values for BC reasons like Collection<T = mixed>) Even constraints on T could be expressed similarly: class Box<T: int|float> { public T $var; } which may get compiled into this not ever valid php: class Box { public BoxT $var; private alias BoxTConstraint => int|float; public function __construct(private alias BoxT) { if (BoxT is not a BoxTConstraint) throw new RunTimeException(); } Basically, it just needs to check that the BoxT alias is of the right type during construction, essentially making generics just a sugary layer over aliases. __Ambitious Type System Replacement__ I've also explored it in the case of types, in general, by replacing the entire type system with this way of representing types (where a special class represents a real type of value and its behavior). This would allow for defining casting rules, operators, passing types as values (for pattern matching), etc on types themselves. I have no idea what that would look like "at scale", but it is interesting to think about because primitive types would have the same way of working as classes and everything else. It would also separate types from their implementation—whether we want to expose this to user-land is a different story. I suspect this could be a 'technical-only' change and not affect user-land at all. zvals would probably get a lot simpler though... I generally stop myself from thinking too much about it, because while interesting, it is a TON of work. Not that I'm afraid of doing that work, I just don't want to do it by myself. So, I'm cautiously optimistic as a >=9.0 type thing and finding the right people/support. I have no idea how to do that, but I can at least try. __Constraints__ Another exploration is that it would potentially allow for setting some constraints on raw values: alias EmailAddress => string { if (!is_valid_email($value)) throw new RuntimeException('not a valid email address'); } However, I think there is a better solution for that (classes/structs/records/etc)... except for __Typed Literals__ This could easily allow typed literals for value types. After all, a typed literal can be expressed as an alias over a type with a constraint. __The Hammer and Screw Problem__ There are probably other use-cases by making the type-system more 'class-like', but I will say that PHP's class-system is quite robust—if not as robust as its arrays—and should probably be relied on more. That being said, I've been working through this for a bit now, and it seems that I might be wielding this as a hammer and seeing nails everywhere, even when they are screws. So, while interesting, in many cases, it probably isn't the best way to do things. — Rob