Numeric Type

php.internals

Deleu

6 years ago
Hello Internals, I'd like to know what would be people's feelings towards having a `numeric` type. I remember reading the nullable casting RFC ( https://wiki.php.net/rfc/nullable-casting) and it's discussion ( https://externals.io/message/105122). Although I would very much prefer to have nullable casting, it seems to be a bit controversial and perhaps Numeric Type could be a middle ground solution. The primary intent would be to safely pass around input that usually comes from HTTP, CI or Database. Whenever interacting with these data providers, string is a common format. `is_int` will not return true for integer values that are string, instead we need to rely on `is_numeric`. Similarly, I think we could benefit from having `foo(?numeric $value)` as a type-safe mechanism to transfer data around without having to forcefully check for numeric value before casting. If we simply `(int) $value`, we may end up casting `null` to `0`. Type-hitting `?numeric` could be a compromise. ``` foo(?int $number); foo($_GET['param']); // TypeError: foo() expects int or null, string given foo((int) $_GET['param']); // null becomes 0 foo($_GET['param'] ? (int) $_GET['param'] : null); // expected behavior bar(?numeric $number); bar($_GET['param']); // would work with any value that passes is_numeric() ``` I also think this approach have a benefit over PHP 8 Union Type (int|string) because is_numeric() does not return true for string values and for consistency the `numeric` type-hint would behave similarly. Thoughts?
-- Marco Aurélio Deleu

Rowan Collins

6 years ago
Hi Marco, On Tue, 2 Jun 2020 at 16:10, Deleu <deleugyn@gmail.com> wrote:
> > The primary intent would be to safely pass around input that usually comes > from HTTP, CI or Database. Whenever interacting with these data providers, > string is a common format. `is_int` will not return true for integer values > that are string, instead we need to rely on `is_numeric`. >
The problem with is_numeric() is that it uses an extremely broad definition of "numeric", which is not often what you actually want - for instance, is_numeric("-2.56E-90") returns true. The case I frequently see is much stricter: the variable should either be an integer, or a string representation of an integer. Whenever this comes up, I struggle for what the correct check actually is: * is_int($x) won't accept any strings * is_numeric($x) will accept far too much * an int type annotation with strict_types=0 is similar to is_numeric; in strict_types=1 it's the same as is_int * (int)$x will never give an error (meaning it's oddly easy to accidentally _suppress_ errors by trying to make code run under strict_types=1) * (int)$x != 0 is sometimes good enough, as long as zero is not a valid value (if you use this when accepting Unix timestamps, for instance, Jan 1st 1970 is going to be rejected!) * ctype_digit( (string)$x ) is confusing to read (the cast is required to accept actual integers, and the name is odd out of its intended context); it will also reject any negative numbers, which may or may not be desirable The only suitable built-in function I'm aware of is filter_var, which has a pretty horrible API: if ( filter_var($x, FILTER_VALIDATE_INT) !== false ) ... In the common case that you want to cast the variable to int on success (to pass to a strictly typed function), you would need to do something like this: $x = filter_var($x, FILTER_VALIDATE_INT); if ( $x === false ) { throw new TypeError; } I would love to see the language have a built-in short-hand for that, in the form of some kind of "strict cast", e.g.: $x = (int!)$x; or: $x = (strict int)$x; Once converted in that way, you could safely pass to a parameter marked int in strict mode, so wouldn't need a new pseudo-type. Regards,
-- Rowan Tommins [IMSoP]

Deleu

6 years ago
Hi Rowan, Although I understand your concerns, I wouldn't consider them a roadblock for this feature. My rationale is that `is_numeric` has been with PHP for so long that I don't think we would ever be able to have a `numeric` type-hint that doesn't align with it, which imho means we either have it or we don't, a 3rd option where a possible type-hint that is inconsistent with the function seems worse than not having it. I fully agree with your concerns that it might not fully represent what the developer wants, but I would make a judgement call when using it the same way developers have to make a judgement call when using `is_numeric`. It might allow far more than you want, but there seems to be enough use-case that it fits where the downside is negligible or even insignificant. My point is that I don't look at the language validation to protect my code against unexpected values and I can setup validation to deal with that. What complicates things is that I want to avoid having a force-cast which is not safe for my usage because I want to preserve `null` while not having to disable strict_types. On Tue, Jun 2, 2020 at 5:47 PM Rowan Tommins <rowan.collins@gmail.com> wrote:
> Hi Marco, > > On Tue, 2 Jun 2020 at 16:10, Deleu <deleugyn@gmail.com> wrote: > > > > > The primary intent would be to safely pass around input that usually > comes > > from HTTP, CI or Database. Whenever interacting with these data > providers, > > string is a common format. `is_int` will not return true for integer > values > > that are string, instead we need to rely on `is_numeric`. > > > > > The problem with is_numeric() is that it uses an extremely broad definition > of "numeric", which is not often what you actually want - for instance, > is_numeric("-2.56E-90") returns true. > > The case I frequently see is much stricter: the variable should either be > an integer, or a string representation of an integer. Whenever this comes > up, I struggle for what the correct check actually is: > > * is_int($x) won't accept any strings > * is_numeric($x) will accept far too much > * an int type annotation with strict_types=0 is similar to is_numeric; in > strict_types=1 it's the same as is_int > * (int)$x will never give an error (meaning it's oddly easy to accidentally > _suppress_ errors by trying to make code run under strict_types=1) > * (int)$x != 0 is sometimes good enough, as long as zero is not a valid > value (if you use this when accepting Unix timestamps, for instance, Jan > 1st 1970 is going to be rejected!) > * ctype_digit( (string)$x ) is confusing to read (the cast is required to > accept actual integers, and the name is odd out of its intended context); > it will also reject any negative numbers, which may or may not be desirable > > The only suitable built-in function I'm aware of is filter_var, which has a > pretty horrible API: > > if ( filter_var($x, FILTER_VALIDATE_INT) !== false ) ... > > In the common case that you want to cast the variable to int on success (to > pass to a strictly typed function), you would need to do something like > this: > > $x = filter_var($x, FILTER_VALIDATE_INT); > if ( $x === false ) { > throw new TypeError; > } > > I would love to see the language have a built-in short-hand for that, in > the form of some kind of "strict cast", e.g.: > > $x = (int!)$x; > > or: > > $x = (strict int)$x; > > Once converted in that way, you could safely pass to a parameter marked int > in strict mode, so wouldn't need a new pseudo-type. > > > Regards, > -- > Rowan Tommins > [IMSoP] >
-- Marco Aurélio Deleu

Rowan Collins

6 years ago
On 3 June 2020 09:50:23 BST, Deleu <deleugyn@gmail.com> wrote:
> `is_numeric` has been with PHP for >so long that I don't think we would ever be able to have a `numeric` >type-hint that doesn't align with it, which imho means we either have it or >we don't, a 3rd option where a possible type-hint that is inconsistent with >the function seems worse than not having it.
I agree, but my conclusion is that the best answer is "don't have it" - I don't think the use cases are common enough to deserve a language feature rather than just using the existing function.
>My point is that I don't look at the language validation to protect my code >against unexpected values and I can setup validation to deal with that.
If it's not to protect against unexpected values, then what is it for?
>What complicates things is that I want to avoid having a force-cast >which is not safe for my usage because I want to preserve `null`
Yes, the combination of strict cast and nullable cast would sometimes be useful, although the syntax might get messy - (int?!)$foo looks horrible. Since we already have ?int though, this doesn't seem a strong reason to add ?numeric meaning almost the same thing.
> while not having to disable strict_types
It feels a bit like you want to "have your cake and eat it" here - if you want a loose check and aren't worried about a few unusual values getting through, why are you running in strict_types mode in the first place? Regards,
-- Rowan Tommins [IMSoP]

Nikita Popov

6 years ago
On Tue, Jun 2, 2020 at 5:10 PM Deleu <deleugyn@gmail.com> wrote:
> Hello Internals, > > I'd like to know what would be people's feelings towards having a `numeric` > type. I remember reading the nullable casting RFC ( > https://wiki.php.net/rfc/nullable-casting) and it's discussion ( > https://externals.io/message/105122). Although I would very much prefer to > have nullable casting, it seems to be a bit controversial and perhaps > Numeric Type could be a middle ground solution. > > The primary intent would be to safely pass around input that usually comes > from HTTP, CI or Database. Whenever interacting with these data providers, > string is a common format. `is_int` will not return true for integer values > that are string, instead we need to rely on `is_numeric`. Similarly, I > think we could benefit from having `foo(?numeric $value)` as a type-safe > mechanism to transfer data around without having to forcefully check for > numeric value before casting. If we simply `(int) $value`, we may end up > casting `null` to `0`. Type-hitting `?numeric` could be a compromise. > > ``` > foo(?int $number); > > foo($_GET['param']); // TypeError: foo() expects int or null, string given > foo((int) $_GET['param']); // null becomes 0 > foo($_GET['param'] ? (int) $_GET['param'] : null); // expected behavior > > bar(?numeric $number); > bar($_GET['param']); // would work with any value that passes is_numeric() > ``` > > I also think this approach have a benefit over PHP 8 Union Type > (int|string) because is_numeric() does not return true for string values > and for consistency the `numeric` type-hint would behave similarly. >
To clarify, what are the semantics of numeric? If you have a numeric argument and "42" is passed in, will it be converted to int(42), or will it remain as "42" (as is_numeric("42") returns true)? From your description, it seems pretty clear that you really want the nullable cast instead and "numeric" is just a workaround that makes a specific case tenable. That does not seem like a good position to argue from. I think it would make more sense to see the nullable cast RFC through to the end first. As far as I remember, it never did go to vote. Regards, Nikita

Deleu

6 years ago
> To clarify, what are the semantics of numeric? If you have a numeric
argument and "42" is passed in, will it be converted to int(42), or will it remain as "42" (as is_numeric("42") returns true)? I imagine it would remain as "42" since implicit casting is the source of some of the problems with the PHP type system. To be honest, I'd be fine with either, but I imagine that advocating for yet another implicit casting on a specific type-hint would just lead to more edge-cases and not be treated as other types are.
> From your description, it seems pretty clear that you really want the
nullable cast instead and "numeric" is just a workaround that makes a specific case tenable. That does not seem like a good position to argue from. I think it would make more sense to see the nullable cast RFC through to the end first. As far as I remember, it never did go to vote. There's no denying this, however when I looked at the discussion, I got the feeling that it may not have move forward due to the arguments against it. The reason I decided to send this email even though it doesn't seem like a good arguing position is the fact that when I looked back at all the times I wanted nullable casting they were all `int|string` scenarios. If a cheap feature such as `numeric` type-hint can solve most use cases, I'd be more than willing to deal with the custom code I will continue to have to write for cases that are not `int|string`. I don't know if there will ever be an interest in deprecating "42" + 10, which correctly results in 52. If there is, I can understand how the development of this feature might be counter-productive. Other than that, I think the benefit of this feature outweight the cost specially when looked at it from the `is_numeric` perspective, which is an existing behavior. On Wed, Jun 3, 2020 at 12:26 PM Nikita Popov <nikita.ppv@gmail.com> wrote:
> On Tue, Jun 2, 2020 at 5:10 PM Deleu <deleugyn@gmail.com> wrote: > >> Hello Internals, >> >> I'd like to know what would be people's feelings towards having a >> `numeric` >> type. I remember reading the nullable casting RFC ( >> https://wiki.php.net/rfc/nullable-casting) and it's discussion ( >> https://externals.io/message/105122). Although I would very much prefer >> to >> have nullable casting, it seems to be a bit controversial and perhaps >> Numeric Type could be a middle ground solution. >> >> The primary intent would be to safely pass around input that usually comes >> from HTTP, CI or Database. Whenever interacting with these data providers, >> string is a common format. `is_int` will not return true for integer >> values >> that are string, instead we need to rely on `is_numeric`. Similarly, I >> think we could benefit from having `foo(?numeric $value)` as a type-safe >> mechanism to transfer data around without having to forcefully check for >> numeric value before casting. If we simply `(int) $value`, we may end up >> casting `null` to `0`. Type-hitting `?numeric` could be a compromise. >> >> ``` >> foo(?int $number); >> >> foo($_GET['param']); // TypeError: foo() expects int or null, string given >> foo((int) $_GET['param']); // null becomes 0 >> foo($_GET['param'] ? (int) $_GET['param'] : null); // expected behavior >> >> bar(?numeric $number); >> bar($_GET['param']); // would work with any value that passes >> is_numeric() >> ``` >> >> I also think this approach have a benefit over PHP 8 Union Type >> (int|string) because is_numeric() does not return true for string values >> and for consistency the `numeric` type-hint would behave similarly. >> > > To clarify, what are the semantics of numeric? If you have a numeric > argument and "42" is passed in, will it be converted to int(42), or will it > remain as "42" (as is_numeric("42") returns true)? > > From your description, it seems pretty clear that you really want the > nullable cast instead and "numeric" is just a workaround that makes a > specific case tenable. That does not seem like a good position to argue > from. I think it would make more sense to see the nullable cast RFC through > to the end first. As far as I remember, it never did go to vote. > > Regards, > Nikita >
-- Marco Aurélio Deleu

Peter Stalman

6 years ago
Hi all, One thought is that `is_numeric(true)` will return `false`, but the `int` type-hint will accept it as a `1`. The other thought I had was no one mentioned floats, so maybe a `number` type could be argued for but with mixed types that can be solved by `int|float` now. Thanks, Peter On Tue., Jun. 2, 2020, 08:10 Deleu, <deleugyn@gmail.com> wrote: