0

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)

nwarp
  • 731
  • 4
  • 8
  • 17

1 Answers1

1

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.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • When memo is omitted, the first value in the Iterable is used as the memo. In that case, T is the same as Tresult. This is the behavior of reduce() in underscore.js and is occasionally useful for commutative operations. For example: _.reduce([1,2,3], (x,y) => x+y) – nwarp Jun 15 '16 at 16:55
  • @nwarp: You're not following me. What **forces** the types to be the same in your Hack program? What is stopping me from passing a null `?Fruit` as `$memo` and a sequence of `Giraffe` for the sequence? The type system is telling you that you have not provided any proof that this *cannot* happen, and therefore it *can* happen. – Eric Lippert Jun 15 '16 at 18:21
  • @nwarp: Your question is "how do I tell the type checker that these two types are the same when this value is null?" when *they are not necessarily the same when the value is null*. You can't tell the type checker that because *that's a lie*. – Eric Lippert Jun 15 '16 at 18:27
  • My interpretation is that in order to do what I want, I'll need either: 1. function overloading 2. some kind of type assertion for generics like invariant(T == Tresult). Since both these features are not available in hacklang, the behavior I desire cannot be implemented in a type-safe way. – nwarp Jun 16 '16 at 04:39
  • @nwarp: I don't see how function overloading helps. All function overloading gives you is the ability to have two different functions with the same name; why is it a hardship to have two different functions with different names? The two behaviours you want are **different functions**; one requires a projection to arbitrary type and the other requires a projection to the element type; *those are different things* so you should have *two functions*. – Eric Lippert Jun 16 '16 at 04:42
  • Function overloading will allow me to call both reduce1 and reduce2 the same name, thus getting the desired behavior. I understand your reasoning behind why these **should be** two different functions. As mentioned, I am trying to reproduce the behavior of a widely used library and **what should be done is irrelevant** in this scenario. – nwarp Jun 16 '16 at 06:47
  • @nwarp: I am kind of horrified by that last sentence. Trying to mimic a library's syntax is a feature with costs and benefits. Don't blindly reject the possibility that the costs are higher than the benefits. – Brian Jun 16 '16 at 13:45
  • @Brian I guess I should rephrase. This is not about finding the best/correct way to do something. It's about learning and testing the limits of Hack's type system. – nwarp Jun 17 '16 at 06:10
  • @nwarp: Another consideration is: your design proposal conflates null with missing. Suppose you *wanted* the initial value of the accumulator to be null; in your proposed method, the initial value being null has a special meaning. – Eric Lippert Jun 17 '16 at 12:04