Wednesday 29 July 2015

PHP 7: new feature: enhanced type-checking on function arguments

G'day:
I'm back to doing some PHP beta testing. Or more "exploration" than testing, as I'm not being very rigorous about it. PHP 7 is tup to beta 2 now (download for Windows here: "PHP 7.0 (7.0.0beta2)"). Beta 1 came and went so quickly I never even installed it, let alone looked at it. I've had too ColdFusion 12 testing to do, and messing around with JavaScript. Even at work a the moment I'm pretty much doign JavaScript day-in/day-out at present. I've probably written 100 lines of PHP code (mostly unit tests) in the last few weeks.

But, anyway, the beta waits for no person, so I figured I should look at some more stuff.

PHP 7 has expanded its type-chekcing capabilities on function arguments. In "hilarious" very typical PHP language "design" fashion, PHP has had argument type-checking on object types for some time (I cannot be bothered looking up since when), but it's never had type checking on scalar values until now. I dunno what the thinking was there. I'm sure there's an oooh so good excuse for it somewhere, but I'm equally sure that would simply start draining IQ points if a person was to read it. I shall avoid as I've already got booze for draining my IQ.

Anyway, the good side of the situation is that they've added a bit of uniformity to the language here. Scalar arguments can be type-checked now too. Kinda. Let's look at that first, as it's an easy one.

<?php
// scalarArg.php

require __DIR__ . "/safeRun.php";

safeRun("Passing an int", function(){
    $i = 1;
    $result = takesInt($i);
    echo "Returned:<br>";
    var_dump($result);
});

safeRun("Passing a float", function(){
    $f = 1.1;
    $result = takesInt($f);
    echo "Returned:<br>";
    var_dump($result);
});

safeRun("Passing a boolean", function(){
    $b = true;
    $result = takesInt($b);
    echo "Returned:<br>";
    var_dump($result);
});

safeRun("Passing a string", function(){
    $s = "1";
    $result = takesInt($s);
    echo "Returned:<br>";
    var_dump($result);
});

safeRun("Passing a string which in no way can be considered an int", function(){
    $s = "not an integer";
    $result = takesInt($s);
    echo "Returned:<br>";
    var_dump($result);
});

safeRun("Passing a PHP object", function(){
    $d = new DateTime();
    $result = takesInt($d);
    echo "Returned:<br>";
    var_dump($result);
});

class C {}
safeRun("Passing a bespoke object", function(){
    $o = new C();
    $result = takesInt($o);
    echo "Returned:<br>";
    var_dump($result);
});

On PHP 5 this results in:

Passing an int

Catchable fatal error: Argument 1 passed to takesInt() must be an instance of int, integer given, called in D:\src\php\php.local\www\experiment\7\types\scalarArg.php on line 12 and defined in D:\src\php\php.local\www\experiment\7\types\scalarArg.php on line 6


