This is a kind of "part 2" to the earlier article about type-checkinng on funnction arguments: "PHP 7: new feature: enhanced type-checking on function arguments". I was goig to roll both of these into one article, but decided I already had enough material for one article with what I already had about arguments, so I pressed "send".
In PHP 5.5 (which is where my career with PHP started, adn currently is... although I use 5.6 and 7.0 at home), the way PHP has implemented type checking on functions is a bit of a incomplete mess:
- one can specify a type on an argument;
- but only if it is a type of object;
- ie: not an-inbuilt type like a string or an int;
- and whilst one can specify a type on some arguments (as per above), one cannot specify a return-type for the function as a whole at all.
I dunno who decided that approach was a good one, but I just hope they can no longer make decisions regarding how stuff is implemented in PHP in future. I imagine it was a "design by committee" sort of exercise, and there was disagreement and no one position in the "organisation" was able to go "this is bloody stupid, we're just going to do it like this [goes on to describe a sensible way of going about things]".
But we are where we are.
So now functions can have return types. The general syntax for a function with type-checking utilised seems to be
[access modifier] [static modifier] functionName([[type] argumentName[=defaultValue]][,...])[ : return type] {
// implementation
}
EG:
public static function getFullName(string $firstName, string $lastName) : string {
return "$firstName $lastName";
}
So here's the weird thing:
- the access and static modifiers are at the beginning of the function signature, before the function name
- the return type modifier is after the rest of the function signature
- but with arguments, the type is before the argument name
I can't help but think that how one would expect this to be implemented is:
public static string function getFullName(string $firstName, string $lastName) {
return "$firstName $lastName";
}
I wonder if other languages have a precedent set of sometimes the type is before the thing the type is for; sometimes it's after? Without checking - and with my predisposition to Java and CFML syntax - I can't help but think the PHP "designers" [cough] sat around and determined the best solution, then reminded themselves this is PHP so shelved that, and instead decided to seek out an implementation which was just a little bit sh!t. Because that's what PHP is usually like, so it's in-keeping with the existing precedent. Such as PHP has precedents.
Oh well. Syntax idiosyncrasies aside, let's have a look at it in action.
Here's the simplest use case:
<?php
// basic.php
function greet() :string {
return "G'day world";
}
echo greet();
Unsurprisingly, this simply says "G'day world". And it demonstrates nothing other than the syntax working. On PHP 5, I get this:
>php basic.php
PHP Parse error: syntax error, unexpected ':', expecting '{' in C:\webroots\sha
red\php\php.local\www\experiment\7\types\returnTypes\basic.php on line 4
Parse error: syntax error, unexpected ':', expecting '{' in C:\webroots\shared\p
hp\php.local\www\experiment\7\types\returnTypes\basic.php on line 4
>
PHP Parse error: syntax error, unexpected ':', expecting '{' in C:\webroots\sha
red\php\php.local\www\experiment\7\types\returnTypes\basic.php on line 4
Parse error: syntax error, unexpected ':', expecting '{' in C:\webroots\shared\p
hp\php.local\www\experiment\7\types\returnTypes\basic.php on line 4
>
(good of it to tell me twice)
Right, so the new syntax works in PHP 7.
Next: let's look at it enforcing its type rules:
<?php
// intReturnType.php
require __DIR__ . "/../safeRun.php";
function echoInt($x): int {
return $x;
}
safeRun("Baseline: returning an int", function(){
$testValue = 42;
$result = echoInt($testValue);
echo "echoInt($testValue): [$result]<br>";
});
safeRun("returning a string", function(){
$testValue = "wha-tekau rua";
$result = echoInt($testValue);
echo "echoInt($testValue): [$result]<br>";
});
And the Output:
Baseline: returning an int
echoInt(42): [42]
Ran OK
returning a string
Code: 0
Message: Return value of echoInt() must be of the type integer, string returned in C:\webroots\shared\php\php.local\www\experiment\7\types\returnTypes\intReturnType.php on line 7
This is pretty reasonable too.
On a whim, I decided to see how a scalar-return-typed function would deal with returning null instead of the expected type. This bombs out
<?php
// null.php
require __DIR__ . "/../safeRun.php";
function returnsAnInt($i): int {
return $i;
}
safeRun("See what happens if we return null on an int function", function(){
$result = returnsAnInt(null);
echo "returnsAnInt(null): [$result]<br>";
});
Like this:
See what happens if we return null on an int function
Code: 0
Message: Return value of returnsAnInt() must be of the type integer, null returned in C:\webroots\shared\php\php.local\www\experiment\7\types\returnTypes\null.php on line 7
I'd not really thought about it because I'm used to CFML in which everything is an object (after a fashion), and my Java is so rusty I forgot that there's no such thing as a null primitive. PHP has the same rule. I must read up why this is intrinsically the case, as I don't really know.
Actually I tried to see what would happen in CFML in case there was a surprise:
// returnsNullNumeric.cfm
include "../../../safeRun.cfm";
numeric function returnsAnInt(i) {
return i;
}
safeRun("See what happens if we return null on an int function", function(){
result = returnsAnInt(null);
writeOutput("returnsAnInt(null): [#result#]<br>");
});
(this is Lucee-only code: ColdFusion doesn't have the null keyword)
And CFML deals with it fine:
See what happens if we return null on an int function
returnsAnInt(null): []
Ran OK
But, as I said: everything in CFML is an object, so this is perhaps no surprise. And equivalent in Java won't even compile.
What about with Objects though?
<?php
// class.php
require __DIR__ . "/../safeRun.php";
class Test {}
class NotTest {}
function returnsTest($test): Test {
return $test;
}
safeRun("Calling returnsTest() with a Test object", function(){
$test = new Test();
returnsTest($test);
});
safeRun("Calling returnsTest() with a NotTest object", function(){
$notTest = new NotTest();
returnsTest($notTest);
});
safeRun("Calling returnsTest() with a null object", function(){
returnsTest(null);
});
So here I've got a Test class and a NotTest class, and I see what happens when I return one of those from a function which returns a Test. And then finally, I return a null to see what happens:
Calling returnsTest() with a Test object
Ran OK
Calling returnsTest() with a NotTest object
Code: 0
Message: Return value of returnsTest() must be an instance of Test, instance of NotTest returned in C:\webroots\shared\php\php.local\www\experiment\7\types\returnTypes\class.php on line 10
Calling returnsTest() with a null object
Code: 0
Message: Return value of returnsTest() must be an instance of Test, null returned in C:\webroots\shared\php\php.local\www\experiment\7\types\returnTypes\class.php on line 10
Well two out of three ain't bad (did I just quote Meatloaf? Jesus). I'm cool with the scalar functions not accepting null in lieu of something of the correct type, but it seems wrong to also fail when the return type is an object. I had a brief interchange about this with someone on Twitter, and they suggested one ought to make sure a function returning a certain type should actually return one. Be that as it may generally, there'll be situations where it's legit, plus it's no the language's job to make coding-standard decisions. Not that he was suggesting this is why PHP doesn't support this, that said.
I also checked that it handled type inheritance:
<?php
// subClass.php
require __DIR__ . "/../safeRun.php";
class BaseClass {}
class SubClass extends BaseClass {}
class NotSubclass {}
function returnsBaseClass($subClass): BaseClass {
return $subClass;
}
safeRun("Calling returnsBaseClass() with a SubClass object", function(){
$subClass = new SubClass();
returnsBaseClass($subClass);
});
safeRun("Calling returnsBaseClass() with a NotSubClass object", function(){
$notSubClass = new NotSubClass();
returnsBaseClass($notSubClass);
});
This worked as expected:
Calling returnsBaseClass() with a SubClass object
Ran OK
Calling returnsBaseClass() with a NotSubClass object
Code: 0
Message: Return value of returnsBaseClass() must be an instance of BaseClass, instance of NotSubclass returned in C:\webroots\shared\php\php.local\www\experiment\7\types\returnTypes\SubClass.php on line 12
And what about interface support:
<?php
// interface.php
require __DIR__ . "/../safeRun.php";
interface SomeInterface {}
class SomeImplementation implements SomeInterface {}
class NotSomeImplementation {}
function returnsSomeInterface($someImplementation): SomeInterface {
return $someImplementation;
}
safeRun("Calling returnsSomeInterface() with a SomeImplementation object", function(){
$someImplementation = new SomeImplementation();
returnsSomeInterface($someImplementation);
});
safeRun("Calling returnsSomeInterface() with a NotSomeImplementation object", function(){
$notSomeImplementation = new NotSomeImplementation();
returnsSomeInterface($notSomeImplementation);
});
All good:
Calling returnsSomeInterface() with a SomeImplementation object
Ran OK
Calling returnsSomeInterface() with a NotSomeImplementation object
Code: 0
Message: Return value of returnsSomeInterface() must implement interface SomeInterface, instance of NotSomeImplementation returned in C:\webroots\shared\php\php.local\www\experiment\7\types\returnTypes\interface.php on line 12
And, for - pedantic - good measure: interface inheritance (look: this is "testing" not "Indiana Jones Part [next]"... it's not supposed to be exciting. Then again, after the previous I Jones outing, perhaps one would not expect [next] to be any good either). So, anyway... interface inheritance:
<?php
// interfaceInheritance.php
require __DIR__ . "/../safeRun.php";
interface SomeBaseInterface {}
interface SomeSubInterface extends SomeBaseInterface {}
class SomeImplementation implements SomeSubInterface {}
class NotSomeImplementation {}
function returnsSomeBaseInterface($someImplementation): SomeBaseInterface {
return $someImplementation;
}
safeRun("Calling returnsSomeBaseInterface() with a SomeImplementation object", function(){
$someImplementation = new SomeImplementation();
returnsSomeBaseInterface($someImplementation);
});
safeRun("Calling returnsSomeBaseInterface() with a NotSomeImplementation object", function(){
$notSomeImplementation = new NotSomeImplementation();
returnsSomeBaseInterface($notSomeImplementation);
});
And - predictably now - this all works as expected to:
Calling returnsSomeBaseInterface() with a SomeImplementation object
Ran OK
Calling returnsSomeBaseInterface() with a NotSomeImplementation object
Code: 0
Message: Return value of returnsSomeBaseInterface() must implement interface SomeBaseInterface, instance of NotSomeImplementation returned in C:\webroots\shared\php\php.local\www\experiment\7\types\returnTypes\InterfaceInheritance.php on line 13
So far, so good. PHP does pretty much what is expected of it, with only the null-object thing letting it down. For sh!ts-n-giggles I'll test array return types, but I expect them to not compile (see my previous article about argument types):
<?php
// intArrayReturnType.php
function returnsArrayOfInts($arrayOfInts): int[] {
return $arrayOfInts;
}
This doesn't compile:
Parse error: syntax error, unexpected '[', expecting '{' in C:\webroots\shared\php\php.local\www\experiment\7\types\returnTypes\intArrayReturnType.php on line 4
I'll not continue down that avenue.
Right...I've got an edge case though... covariance.
Here's the test code:
<?php
// covariance.php
require __DIR__ . "/../safeRun.php";
class Base {
}
class Sub extends Base {
}
interface BaseInterface {
function returnsOneOfItsOwn($obj) : Base;
}
class TestBase implements BaseInterface {
function returnsOneOfItsOwn($obj) : Base {
return $obj;
}
}
class TestSub extends TestBase {
function returnsOneOfItsOwn($obj) : Sub {
return $obj;
}
}
This faceplants:
Fatal error: Declaration of TestSub::returnsOneOfItsOwn($obj): Sub must be compatible with BaseInterface::returnsOneOfItsOwn($obj): Base in D:\src\php\php.local\www\experiment\7\types\returnTypes\covariance.php on line 22
The error is not really accurate here. Because obviously a Sub is a Base, so it should be fine. The issue is that PHP (like CFML in this case) does not implement interface covariance. What this means (as far as my understanding goes, which is sketchy), that the interface is interpreted literally, and without any sense of possible inheritance. So for a class to fulfil BaseInterface, it must return a Base from returnOneOfItsOwn(). And exactly a Base. A Sub - whilst being a subclass of Base - is just not Base enough. As it were.
So what's the bottom line here? I think it's about time PHP got proper type-checking on function arguments and return types. And for the most part it's been implemented OK. A glaring omission is how how they deal with arrays; and the general syntax of function return-types is disappointing, on superficial assessment. My next mission is to check various other languages to see if any of them specify function signatures in the same order as PHP (half in front, the rest in back).
Okey doke. I have half a pint to finish - am sitting in my newly refurbed local as I type the latter part of this - and I'm gonna do that without typing, nose-down, into this laptop.
Righto.
--
Adam