I am trying to implement the reduce function from underscore in hack. In underscore, the reduce function has the following behavior:
If no memo is passed to the initial invocation of reduce, the iteratee is not invoked on the first element of the list. The first element is instead passed as the memo in the invocation of the iteratee on the next element in the list.
My attempt to implement the function:
function reduce<T, Tresult>(
Iterable<T> $iterable,
(function(?Tresult, T):Tresult) $fn,
?Tresult $memo=null):?Tresult {
if (is_null($memo)) {
$memo = $iterable->firstValue();
$iterable = $iterable->skip(1);
}
foreach ($iterable as $value) {
$memo = $fn($memo, $value);
}
return $memo;
}
This results in the error:
Invalid return type (Typing[4110])
This is a value of generic type Tresult
It is incompatible with a value of generic type T
via this generic Tv
How do I tell the type checker that T == Tresult
when is_null($memo)
I note that the line
$memo = $iterable->firstValue();
assigns a value of type T
to $memo
. This seems wrong; $memo
is given to be of type ?Tresult
in the declaration, and assigned a value of type Tresult
here:
$memo = $fn($memo, $value);
Can you explain why $memo
is assigned a value of type T
in the first instance? How do you know that T
and Tresult
are the same? I see no evidence whatsoever that these two types are ever constrained to be the same thing. The type checker is giving you an error here because this program isn't typesafe; if T is Animal and Tresult is Fruit, and someone passes in a null fruit, there's no way to get a fruit out of the sequence.
Also, I find it weird that reduce
returns a nullable result; surely it should be returning a result of the given result type, no?
If you want this function to have two different behaviours depending on the nullity of the argument, then why not instead simply have two functions?
function reduce1<T, Tresult>(
Iterable<T> $iterable,
(function(Tresult, T):Tresult) $fn,
Tresult $memo): Tresult {
foreach ($iterable as $value) {
$memo = $fn($memo, $value);
}
return $memo;
}
function reduce2<T>(
Iterable<T> $iterable,
(function(T, T):T) $fn): T {
return reduce1($iterable->skip(1), $fn, $iterable->firstValue());
}
There, now we have two different forms of reduce, and both of them are typesafe.