Search code examples
hacklang

Combine multiple generic types in hacklang


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)


Solution

  • 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.