Welcome! Log In Create A New Profile

Advanced

[PHP-DEV] [Discussion] is_string(), string type and objects implementing __toString()

Posted by Andrey Andreev 
On 3/9/2017 8:51 PM, Rowan Collins wrote:
> On 09/03/2017 17:37, Fleshgrinder wrote:
>> On 3/9/2017 3:18 AM, Stanislav Malyshev wrote:
>>
>>> 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:
>>
>> 1. is_dir
>> 2. is_executable
>> 3. is_file
>> 4. is_finite
>> 5. is_infinite
>> 6. is_link
>> 7. is_nan
>> 8. is_readable
>> 9. is_resource (checks the resource's type too)
>> 10. is_uploaded_file
>> 11. 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

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On 08/03/2017 23:32, Andrey Andreev wrote:
> 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]


--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On 09.03.2017 at 21:09, Rowan Collins wrote:

> On 08/03/2017 23:32, Andrey Andreev wrote:
>
>> 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


--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
Hi,

On Thu, Mar 9, 2017 at 7:47 PM, Fleshgrinder <[email protected]> wrote:
> On 3/9/2017 12:47 PM, Andrey Andreev wrote:
>> 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.
>
> 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.

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
Hi Rowan,

On Thu, Mar 9, 2017 at 10:09 PM, Rowan Collins <[email protected]> wrote:
> On 08/03/2017 23:32, Andrey Andreev wrote:
>>
>> 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.

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On 10 March 2017 10:57:42 GMT+00:00, Andrey Andreev <[email protected]> wrote:
>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]

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On Fri, Mar 10, 2017 at 4:20 PM, Rowan Collins <[email protected]> wrote:
> On 10 March 2017 10:57:42 GMT+00:00, Andrey Andreev <[email protected]> wrote:
>>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.

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On 10 March 2017 15:11:39 GMT+00:00, Andrey Andreev <[email protected]> wrote:
>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]

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
Hi!

> This is not true at all:
>
> 1. 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

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On 3/10/2017 11:57 AM, Andrey Andreev wrote:
> 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.

On 3/10/2017 4:11 PM, Andrey Andreev wrote:
> 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

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On Sat, Mar 11, 2017 at 1:39 PM, Fleshgrinder <[email protected]> wrote:
> On 3/10/2017 11:57 AM, Andrey Andreev wrote:
>> 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

> On 3/10/2017 4:11 PM, Andrey Andreev wrote:
>> 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.

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On 11 March 2017 13:23:16 GMT+00:00, Andrey Andreev <[email protected]> wrote:
>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]

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On Sat, Mar 11, 2017 at 3:34 PM, Rowan Collins <[email protected]> wrote:
> On 11 March 2017 13:23:16 GMT+00:00, Andrey Andreev <[email protected]> wrote:
>>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.

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On 3/11/2017 2:53 PM, Andrey Andreev wrote:
> 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

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On Sat, Mar 11, 2017 at 3:57 PM, Fleshgrinder <[email protected]> wrote:
> On 3/11/2017 2:53 PM, Andrey Andreev wrote:
>> 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.

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
On 11 March 2017 13:53:10 GMT+00:00, Andrey Andreev <[email protected]> wrote:
>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]

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php
Sorry, only registered users may post in this forum.

Click here to login