Hi all,
I submitted a GitHub PR* to allow objects implementing __toString() to
optionally pass is_string()
validation. More verbose wording of my
motivation can be seen in the PR description, but here are the main
points:
- Simpler way to do checks like: is_string($var) ||
method_exists($var, '__toString') - Can be used for stricter string parameter validation in
strict_types=0 mode (otherwise any scalar type is accepted) - Can be used for looser string parameter validation in strict_types=1
mode (__toString() objects aren't accepted there) - Regardless of the last 2 points, it is intentionally not limited to
parameter types
I didn't have time to write this email right after submitting the
patch, and in the meantime got some feedback from Fleshgrinder on
GitHub, which I'll quote and address here:
Thanks for your effort and initiative.
However, I strongly believe that this is the wrong approach. Adding a flag to a function directly results in the fact that the function violates the single responsibility principle. What we actually need to make this work is a "stringable" pseudo-type like the iterable type that was introduced in PHP 7.1. This "stringable" pseudo-type is the union of the scalar primitive string and any class that implements the __toString method.
This has the advantage that we are actually able to use it together with strict_types, plus we have separate dedicated functions like "is_stringable" that adhere to the single responsibility principle. I actually wanted to create an RFC for that along with an implementation since iterable was accepted, but did not find the time yet.
Closing note: these pseudo-types are necessary in PHP because it has no coherent type system, and there is nothing we can do about this in short term. Hence, adding such pseudo-types is the only short term solution that we actually have.
I ultimately wouldn't care if it's a separate function and did in fact
think of an is_stringable() function, but wasn't happy with the naming
- who's to say that e.g. integers aren't stringable? Bar
horribly-verbose names like
"string_or_objects_implementing__toString", I don't think there's a
way to avoid that ambiguity. :/
If we want a "stringable" type though, I guess we'll have to live with that.
I feel that debating the actual type system is way broader than I
intended this to be, so I'll refrain from going further on that for
now, as I've got some more radical ideas about it.
Thoughts?
Cheers,
Andrey.
Hi Andrey,
I think this is an interesting area to explore, but do think the scope needs to be widened slightly.
I ultimately wouldn't care if it's a separate function and did in fact
think of an is_stringable() function, but wasn't happy with the naming
- who's to say that e.g. integers aren't stringable? Bar
horribly-verbose names like
"string_or_objects_implementing__toString", I don't think there's a
way to avoid that ambiguity. :/
If we want a "stringable" type though
Sometimes, the fact that you can't think of a good name for a function is a clue that the function isn't solving a well-defined problem.
In this case, why wouldn't the function return true for integers? If the question the function is asking is "can this value be cast to string?" then the answer for any integer should be "yes". If the question is "will it pass a strict type check as a string", then the answer for objects would always be "no". Am I missing a situation where casting an object would be safe, but casting an integer wouldn't?
My own thought, mentioned somewhere in the long debate about scalar type hints, was to have a "can be cast to" function, which basically predicts if a weak type hint would accept the value without errors or warnings. This would work for stringable objects, but also for numeric strings, etc.
Regards,
--
Rowan Collins
[IMSoP]
Hi Rowan,
Hi Andrey,
I think this is an interesting area to explore, but do think the scope needs to be widened slightly.
I ultimately wouldn't care if it's a separate function and did in fact
think of an is_stringable() function, but wasn't happy with the naming
- who's to say that e.g. integers aren't stringable? Bar
horribly-verbose names like
"string_or_objects_implementing__toString", I don't think there's a
way to avoid that ambiguity. :/
If we want a "stringable" type thoughSometimes, the fact that you can't think of a good name for a function is a clue that the function isn't solving a well-defined problem.
In this case, why wouldn't the function return true for integers? If the question the function is asking is "can this value be cast to string?" then the answer for any integer should be "yes". If the question is "will it pass a strict type check as a string", then the answer for objects would always be "no". Am I missing a situation where casting an object would be safe, but casting an integer wouldn't?
Well, it may be confusing and/or a bit inconsistent since what
__toString() does is casting (and the idea will probably be ridiculed
because of this), but putting that aside - it's not about casting at
all.
The question is rather "is this value a string?", only with the added
assumption that __toString() objects are treated as "string objects"
and thus fulfill the condition (another reason why I went for an
is_string()
parameter).
It's not so much about whether casting would result in loss of data,
which is what I assume you mean by "safe", but about whether the value
was ever intended to be used as a string - an object implementing
__toString() obviously is, while the same cannot be certainly said for
integers.
An integer can be a bit flag, or a key from an id => name pair, etc.
Cheers,
Andrey.
Hi,
Andrey Andreev wrote:
The question is rather "is this value a string?", only with the added
assumption that __toString() objects are treated as "string objects"
and thus fulfill the condition (another reason why I went for an
is_string()
parameter).
This is a faulty assumption. The presence of __toString() doesn't mean
the object is a string, or intended to be used like one.
--
Andrea Faulds
https://ajf.me/
Hi,
Andrey Andreev wrote:
The question is rather "is this value a string?", only with the added
assumption that __toString() objects are treated as "string objects"
and thus fulfill the condition (another reason why I went for an
is_string()
parameter).This is a faulty assumption. The presence of __toString() doesn't mean
the object is a string, or intended to be used like one.
What is it then in your book?
--
Richard "Fleshgrinder" Fussenegger
Fleshgrinder wrote:
Hi,
Andrey Andreev wrote:
The question is rather "is this value a string?", only with the added
assumption that __toString() objects are treated as "string objects"
and thus fulfill the condition (another reason why I went for an
is_string()
parameter).This is a faulty assumption. The presence of __toString() doesn't mean
the object is a string, or intended to be used like one.What is it then in your book?
It means the object can be converted to a string. But such a conversion
may entail a loss of information and not be equivalent to the object
itself. It might be a “human-readable” form, for instance.
--
Andrea Faulds
https://ajf.me/
Fleshgrinder wrote:
Hi,
Andrey Andreev wrote:
The question is rather "is this value a string?", only with the added
assumption that __toString() objects are treated as "string objects"
and thus fulfill the condition (another reason why I went for an
is_string()
parameter).This is a faulty assumption. The presence of __toString() doesn't mean
the object is a string, or intended to be used like one.What is it then in your book?
It means the object can be converted to a string. But such a conversion
may entail a loss of information and not be equivalent to the object
itself. It might be a “human-readable” form, for instance.
To give an example: Exceptions implement __toString(), which contains the
exception message, location information and backtrace. Of course,
exceptions are rather different from strings and treating an exceptions as
a string is usually incorrect.
Nikita
Fleshgrinder wrote:
Hi,
Andrey Andreev wrote:
The question is rather "is this value a string?", only with the added
assumption that __toString() objects are treated as "string objects"
and thus fulfill the condition (another reason why I went for an
is_string()
parameter).This is a faulty assumption. The presence of __toString() doesn't mean
the object is a string, or intended to be used like one.What is it then in your book?
It means the object can be converted to a string. But such a conversion
may entail a loss of information and not be equivalent to the object
itself. It might be a “human-readable” form, for instance.
Sure, and? The to-be-called function does not require that all
information must be preserved, it requires that the to-be-passed
argument is convertible into a string. Most probably because it wants to
perform some actions on the string value, but the reasons are not of
interest.
As a matter of fact, any function that requests a string only asks for a
string and is not aware of its contents, hence, it is also not aware of
whether the to-be-passed argument might suffer from loss of information.
The same is true for instance if we pass a traversable of some sort to a
function that requires an iterable. We loose information! The only
difference here would be that we can use feature detection to gather
that information again, whereas in the Stringable implementation I
envision we cannot. This is because I would convert the object to a
primitive string before handing it in to the to-be-called function.
This is a controlled conversion, from the sender and receiver side which
is very handy in many situations.
That being said, it is just sugar topping, and not having it is not a
deal breaker either (since explicit casts or dedicated toString
methods are always there). However, it would avoid certain situations of
explicit casts where one might needlessly cast primitive strings,
whereas with the stringable data type the runtime could take care of it.
It should also be possible to generate more efficient code, since the
runtime knows better what situations are possible. Something that is
unclear with an explicit cast.
To give an example: Exceptions implement __toString(), which contains the
exception message, location information and backtrace. Of course,
exceptions are rather different from strings and treating an exceptions as
a string is usually incorrect.Nikita
For the receiver its a string and not an exception, and it does not care
that it was one.
Now, after thinking more about this, I actually think that it should
cover other scalar types too. It might be very strict, which is nice,
but at the same time this strictness does not truly add a lot of value.
After all, bool, int, float, and null can always be converted to a
string too.
In the end the use-case is to have a type constraint in strict mode that
allows more types to pass through than string only. Like a formatter
(think printf).
--
Richard "Fleshgrinder" Fussenegger
Hi Andrea,
Fleshgrinder wrote:
Hi,
Andrey Andreev wrote:
The question is rather "is this value a string?", only with the added
assumption that __toString() objects are treated as "string objects"
and thus fulfill the condition (another reason why I went for an
is_string()
parameter).This is a faulty assumption. The presence of __toString() doesn't mean
the object is a string, or intended to be used like one.What is it then in your book?
It means the object can be converted to a string. But such a conversion may
entail a loss of information and not be equivalent to the object itself. It
might be a “human-readable” form, for instance.
There will be loss of information most of the time, indeed. That's not
the point; broadly speaking, you'd have to alias __toString() to
serialize()
in order to avoid that.
If it has an explicitly declared string representation, I want to have
the option to use that easily, without writing boilerplate code to
filter-out every other type.
Cheers,
Andrey.
Hi all,
I submitted a GitHub PR* to allow objects implementing __toString() to
optionally passis_string()
validation. More verbose wording of my
motivation can be seen in the PR description, but here are the main
points:
- Simpler way to do checks like: is_string($var) ||
method_exists($var, '__toString')- Can be used for stricter string parameter validation in
strict_types=0 mode (otherwise any scalar type is accepted)
- Can be used for looser string parameter validation in strict_types=1
mode (__toString() objects aren't accepted there)
- Regardless of the last 2 points, it is intentionally not limited to
parameter types
If I understand correctly, you want the following to work:
declare(strict_type = 0);
function foo(string $bar) {
I didn't have time to write this email right after submitting the
patch, and in the meantime got some feedback from Fleshgrinder on
GitHub, which I'll quote and address here:Thanks for your effort and initiative.
However, I strongly believe that this is the wrong approach. Adding a
flag to a function directly results in the fact that the function violates
the single responsibility principle. What we actually need to make this
work is a "stringable" pseudo-type like the iterable type that was
introduced in PHP 7.1. This "stringable" pseudo-type is the union of the
scalar primitive string and any class that implements the __toString method.This has the advantage that we are actually able to use it together with
strict_types, plus we have separate dedicated functions like
"is_stringable" that adhere to the single responsibility principle. I
actually wanted to create an RFC for that along with an implementation
since iterable was accepted, but did not find the time yet.Closing note: these pseudo-types are necessary in PHP because it has no
coherent type system, and there is nothing we can do about this in short
term. Hence, adding such pseudo-types is the only short term solution that
we actually have.I ultimately wouldn't care if it's a separate function and did in fact
think of an is_stringable() function, but wasn't happy with the naming
- who's to say that e.g. integers aren't stringable? Bar
horribly-verbose names like
"string_or_objects_implementing__toString", I don't think there's a
way to avoid that ambiguity. :/
If we want a "stringable" type though, I guess we'll have to live with
that.I feel that debating the actual type system is way broader than I
intended this to be, so I'll refrain from going further on that for
now, as I've got some more radical ideas about it.
Thoughts?
Cheers,
Andrey.
Sorry, accidently sent in the middle of typing that...
Hi all,
I submitted a GitHub PR* to allow objects implementing __toString() to
optionally passis_string()
validation. More verbose wording of my
motivation can be seen in the PR description, but here are the main
points:
- Simpler way to do checks like: is_string($var) ||
method_exists($var, '__toString')- Can be used for stricter string parameter validation in
strict_types=0 mode (otherwise any scalar type is accepted)
- Can be used for looser string parameter validation in strict_types=1
mode (__toString() objects aren't accepted there)
- Regardless of the last 2 points, it is intentionally not limited to
parameter typesIf I understand correctly, you want the following to work:
declare(strict_type = 0);
function foo(string $bar) {
return $bar.'foo';
}
class Foo {
private $val;
public function __construct(string $val) {
$this->val = $val;
}
public function __toString() {
return $this->$val;
}
}
echo foo(new Foo('this is ')); // this is foo
But what happens if I change the foo function like:
function foo(string &$bar) {
$bar .= 'foo';
}
$foo = new Foo('object');
foo($foo);
var_dump($foo); // will this be an instance of Foo, or the string
"objectfoo"??
If $foo remains an object in this scope, then the function is not modifying
its value. If it becomes a string, it's an unexpected change IMO. It is
probably fine in this case, but not in the case of a more complex object.
I didn't have time to write this email right after submitting the
patch, and in the meantime got some feedback from Fleshgrinder on
GitHub, which I'll quote and address here:Thanks for your effort and initiative.
However, I strongly believe that this is the wrong approach. Adding a
flag to a function directly results in the fact that the function violates
the single responsibility principle. What we actually need to make this
work is a "stringable" pseudo-type like the iterable type that was
introduced in PHP 7.1. This "stringable" pseudo-type is the union of the
scalar primitive string and any class that implements the __toString method.This has the advantage that we are actually able to use it together
with strict_types, plus we have separate dedicated functions like
"is_stringable" that adhere to the single responsibility principle. I
actually wanted to create an RFC for that along with an implementation
since iterable was accepted, but did not find the time yet.Closing note: these pseudo-types are necessary in PHP because it has no
coherent type system, and there is nothing we can do about this in short
term. Hence, adding such pseudo-types is the only short term solution that
we actually have.I ultimately wouldn't care if it's a separate function and did in fact
think of an is_stringable() function, but wasn't happy with the naming
- who's to say that e.g. integers aren't stringable? Bar
horribly-verbose names like
"string_or_objects_implementing__toString", I don't think there's a
way to avoid that ambiguity. :/
If we want a "stringable" type though, I guess we'll have to live with
that.I feel that debating the actual type system is way broader than I
intended this to be, so I'll refrain from going further on that for
now, as I've got some more radical ideas about it.
Thoughts?
Cheers,
Andrey.
Hi Ryan,
Sorry, accidently sent in the middle of typing that...
Hi all,
I submitted a GitHub PR* to allow objects implementing __toString() to
optionally passis_string()
validation. More verbose wording of my
motivation can be seen in the PR description, but here are the main
points:
Simpler way to do checks like: is_string($var) ||
method_exists($var, '__toString')Can be used for stricter string parameter validation in
strict_types=0 mode (otherwise any scalar type is accepted)Can be used for looser string parameter validation in strict_types=1
mode (__toString() objects aren't accepted there)Regardless of the last 2 points, it is intentionally not limited to
parameter typesIf I understand correctly, you want the following to work:
declare(strict_type = 0);
function foo(string $bar) {
return $bar.'foo';
}class Foo {
private $val;
public function __construct(string $val) {
$this->val = $val;
}
public function __toString() {
return $this->$val;
}
}echo foo(new Foo('this is ')); // this is foo
But what happens if I change the foo function like:
function foo(string &$bar) {
$bar .= 'foo';
}$foo = new Foo('object');
foo($foo);
var_dump($foo); // will this be an instance of Foo, or the string
"objectfoo"??If $foo remains an object in this scope, then the function is not modifying
its value. If it becomes a string, it's an unexpected change IMO. It is
probably fine in this case, but not in the case of a more complex object.
This already works and while the reference thing is indeed ugly, the
problems I have with it are different:
- It also accepts every other scalar type.
- It will not work with strict_types=1.
- I want to do it mid-runtime, not just on function parameters.
Cheers,
Andrey.
Hey! :)
The reference is actually not a problem for a Stringable because you
would get the "Only variables can be passed by reference" error with an
object if strict_types
is enabled.
Simply because the object MUST be converted into a string. The
object itself does not satisfy the constraint, but the object clearly
states that it can be converted into a string at any point.
Not doing so would violate what strict_types
actually promise us. ;)
--
Richard "Fleshgrinder" Fussenegger
Hey! :)
The reference is actually not a problem for a Stringable because you
would get the "Only variables can be passed by reference" error with an
object ifstrict_types
is enabled.Simply because the object MUST be converted into a string. The
object itself does not satisfy the constraint, but the object clearly
states that it can be converted into a string at any point.
This is the part I disagree with. The object clearly states that it can be
turned into a string when you are done using it as a object. If it gets
turned into a string, you can no longer use it as a object.
There is a difference between changing an int to string and an object to
string, in that afterwards the int->string can continue to be treated as an
int afterwards, thanks to loose typing (otherwise it wouldn't have become a
string in the first place). However with an object->string afterwards it
can ONLY be treated as a string, it can no longer be treated as an object.
Meaning
$int = 3;
foo(3);
var_dump(++$int); // 4, success, no errors
$obj = new Foo('a');
foo($obj);
var_dump($obj->method()); // Fatal error: call to member function method on
string.
To me, this doesn't make sense.
Not doing so would violate what
strict_types
actually promise us. ;)--
Richard "Fleshgrinder" Fussenegger
Hey! :)
The reference is actually not a problem for a Stringable because you
would get the "Only variables can be passed by reference" error with an
object ifstrict_types
is enabled.Simply because the object MUST be converted into a string. The
object itself does not satisfy the constraint, but the object clearly
states that it can be converted into a string at any point.This is the part I disagree with. The object clearly states that it can be
turned into a string when you are done using it as a object. If it gets
turned into a string, you can no longer use it as a object.There is a difference between changing an int to string and an object to
string, in that afterwards the int->string can continue to be treated as an
int afterwards, thanks to loose typing (otherwise it wouldn't have become a
string in the first place). However with an object->string afterwards it
can ONLY be treated as a string, it can no longer be treated as an object.
Meaning$int = 3;
foo(3);
Sorry this should have been:
foo($int);
var_dump(++$int); // 4, success, no errors
$obj = new Foo('a');
foo($obj);
var_dump($obj->method()); // Fatal error: call to member function method
on string.To me, this doesn't make sense.
Not doing so would violate what
strict_types
actually promise us. ;)--
Richard "Fleshgrinder" Fussenegger
Hey! :)
The reference is actually not a problem for a Stringable because you
would get the "Only variables can be passed by reference" error with an
object ifstrict_types
is enabled.Simply because the object MUST be converted into a string. The
object itself does not satisfy the constraint, but the object clearly
states that it can be converted into a string at any point.This is the part I disagree with. The object clearly states that it can be
turned into a string when you are done using it as a object. If it gets
turned into a string, you can no longer use it as a object.There is a difference between changing an int to string and an object to
string, in that afterwards the int->string can continue to be treated as an
int afterwards, thanks to loose typing (otherwise it wouldn't have become a
string in the first place). However with an object->string afterwards it
can ONLY be treated as a string, it can no longer be treated as an object.
Meaning$int = 3;
foo(3);Sorry this should have been:
foo($int);var_dump(++$int); // 4, success, no errors
$obj = new Foo('a');
foo($obj);
var_dump($obj->method()); // Fatal error: call to member function method
on string.To me, this doesn't make sense.
Not doing so would violate what
strict_types
actually promise us. ;)--
Richard "Fleshgrinder" Fussenegger
function foo(string &$s) {
$s = 'foo';
}
final class Stringable {
public function __toString() {
return 'stringable';
}
}
$stringable = new Stringable;
foo($stringable);
// Fatal error: Only variables can be passed by reference in ...
$string = (string) $stringable;
foo($string);
// Heureka!
Why is that?
At the point where we reach the call foo($stringable)
we need to
perform various checks, which will lead to the conclusion that we
require a string and that the value of $stringable
is an object that
can be converted to a string. That is exactly what we do. Hence, the
call is not foo($stringable)
but rather foo('stringable')
, the value
that we received from the conversion.
The conversion is in place, just like a cast would be, hence ...
foo((string) $stringable);
... is equivalent to ...
foo($stringable);
... with a type constraint of string for that particular argument.
Obviously we should yield another error message in such cases, to ensure
that people are not confused. However, this is only about the logic.
This means by implication that a ...
function foo(stringable &$s) { }
... is impossible. Something we encounter a lot lately with many
features that we would like to have. This is also one of the reasons why
many higher languages do not support pointers in their type systems.
--
Richard "Fleshgrinder" Fussenegger
Hi all,
I submitted a GitHub PR* to allow objects implementing __toString() to
optionally passis_string()
validation. More verbose wording of my
motivation can be seen in the PR description, but here are the main
points:
- Simpler way to do checks like: is_string($var) ||
method_exists($var, '__toString')- Can be used for stricter string parameter validation in
strict_types=0 mode (otherwise any scalar type is accepted)- Can be used for looser string parameter validation in strict_types=1
mode (__toString() objects aren't accepted there)- Regardless of the last 2 points, it is intentionally not limited to
parameter types
I didn't have time to write this email right after submitting the
patch, and in the meantime got some feedback from Fleshgrinder on
GitHub, which I'll quote and address here:Thanks for your effort and initiative.
However, I strongly believe that this is the wrong approach. Adding a flag to a function directly results in the fact that the function violates the single responsibility principle. What we actually need to make this work is a "stringable" pseudo-type like the iterable type that was introduced in PHP 7.1. This "stringable" pseudo-type is the union of the scalar primitive string and any class that implements the __toString method.
This has the advantage that we are actually able to use it together with strict_types, plus we have separate dedicated functions like "is_stringable" that adhere to the single responsibility principle. I actually wanted to create an RFC for that along with an implementation since iterable was accepted, but did not find the time yet.
Closing note: these pseudo-types are necessary in PHP because it has no coherent type system, and there is nothing we can do about this in short term. Hence, adding such pseudo-types is the only short term solution that we actually have.
I ultimately wouldn't care if it's a separate function and did in fact
think of an is_stringable() function, but wasn't happy with the naming
- who's to say that e.g. integers aren't stringable? Bar
horribly-verbose names like
"string_or_objects_implementing__toString", I don't think there's a
way to avoid that ambiguity. :/
If we want a "stringable" type though, I guess we'll have to live with that.I feel that debating the actual type system is way broader than I
intended this to be, so I'll refrain from going further on that for
now, as I've got some more radical ideas about it.
Thoughts?
Cheers,
Andrey.
I would concur with Fleshgrinder. Given the type system we have to work
with, a stringable pseudo-type that translates to is_string()
(subject
to the type mode) || method_exists($var, '__toString') (not subject to
type mode) would be cleaner and more flexible than yet another flag.
Flags on method signatures are almost always a code smell.
(I suppose there's a debate to be had if an int is stringable in strict
mode; I'm not sure there myself.)
--Larry Garfield
(I suppose there's a debate to be had if an int is stringable in strict
mode; I'm not sure there myself.)
I think the main difference between strict and what I will call normal
mode is that in the strict world of a compiler you have to call code to
provide a string of characters from a binary variable such as an
integer, while PHP was originally designed simply to provide the string
view when that is needed. Conventional user input is typed as strings,
and needs to be displayed as strings on the way back out how ever you
restrict the filtering of that data internally. Strict may provide some
users with a comfort blanket that internally they have to do fewer
checks on the data coming in but it is only creating an uncontrollable
mess instead since there are now even more ways to filter what is
essentially the same simple data.
--
Lester Caine - G8HFL
Contact - http://lsces.co.uk/wiki/?page=contact
L.S.Caine Electronic Services - http://lsces.co.uk
EnquirySolve - http://enquirysolve.com/
Model Engineers Digital Workshop - http://medw.co.uk
Rainbow Digital Media - http://rainbowdigitalmedia.co.uk
Hey guys!
The question if an integer is a stringable is pretty much the same as
the question if an stdClass is an iterable. Sure, you can iterate an
stdClass instance with a foreach, but does it qualify as an iterable?
Definitely not, and luckily it was not implemented as such.
We can coerce an integer to a string but an integer is not a string. The
situation is different for an object that implements the __toString
method, since this object officially opted-in to the contract that it
can safely be used as a string.
In other words: in a coherent type system any object that is stringable
would extend the string class and thus would go through any string type
constraint. This is something that we cannot implement anymore at this
point because we have no coherent type system.
mixed (super type)
|
|- array is_array
|- bool is_bool
|- float is_float
|- int is_int
|- iterable is_iterable
|- null is_null
|- object is_object || instanceof
|- resource is_resource
|- closed resource get_resource_type === 'Unknown'
|- scalar is_scalar
|- string is_string
|
void (bottom type)
We have a single root node, but we directly branch into many types
without any relation to each other, a relation always requires hard
coding. Iterable is the very first of those hard coded types which was
added to the runtime to bridge a gab, namely the gab between array and
any object that implements the Traversable interface.
Stringable would be the same as Traversable---hence the -able suffix
(and I would love it if it would be possible to simply have a String
interface but that is impossible)---and bridge the gab between string
and any object that implements the Stringable interface.
Now comes the next specialty that we need to deal with: magic methods.
Any object that provides a magic __toString method automatically
implements the Stringable interface, hence, there is none. I do not like
this, and I would never do something like this if I would design a
programming language or a type system, but here we are. (Note that Go
works like this everywhere, urgs.)
To conclude: integers are not stringable because they are not part of
any of the two types (string + Stringable objects), just like stdClass
instances are not part of any of the two types that iterable covers.
I am open to other names, but I cannot come up with any that clearly
states its purpose: CharBuffer
?
At this point we could also argue that any object with the magic
__toString method actually is a string. However, I believe that this
would be a crass change, which would definitely break code, e.g.:
if (is_string($v) && $v !== '') echo $v{0};
We need a pseudo-type here! We most definitely require more pseudo-types
to bridge more of our root leaves. Number comes naturally (the union of
float and int which does not coerce to any of the two), an making scalar
available to userland might be handy too. A possible pseudo-type to
cover everything that can safely be converted to a string could be str
(like in Rust). But note that safely would cover many of our types
(namely bool, float, int, null, stringable objects, resources, closed
resources, and strings). However, I definitely see use cases for all
these types to be honest (think of a message formatter).
--
Richard "Fleshgrinder" Fussenegger
Hi Richard,
Please note that I had drafted this before I saw your last e-mail, so I
think some of its points are now redundant. I'm sending it anyway, in
case it helps with further thoughts.
We can coerce an integer to a string but an integer is not a string. The
situation is different for an object that implements the __toString
method, since this object officially opted-in to the contract that it
can safely be used as a string.
The problem is in the definition of "safely convert".
If "safely" means "reversably", i.e. the cast is non-lossy, then an
integer can be "safely" converted to a string, but (for example) an
Exception object (which hasn't ) cannot:
$a = 42;
$b = (string)$a;
$c = (int)$b;
assert($a === $c);
$a = new Exception;
$b = (string)$a;
Cannot convert back to Exception
To conclude: integers are not stringable because they are not part of
any of the two types (string + Stringable objects), just like stdClass
instances are not part of any of the two types that iterable covers.
This comes back to the same point I made to Ryan earlier: you have to
define this contract in terms of its implementation, not its purpose.
What is it that strings and Stringable objects have in common other
than their ability to cast to string? And since an integer can always
be cast to a string and back again, what makes it not stringable?
Going with the thought experiment of "Stringable" as an interface,
remember that interfaces implement a form of multiple inheritance. So we
don't need an ancestral relationship which links strings to ints, we can
just say that "int implements stringable". This seems perfectly
reasonable to me.
To conclude: integers are not stringable because they are not part of
any of the two types (string + Stringable objects), just like stdClass
instances are not part of any of the two types that iterable covers.
I don't think this is the same case at all; the problem with "iterable"
is that stdClass should never have been usable with foreach in the first
place. The relevant comparison is not to integers, but to objects which
don't implement __toString(). Until PHP 5.2, "$foo = new stdClass; $bar
= (string)$foo;" would actually give you a string
[https://3v4l.org/XDWTh]; not a very useful one, admittedly, but by pure
logic, any object in those PHP versions was "stringable".
I think it comes down to what you're trying to achieve: the language
can't have pseudo-types for every possible combination of types, so if
you want to detect integers as one case, and other things that can be
converted to string as another, just perform your checks in the right order:
if ( is_int($foo) ) {
// ...
} elseif ( is_stringable($foo) ) {
// ...
}
Regards,
--
Rowan Collins
[IMSoP]
Hi again,
I think it comes down to what you're trying to achieve: the language can't
have pseudo-types for every possible combination of types, so if you want to
detect integers as one case, and other things that can be converted to
string as another, just perform your checks in the right order:if ( is_int($foo) ) {
// ...
} elseif ( is_stringable($foo) ) {
// ...
}
This is why I was concerned about the discussion becoming too broad -
it brings us (or me anyway) back to square 1, if not even out of
bounds. I wanted the ability to do "string or string object" checks
without multiple function calls, and now the suggested solution
doesn't do that.
I do agree that we can't have pseudo-types for everything, but can we
at least have meaningful ones? How would "stringable" be different to
"string", and what's the point of either of them if they accept every
other scalar type? We're having this discussion because outside of the
so called "strict mode", PHP's string type is just as meaningful as
"scalar", which makes it useless IMO.
Cheers,
Andrey.
This is why I was concerned about the discussion becoming too broad -
it brings us (or me anyway) back to square 1, if not even out of
bounds.
Sorry :(
I do agree that we can't have pseudo-types for everything, but can we
at least have meaningful ones?
Sure, but you need to justify why your particular choice of pseudo-type
is more meaningful than any of the other combinations.
I still don't understand what you're using this check for that means you
want to exclude integers. If you're passing on the value to anything
that actually needs a string, you're doing a string cast, either
explicitly or implicitly, so there's no difference between me passing
you (string)'42', (int)42, or new class { function __toString() { return
'42'; } }
My best guess was that you wanted to do something different if it was
an integer, but as I showed in my last example that doesn't actually
need you to exclude them from the new function.
Regards,
--
Rowan Collins
[IMSoP]
I still don't understand what you're using this check for that means you
want to exclude integers. If you're passing on the value to anything that
actually needs a string, you're doing a string cast, either explicitly or
implicitly, so there's no difference between me passing you (string)'42',
(int)42, or new class { function __toString() { return '42'; } }
This goes all the way back to the heated discussion about scalar type
hints ... Being explicit is the entire point, and why many people
wanted strict typing.
What is the use case where every other scalar (and null) type is not
acceptable? I defended that stringable should bridge only string and
objects with __toString too first, but after thinking more about it,
there is no real world reason why one would need that. Almost all use
cases I can think about evolve around strict mode and some function that
simply does not care what it was. Hence, stringable would truly act like
the into trait in Rust.
Think of value objects. Perhaps you'd have a few methods on a value
object, but mostly use it to give context to a scalar type value.
For example, a Cookie object may have the cookie attributes (domain,
path, etc.) as value objects, but they can easily be created from raw
strings, while other types would be ambiguous.
A similar effect could be desirable for HTTP headers.
Also, we're talking about strings here only because we don't have
__toInteger(), __toFloat(), etc. I'm not saying we should, but if we
did - similar use cases would be present for other scalar types too.
Even easier to imagine - a DateTime object would probably have
__toInteger() returning a UNIX timestamp.
Cheers,
Andrey.
I still don't understand what you're using this check for that means you
want to exclude integers. If you're passing on the value to anything that
actually needs a string, you're doing a string cast, either explicitly or
implicitly, so there's no difference between me passing you (string)'42',
(int)42, or new class { function __toString() { return '42'; } }This goes all the way back to the heated discussion about scalar type
hints ... Being explicit is the entire point, and why many people
wanted strict typing.
Stringable seems very explicit and strict to me, since it is opt-in.
Currently there is no way to have the ergonomics of coercion if strict
mode is active for a file. This could be a very explicit way to enable
it for portions.
What is the use case where every other scalar (and null) type is not
acceptable? I defended that stringable should bridge only string and
objects with __toString too first, but after thinking more about it,
there is no real world reason why one would need that. Almost all use
cases I can think about evolve around strict mode and some function that
simply does not care what it was. Hence, stringable would truly act like
the into trait in Rust.Think of value objects. Perhaps you'd have a few methods on a value
object, but mostly use it to give context to a scalar type value.For example, a Cookie object may have the cookie attributes (domain,
path, etc.) as value objects, but they can easily be created from raw
strings, while other types would be ambiguous.
A similar effect could be desirable for HTTP headers.
All of these can work with any other scalar value that was coerced to a
string. I actually think that most examples given will have dedicated
strictly typed methods to ensure that the value is correct for their
domain. Don't forget that a string in PHP is a binary buffer and may
contain pretty much every kind of malicious stuff that you never wanted
or expected. Validation is absolutely necessary at all times when
dealing with strings. Having an integer that is converted to a string
does not make strings more evil than they already are.
You actually already need to deal with all kinds of data that gets
coerced, since you have no control over the strict mode of the caller. ;)
Also, we're talking about strings here only because we don't have
__toInteger(), __toFloat(), etc. I'm not saying we should, but if we
did - similar use cases would be present for other scalar types too.
Even easier to imagine - a DateTime object would probably have
__toInteger() returning a UNIX timestamp.
I actually consider the existence of almost all magic methods as a
hindrance for evolving the language, and definitely do not desire to get
more of them into core. Having a DateTime::toTimestamp()
is much more
valuable than a DateTime::__toInteger()
.
These magic methods are a result of the incoherent type system. Treating
the symptoms instead of the root cause. The story would be different if
a Timestamp
could extend Integer
which would directly allow it to be
passed to all functions that are capable of dealing with an Integer
.
(Or any other possible constellation of a more sophisticated type system.)
--
Richard "Fleshgrinder" Fussenegger
Hi,
I still don't understand what you're using this check for that means you
want to exclude integers. If you're passing on the value to anything that
actually needs a string, you're doing a string cast, either explicitly or
implicitly, so there's no difference between me passing you (string)'42',
(int)42, or new class { function __toString() { return '42'; } }This goes all the way back to the heated discussion about scalar type
hints ... Being explicit is the entire point, and why many people
wanted strict typing.Stringable seems very explicit and strict to me, since it is opt-in.
Currently there is no way to have the ergonomics of coercion if strict
mode is active for a file. This could be a very explicit way to enable
it for portions.
I don't understand what you're trying to say here, or rather what you
mean by "stringable" at this point ... Is it a compound type for
string and __toString() objects only, or not? Your last comments
suggest that it isn't.
What is the use case where every other scalar (and null) type is not
acceptable? I defended that stringable should bridge only string and
objects with __toString too first, but after thinking more about it,
there is no real world reason why one would need that. Almost all use
cases I can think about evolve around strict mode and some function that
simply does not care what it was. Hence, stringable would truly act like
the into trait in Rust.Think of value objects. Perhaps you'd have a few methods on a value
object, but mostly use it to give context to a scalar type value.For example, a Cookie object may have the cookie attributes (domain,
path, etc.) as value objects, but they can easily be created from raw
strings, while other types would be ambiguous.
A similar effect could be desirable for HTTP headers.All of these can work with any other scalar value that was coerced to a
string. I actually think that most examples given will have dedicated
strictly typed methods to ensure that the value is correct for their
domain. Don't forget that a string in PHP is a binary buffer and may
contain pretty much every kind of malicious stuff that you never wanted
or expected. Validation is absolutely necessary at all times when
dealing with strings. Having an integer that is converted to a string
does not make strings more evil than they already are.
How can "any other scalar value" work? Using the cookie and headers examples:
- booleans can be used as On/Off flags for the secure and httpOnly
cookie attributes, but aren't valid literal values for any of the
attributes, or any HTTP header. - integers are valid as a few headers' values - that is true, but
certainly in a minority of cases - floats may be used for the q(uality) attribute in content
negotiation (and nothing else AFAIK), but that is a very, very narrow
domain - null is obviously invalid ... who sends an empty header? And if you
have a use case where you do want to use them, we can already make
anything nullable
Of course the string values should be validated, unless you want to
allow setting arbitrary headers, e.g.:
abstract function setHeader(stringable $name, stringable $value);
... but filtering out the known to be invalid types is validation.
And just that much better if it happens at compile time.
You actually already need to deal with all kinds of data that gets
coerced, since you have no control over the strict mode of the caller. ;)
Yes, and I hate that, which is why I want something that doesn't
silently accept every single scalar type.
Here's the radical idea I mentioned in my initial email: make strict
mode enforceable and/or add dedicated syntax for strict type hints.
But that goes way beyond my current proposal ... if anybody wants to
talk about this, please start a separate discussion.
Also, we're talking about strings here only because we don't have
__toInteger(), __toFloat(), etc. I'm not saying we should, but if we
did - similar use cases would be present for other scalar types too.
Even easier to imagine - a DateTime object would probably have
__toInteger() returning a UNIX timestamp.I actually consider the existence of almost all magic methods as a
hindrance for evolving the language, and definitely do not desire to get
more of them into core. Having aDateTime::toTimestamp()
is much more
valuable than aDateTime::__toInteger()
.These magic methods are a result of the incoherent type system. Treating
the symptoms instead of the root cause. The story would be different if
aTimestamp
could extendInteger
which would directly allow it to be
passed to all functions that are capable of dealing with anInteger
.
(Or any other possible constellation of a more sophisticated type system.)
I explicitly noted that I'm NOT suggesting we should be adding more
magic, but simply using that for a hypothetical example.
Cheers,
Andrey.
Hi,
Hey :)
Stringable seems very explicit and strict to me, since it is opt-in.
Currently there is no way to have the ergonomics of coercion if strict
mode is active for a file. This could be a very explicit way to enable
it for portions.I don't understand what you're trying to say here, or rather what you
mean by "stringable" at this point ... Is it a compound type for
string and __toString() objects only, or not? Your last comments
suggest that it isn't.
All scalars, null, and objects with a __toString are stringable. I
argued at first that we should only bridge string and objects with a
__toString but later came to the conclusion that it does not add value.
I understand that this might be confusing, since there are right now
many messages in this thread.
How can "any other scalar value" work? Using the cookie and headers examples:
- booleans can be used as On/Off flags for the secure and httpOnly
cookie attributes, but aren't valid literal values for any of the
attributes, or any HTTP header.- integers are valid as a few headers' values - that is true, but
certainly in a minority of cases- floats may be used for the q(uality) attribute in content
negotiation (and nothing else AFAIK), but that is a very, very narrow
domain- null is obviously invalid ... who sends an empty header? And if you
have a use case where you do want to use them, we can already make
anything nullableOf course the string values should be validated, unless you want to
allow setting arbitrary headers, e.g.:abstract function setHeader(stringable $name, stringable $value);
... but filtering out the known to be invalid types is validation.
And just that much better if it happens at compile time.
Because:
- bool(true) = '0'
- bool(false) = '1'
- int(n) = 'n'
- float(n) = 'n'
- null = ''
- object(s) = 's'
- string(s) = 's'
All values are possible values that I can pass to you if you use the
string type constraint. Hence, all these types are valid string values
if you request a stringable.
Stringable should work exactly like the string constraint in non-strict
mode, but regardless of the strict mode. The difference to a scalar type
constraint is that the passed values are always converted to a scalar
string, hence, the source type is unknown to the receiver.
As you can see, it does not matter if the stringable pseudo-type accepts
more than just string and objects with a __toString method.
--
Richard "Fleshgrinder" Fussenegger
Hi,
How can "any other scalar value" work? Using the cookie and headers examples:
- booleans can be used as On/Off flags for the secure and httpOnly
cookie attributes, but aren't valid literal values for any of the
attributes, or any HTTP header.- integers are valid as a few headers' values - that is true, but
certainly in a minority of cases- floats may be used for the q(uality) attribute in content
negotiation (and nothing else AFAIK), but that is a very, very narrow
domain- null is obviously invalid ... who sends an empty header? And if you
have a use case where you do want to use them, we can already make
anything nullableOf course the string values should be validated, unless you want to
allow setting arbitrary headers, e.g.:abstract function setHeader(stringable $name, stringable $value);
... but filtering out the known to be invalid types is validation.
And just that much better if it happens at compile time.Because:
- bool(true) = '0'
- bool(false) = '1'
- int(n) = 'n'
- float(n) = 'n'
- null = ''
- object(s) = 's'
- string(s) = 's'
All values are possible values that I can pass to you if you use the
string type constraint. Hence, all these types are valid string values
if you request a stringable.
Yes, they're valid string values, but the examples I gave were meant
to show that context can make them predictably invalid, and hence why
strict typing is desirable.
Stringable should work exactly like the string constraint in non-strict
mode, but regardless of the strict mode. The difference to a scalar type
constraint is that the passed values are always converted to a scalar
string, hence, the source type is unknown to the receiver.
I'm not really interested in making "strict mode" less strict - it's
already opt-in and non-enforceable.
I want ways to write stonger-type code in "non-strict mode", because
the fact that "strict mode" is non-enforceable means I can never rely
on it.
Cheers,
Andrey.
I'm not really interested in making "strict mode" less strict - it's
already opt-in and non-enforceable.
I want ways to write stonger-type code in "non-strict mode", because
the fact that "strict mode" is non-enforceable means I can never rely
on it.
This is a common misconception - you can absolutely rely on strict mode enforcing your contract.
Basically all that happens in non-strict mode is that if the caller writes:
foo($value);
The compiler automatically changes that to:
foo((string)$value);
(That's not literally how it's implemented, but it's the effect you get.)
As the receiver of that parameter, you can't tell, and don't care, if it was the human writing the code who added the cast, or the compiler adding it for them.
You can't detect someone blindly writing "(string)" everywhere any more than you can detect whether they are running in strict mode. Nor can you know if they took it straight from unfiltered user input, or copy-pasted a literal string to 10 different files, or a hundred other things you'd really like them not to do. All you know is, you asked for a string, and you got one.
Regards,
--
Rowan Collins
[IMSoP]
I'm not really interested in making "strict mode" less strict - it's
already opt-in and non-enforceable.
I want ways to write stonger-type code in "non-strict mode", because
the fact that "strict mode" is non-enforceable means I can never rely
on it.This is a common misconception - you can absolutely rely on strict mode enforcing your contract.
Basically all that happens in non-strict mode is that if the caller writes:
foo($value);
The compiler automatically changes that to:
foo((string)$value);
(That's not literally how it's implemented, but it's the effect you get.)
If I want to enforce strict_types=1 operation on the caller - I can't,
it can always be overriden, and thus I can never rely on it.
The fact that strict_types=0 will do casting for me has no relation to this.
If there's any misconception here, it is that both modes are equal -
if they were, we wouldn't have 2 of them.
As the receiver of that parameter, you can't tell, and don't care, if it was the human writing the code who added the cast, or the compiler adding it for them.
You don't, but I do care at times.
I want to know that the caller gave me what I want, and that the
compiler didn't modify it before I received it.
Why I may care about that is a different question, and nototiously
hard to explain to people who don't, as all you'd always say something
like "you asked for an integer and got an integer" ...
Let's say I asked for one of 3 class constants, that happen to hold
integer values, and you gave me a string that just happens to be
castable to one of those values - you obviously aren't using my API
correctly, but I have no way of telling you this because the compiled
hid it from both of us.
These cases may be rare and very specific, but they do exist and are
valid. Yet for some reason, very few people around here want to admit
that - all in the name of keeping PHP a weakly-typed language all the
way. As if adding one feature would all of a sudden change your
ability to write the same code you always did.
Sorry about the rant, you can probably tell this irritates me a lot
and I can't help it. I'll shut up now, just don't go on a mission to
convince me otherwise - won't ever work.
You can't detect someone blindly writing "(string)" everywhere any more than you can detect whether they are running in strict mode. Nor can you know if they took it straight from unfiltered user input, or copy-pasted a literal string to 10 different files, or a hundred other things you'd really like them not to do. All you know is, you asked for a string, and you got one.
... exactly - uncertainty all over the place. :)
Cheers,
Andrey.
Let's say I asked for one of 3 class constants, that happen to hold
integer values, and you gave me a string that just happens to be
castable to one of those values - you obviously aren't using my API
correctly, but I have no way of telling you this because the compiled
hid it from both of us.
I actually nearly used that same example for the opposite point: you can't ever know if someone used those constants, and it has nothing to do with strict types.
The only way typing would help you know that is if the language had native enums, and was strict about casting to those.
Regards,
--
Rowan Collins
[IMSoP]
Yes, they're valid string values, but the examples I gave were meant
to show that context can make them predictably invalid, and hence why
strict typing is desirable.
I am totally in favor of strict types, but having a union of some type
and having the ability to constraint to it is strict. The union of
bool|int|float|null|string is stricter than the super type mixed. That's
the whole point of having a stringable. You can more clearly communicate
what you require to do your work.
Once more, it does not matter what the caller give you, you need to
validate it no matter what.
You don't, but I do care at times.
Sorry, but your example makes no sense at all. Just because you got an
int does not even remotely mean that one of those constants was used. On
top of that all, you still need to validate the int you got because it
has 2^31-1 possible states, or more in case of 64bit. You need an enum
in such a case, and that's the only thing that helps, nothing else. It
is also inherently simple to create one, and be type safe forever.
--
Richard "Fleshgrinder" Fussenegger
Yes, they're valid string values, but the examples I gave were meant
to show that context can make them predictably invalid, and hence why
strict typing is desirable.I am totally in favor of strict types, but having a union of some type
and having the ability to constraint to it is strict. The union of
bool|int|float|null|string is stricter than the super type mixed. That's
the whole point of having a stringable.
We already have an unambiguous name for that: scalar
You don't, but I do care at times.
Sorry, but your example makes no sense at all. Just because you got an
int does not even remotely mean that one of those constants was used.
I'm sorry, I thought I wouldn't have to explicitly state that of
course I don't know a constant was used ... "constant" is not a type.
The important thing is that I know a constant was not used.
Apparently, I am bad at examples, but take a look at sort()
.
Yes, even after I validate the $sort_flags values, I would never know
if you passed int(1) or SORT_NUMERIC.
However, knowing that no sane developer writes sort($array, 1), I can
very reasonably tell you that the following is an error:
$foo = '1';
sort($array, $foo);
And the mere existence of SORT_FLAG_CASE
makes it that much important
that an error is triggered, as now my logic would be built with
bitwise operations and I can't just check for one of X values.
Ironically enough, the following code executes silently:
$array = ['a', 'b', 'c'];
sort($array, '2234234324');
If you don't see the problem with that, I guess it does make "no sense
at all" from your POV. Just agree to disagree.
Cheers,
Andrey.
Ironically enough, the following code executes silently:
$array = ['a', 'b', 'c'];
sort($array, '2234234324');If you don't see the problem with that, I guess it does make "no sense
at all" from your POV. Just agree to disagree.
I see a problem with that, but I see exactly the same problem with this:
$array = ['a', 'b', 'c'];
sort($array, 2234234324);
The fact is, "int" is far too loose a type constraint to meaningfully validate that parameter. The solution to that is not to be more strict in rejecting strings, but to create richer types of constraint: enums, unions, domains, etc.
Regards,
--
Rowan Collins
[IMSoP]
Ironically enough, the following code executes silently:
$array = ['a', 'b', 'c'];
sort($array, '2234234324');If you don't see the problem with that, I guess it does make "no sense
at all" from your POV. Just agree to disagree.I see a problem with that, but I see exactly the same problem with this:
$array = ['a', 'b', 'c'];
sort($array, 2234234324);The fact is, "int" is far too loose a type constraint to meaningfully validate that parameter. The solution to that is not to be more strict in rejecting strings, but to create richer types of constraint: enums, unions, domains, etc.
I don't disagree with that in general, but strictly rejecting strings
and other non-integer values would alleviate the problem for a
majority of cases; i.e. would solve the 90% problem.
What I strongly disagree on is that I should be happy with coercion,
and the almost religious resistance against (non-overridable) strict
scalar typing.
Cheers,
Andrey.
I don't disagree with that in general, but strictly rejecting strings
and other non-integer values would alleviate the problem for a
majority of cases; i.e. would solve the 90% problem.What I strongly disagree on is that I should be happy with coercion,
and the almost religious resistance against (non-overridable) strict
scalar typing.
From whom? Where are you getting this from? We are all in favor of
adding more types to the runtime to solve more use cases. We are all
just concluding that a stringable type should not be constrained to
string + object::__toString only. All your examples just help to make it
more clear that there is no benefit in doing anything other than that.
--
Richard "Fleshgrinder" Fussenegger
I don't disagree with that in general, but strictly rejecting strings
and other non-integer values would alleviate the problem for a
majority of cases; i.e. would solve the 90% problem.What I strongly disagree on is that I should be happy with coercion,
and the almost religious resistance against (non-overridable) strict
scalar typing.From whom? Where are you getting this from? We are all in favor of
adding more types to the runtime to solve more use cases. We are all
just concluding that a stringable type should not be constrained to
string + object::__toString only. All your examples just help to make it
more clear that there is no benefit in doing anything other than that.
Oh, FFS! I stopped talking about __toString() 10 emails ago, and
probably the 3 of us here are all thinking about different things now.
I'm out.
I don't disagree with that in general, but strictly rejecting strings
and other non-integer values would alleviate the problem for a
majority of cases; i.e. would solve the 90% problem.
I guess I just don't see that as 90% at all. The interesting values to detect are the ones that are out of range, not just that somebody wrote '4' instead of 4.
What I strongly disagree on is that I should be happy with coercion,
and the almost religious resistance against (non-overridable) strict
scalar typing.
For me, it's about division of responsibility: a library defines a contract, and it's up to me how I meet that contract. If I go through writing (string) everywhere, or use a pre-compiler that does that for me, I'm meeting the contract. It's no more the library's business than whether I use an IDE with dozens of templates, or hand craft my code in notepad. All coercive typing does is turn that pre-compiler on by default.
What's more interesting to me is how the library can express the contract it actually wants, and scalar types by their nature make for weak constraints. All of the examples you've given are good illustrations of that, and that's why I've been trying to tease out some things that the language could do to actually help with those cases.
Regards,
--
Rowan Collins
[IMSoP]
For example, a Cookie object may have the cookie attributes (domain,
path, etc.) as value objects, but they can easily be created from raw
strings, while other types would be ambiguous.
A similar effect could be desirable for HTTP headers.
OK, now we have some concrete examples, thanks. I would say that in
these cases, what you actually want is a much tighter contract: not just
"can be converted to string", but "intended to be used in this context".
It fits with what I was saying before about "if you can't name it, maybe
it isn't the right abstraction". In this case, you want to accept some
particular value objects but not, say, Exceptions - which are as useless
for your purpose as an integer, even though (string)$foo would work fine
on both.
So you want objects that have promised to behave appropriately for this
context; that could be as simple as:
interface HTTPHeaderObject {
public function __toString();
}
...with appropriate documentation that objects declaring that they
implement this interface promise to behave in a specific way when cast
to string. This is much clearer than detecting __toString(), which only
promises "I can be cast to string" - the same promise that is made by
all scalar values.
In order to accept this or a string in a type hint, you need Union Types:
function setHeaderValue(string|HTTPHeaderObject $value) { ...
For broader use, we could perhaps have named unions:
type HTTPHeaderType = union(string, HTTPHeaderObject);
if ( $value instanceOf HTTPHeaderType ) { ...
Meanwhile, of course, you can just use a boring old user-defined function:
function is_valid_http_header_value($value) {
return is_string($value) || $value instanceOf HTTPHeaderObject;
}
Regards,
--
Rowan Collins
[IMSoP]
For example, a Cookie object may have the cookie attributes (domain,
path, etc.) as value objects, but they can easily be created from raw
strings, while other types would be ambiguous.
A similar effect could be desirable for HTTP headers.OK, now we have some concrete examples, thanks. I would say that in
these cases, what you actually want is a much tighter contract: not just
"can be converted to string", but "intended to be used in this context".It fits with what I was saying before about "if you can't name it, maybe
it isn't the right abstraction". In this case, you want to accept some
particular value objects but not, say, Exceptions - which are as useless
for your purpose as an integer, even though (string)$foo would work fine
on both.So you want objects that have promised to behave appropriately for this
context; that could be as simple as:interface HTTPHeaderObject {
public function __toString();
}....with appropriate documentation that objects declaring that they
implement this interface promise to behave in a specific way when cast
to string. This is much clearer than detecting __toString(), which only
promises "I can be cast to string" - the same promise that is made by
all scalar values.In order to accept this or a string in a type hint, you need Union Types:
function setHeaderValue(string|HTTPHeaderObject $value) { ...
Or you could require the client to pass a respective string wrapper
which extends HTTPHeaderObject instead of a plain string.
--
Christoph M. Becker
Hi Rowan,
For example, a Cookie object may have the cookie attributes (domain,
path, etc.) as value objects, but they can easily be created from raw
strings, while other types would be ambiguous.
A similar effect could be desirable for HTTP headers.OK, now we have some concrete examples, thanks. I would say that in these
cases, what you actually want is a much tighter contract: not just "can be
converted to string", but "intended to be used in this context".
I know you're trying to help, but I keep reading this as "OK, now I
can try to invalidate your use case".
There's always an alternative solution. I only ask that our
alternatives here include a middle ground between the two extremes of
"accept pretty much everything" and "be as strict as possible".
It fits with what I was saying before about "if you can't name it, maybe it
isn't the right abstraction". In this case, you want to accept some
particular value objects but not, say, Exceptions - which are as useless for
your purpose as an integer, even though (string)$foo would work fine on
both.
I can name it; the name is just ridiculously long/impractical because
__toString() is a special case unlike anything else in PHP.
So you want objects that have promised to behave appropriately for this
context; that could be as simple as:interface HTTPHeaderObject {
public function __toString();
}...with appropriate documentation that objects declaring that they implement
this interface promise to behave in a specific way when cast to string. This
is much clearer than detecting __toString(), which only promises "I can be
cast to string" - the same promise that is made by all scalar values.
My argument is that while every scalar value may promise that it "can
be cast to string", the fact that you need to explicitly declare it
means that only __toString() can promise that it "can be useful as a
string" (context-dependent of course).
In order to accept this or a string in a type hint, you need Union Types:
function setHeaderValue(string|HTTPHeaderObject $value) { ...
I would love union types, but unfortunately:
- https://wiki.php.net/rfc/union_types was declined
- They only address parameter hints, and I also want runtime checks
- Would still leave a gap for me, as I (perhaps stubbornly) want to
treat __toString() objects as regular strings
Though, I can live with the extra function call if we have union types
as that would greatly reduce my needs related to that last point.
For broader use, we could perhaps have named unions:
type HTTPHeaderType = union(string, HTTPHeaderObject);
if ( $value instanceOf HTTPHeaderType ) { ...
An excellent idea, indeed - I'd be a huge proponent of that.
Meanwhile, of course, you can just use a boring old user-defined function:
function is_valid_http_header_value($value) {
return is_string($value) || $value instanceOf HTTPHeaderObject;}
Yes, and I can already wrap is_string($var) || method_exists($var,
'__toString') in a function, even "override" the original is_string()
with my version, in a namespace. The point was to eliminate that
boilerplate code.
Obviously, I'm getting zero support on the idea and we're running
around in circles at this point, so I guess that's it from me here.
Cheers,
Andrey.
Hi again,
I think it comes down to what you're trying to achieve: the language can't
have pseudo-types for every possible combination of types, so if you want to
detect integers as one case, and other things that can be converted to
string as another, just perform your checks in the right order:if ( is_int($foo) ) {
// ...
} elseif ( is_stringable($foo) ) {
// ...
}This is why I was concerned about the discussion becoming too broad -
it brings us (or me anyway) back to square 1, if not even out of
bounds. I wanted the ability to do "string or string object" checks
without multiple function calls, and now the suggested solution
doesn't do that.I do agree that we can't have pseudo-types for everything, but can we
at least have meaningful ones? How would "stringable" be different to
"string", and what's the point of either of them if they accept every
other scalar type? We're having this discussion because outside of the
so called "strict mode", PHP's string type is just as meaningful as
"scalar", which makes it useless IMO.Cheers,
Andrey.
What is the use case where every other scalar (and null) type is not
acceptable? I defended that stringable should bridge only string and
objects with __toString too first, but after thinking more about it,
there is no real world reason why one would need that. Almost all use
cases I can think about evolve around strict mode and some function that
simply does not care what it was. Hence, stringable would truly act like
the into trait in Rust.
Although, to be fair, Rust does not provide Into<String> nor Into<&'a
str> for numeric types. I actually don't know why, since fmt::Display is
implemented for all of them. Then again, Rust is very, very different to
PHP.
--
Richard "Fleshgrinder" Fussenegger
Hi!
I submitted a GitHub PR* to allow objects implementing __toString() to
optionally passis_string()
validation. More verbose wording of my
motivation can be seen in the PR description, but here are the main
points:
I don't think it's right approach. is_* functions check the current type
of the value, not whether it can be converted to another type. If we
need ones that express the latter, we should have different functions.
Also, as already noted, having __toString doesn't mean it returns
something useful.
--
Stas Malyshev
smalyshev@gmail.com
Hi!
I submitted a GitHub PR* to allow objects implementing __toString() to
optionally passis_string()
validation. More verbose wording of my
motivation can be seen in the PR description, but here are the main
points:I don't think it's right approach. is_* functions check the current type
of the value, not whether it can be converted to another type. If we
need ones that express the latter, we should have different functions.Also, as already noted, having __toString doesn't mean it returns
something useful.
This is not true at all:
- is_dir
- is_executable
- is_file
- is_finite
- is_infinite
- is_link
- is_nan
- is_readable
- is_resource (checks the resource's type too)
- is_uploaded_file
- is_writable
All other is_*
functions are either checking the type or the instance
of an object in some way.
--
Richard "Fleshgrinder" Fussenegger
I don't think it's right approach. is_* functions check the current type
of the value, not whether it can be converted to another type. If we
need ones that express the latter, we should have different functions.Also, as already noted, having __toString doesn't mean it returns
something useful.This is not true at all:
- is_dir
- is_executable
- is_file
- is_finite
- is_infinite
- is_link
- is_nan
- is_readable
- is_resource (checks the resource's type too)
- is_uploaded_file
- is_writable
I think a good example is "is_callable", which evaluates not the type
of the value, but the possible behaviour. You could think of
Closure::fromCallable($foo) as "cast $foo to Closure", in which case
is_callable($foo) is "can $foo be cast to Closure?" (at least, in it's
default behaviour).
This is then much more similar to defining is_stringable($foo) to return
true for everything that can be cast to string.
Regards,
--
Rowan Collins
[IMSoP]
I don't think it's right approach. is_* functions check the current type
of the value, not whether it can be converted to another type. If we
need ones that express the latter, we should have different functions.Also, as already noted, having __toString doesn't mean it returns
something useful.This is not true at all:
- is_dir
- is_executable
- is_file
- is_finite
- is_infinite
- is_link
- is_nan
- is_readable
- is_resource (checks the resource's type too)
- is_uploaded_file
- is_writable
I think a good example is "is_callable", which evaluates not the type
of the value, but the possible behaviour. You could think of
Closure::fromCallable($foo) as "cast $foo to Closure", in which case
is_callable($foo) is "can $foo be cast to Closure?" (at least, in it's
default behaviour).This is then much more similar to defining is_stringable($foo) to return
true for everything that can be cast to string.Regards,
I also forgot about is_numeric which checks the content of a string. ;)
--
Richard "Fleshgrinder" Fussenegger
Hi!
This is not true at all:
- is_dir
Oh come on. I assumed I don't need to explain that the context was about
is_* functions for types, not every function that starts with is_*. It
doesn't even make sense to compare is_string to is_dir.
--
Stas Malyshev
smalyshev@gmail.com