(it claims it's a catchable fatal error, but I'm buggered if I know how to catch an error in PHP 5. One can in PHP 7, so let's ignore this).

This demonstrates this shortfall in PHP 5. It doesn't allow scalar type checking (where scalars are simple values like integers, strings, etc), so PHP is assuming my return type of int is actually some sort of object. Which the argument is not, so I get a fatal error. Nice. At most this should be a type-check exception, but still. PHP sucks at error handling, we know this.

Here's how PHP 5 is expecting an "int" return type to "work":

// intObject.php

require __DIR__ . "/safeRun.php";

class int {}

function takesInt(int $i){
    return $i;
}

safeRun("Passing an int (object)", function(){
    $i = new int();
    $result = takesInt($i);
    echo "Returned:<br>";
    var_dump($result);
});

This yields:

Passing an int (object)
Returned:
object(int)#2 (0) { } Ran OK


And that's correct (for PHP 5).

Anyway, PHP 7:

Passing an int
Returned:
int(1)
Ran OK


Passing a float
Returned:
int(1)
Ran OK


Passing a boolean
Returned:
int(1)
Ran OK


Passing a string
Returned:
int(1)
Ran OK


Passing a string which in no way can be considered an int
Code: 0
Message: Argument 1 passed to takesInt() must be of the type integer, string given, called in D:\src\php\php.local\www\experiment\7\types\scalarArg.php on line 40


Passing a PHP object
Code: 0
Message: Argument 1 passed to takesInt() must be of the type integer, object given, called in D:\src\php\php.local\www\experiment\7\types\scalarArg.php on line 47


Passing a bespoke object
Code: 0
Message: Argument 1 passed to takesInt() must be of the type integer, object given, called in D:\src\php\php.local\www\experiment\7\types\scalarArg.php on line 55




So it seems we're doing a bit of duck-typing here. And I don't think it's doing it right. Let's look at the results.
  • passing an int just works. Phew.
  • passing a float "works",
  • but loses precision. No. I can understand duck-typing a float which has no decimal part: that'd be OK. But
  • passing a boolean works
  • a string which can be duck-typed as an int will work. I guess this is OK.
  • a string which cannot be duck-typed as an int raises an exception or error of some type. Oddly the Throwable interface does not seem to expose a method to get the actual type out?!
  • and passing objects likewise error.
I had some grief with an adjunct to this test round, which I documented as part of yesterday's article: "PHP 7: PHP's error "handling" lunacy is getting on my tits", but in the context of this exercise, I was pretty pleased this works:

<?php
// classWithToString.php

require __DIR__ . "/safeRun.php";

class Person{

    protected $name;

    function __construct($name){
        $this->name = $name;
    }

}

class StringablePerson extends Person {

    function __toString(){
        return $this->name;
    }
}

safeRun("Outputing a StringablePerson", function(){
    $son = new StringablePerson("Zachary");
    echo "The boy's name is $son<br>";
});

safeRun("Outputing a Person (which gives a fatal error)", function(){
    $son = new Person("Zachary");
    echo "The boy's name is $son<br>";
});

Output:

Outputing a StringablePerson
The boy's name is Zachary
Ran OK


Outputing a Person (which gives a fatal error)

Catchable fatal error: Object of class Person could not be converted to string in D:\src\php\php.local\www\experiment\7\types\classWithToString.php on line 30


The error demonstrates the success of the first test here. My test function expects a string, and an object of a class implementing __toString() passes muster. This is excellent.

The next thing I wondered about is arrays. I did not know off  the top of my head whether PHP already supported specifying an array as as an argument type, so here's a baseline:

<?php
// arrayArg.php

require __DIR__ . "/safeRun.php";

function countArray(array $array){
    return count($array);
}

safeRun("Passing an array as an argument", function(){
    $rainbow = ["whero", "karaka", "kowhai", "kakariki", "kikorangi", "poropango", "papura"];
    $result = countArray($rainbow);
    echo "The rainbow has $result colours<br>";
});


safeRun("Passing a string as an argument", function(){
    countArray("Not an array");
});

And running this on PHP 5 demonstrates "array" as a type is already supported:

Passing an array as an argument
The rainbow has 7 colours
Ran OK


Passing a string as an argument

Catchable fatal error: Argument 1 passed to countArray() must be of the type array, string given, called in D:\src\php\php.local\www\experiment\7\types\arrayArg.php on line 19 and defined in D:\src\php\php.local\www\experiment\7\types\arrayArg.php on line 7


OK, but let's push the boat out a bit. It's a very simplistic requirement that an argument needs to be an array. The difference between an array and an individual value is one of plurality, not of type. If a function might take a string as an argument, the equivalent for an array situation would be that a function might take any array of strings. Not simply "an array".

Here's an example:

<?php
// arrayOfStringsArg.php

function reverseArrayElements(string[] $strings){
    return array_map(function($string){
        return strrev($string);
    }, $strings);
}

$wobniar = ["orehw","akarak" ,"iahwok", "ikirakak","ignarokik","ognaporop", "arupap"];
$rainbow = reverseArrayElements($wobniar);
var_dump($rainbow);

This demonstrates it's not simply an array we need here, it's an array of a specific type of object (points off it you write in saying "strings aren't objects in PHP": you're missing the point).

Other than when writing array utility functions (and bloody hell... PHP does not need more of those!), then in general it's the type of element in the array one is concerned about, not that the passed-in argument is simply an array of any old sh!t.

Anyway, it seems PHP has missed a trick here, as this code doesn't even parse:

Parse error: syntax error, unexpected '[', expecting variable (T_VARIABLE) in D:\src\php\php.local\www\experiment\7\types\arrayOfStringsArg.php on line 4


Suck. I checked to see if it was just an oversight with scalars (although give it didn't parse, it didn't look like it), and that failed too (arrayOfObjectArg.php).

I suppose the syntax might be different than I'm guessing, but I can't find what it is if it does exist.

There's some other stuff to test, but I want to write an article on type-checking function return-types (which is completely new to PHP 7, believe it or not?!), and I'll use those examples there. That'll probably be tomorrow, as I've got most of the code written already, I just need to write it up.

--
Adam