facebook / hhvm

A virtual machine for executing programs written in Hack.
https://hhvm.com
Other
18.12k stars 2.98k forks source link

Hack: No way to write generic function type #7451

Open jesseschalken opened 7 years ago

jesseschalken commented 7 years ago

HHVM Version

HipHop VM 3.15.2 (rel)
Compiler: tags/HHVM-3.15.2-0-g83ac3e5e3f5657be0cf4c55884044f86a7818b90
Repo schema: 608339137764e8365964a1adaa7a27d125b6076f

Standalone code, or other way to reproduce the problem

// Interface for things that must be producible from one of many possible inputs
interface Producer<+T> {
  public function fromFoo(Foo $foo): T;
  public function fromBar(Bar $bar): T;
  public function fromXmlString(string $xml): T;
  public function fromJsonString(string $json): T;
  // ...
}

// The input to a Producer then is a function that accepts a Producer and
// returns a value produced by the Producer.
type producer_input = (function <T>(Producer<T> $p): T);

Expected result

No errors.

Actual result

Syntax error in definition of producer_input: Expected (.

image

This was originally posted on Stack Overflow here.

dlreeves commented 7 years ago

Could this be written as:

type producer_input<T> = (function (Producer<T>): T);

I'm not familiar with another language that would allow you write a function type like you suggested. The question that comes to mind is how/when does the generic that is introduced gets bound.

jesseschalken commented 7 years ago
type producer_input = (function <T>(Producer<T> $p): T);

is a function that accepts a producer of any type and returns a value of that type. The generic is bound at the point that the function is called.

type producer_input<T> = (function (Producer<T>): T);

is a function that accepts a producer of a specific type and returns a value of that type. The generic is bound at the point that the function is created.

Consider any generic function, eg one which concats arrays:

function concat<T>(array<T> $a, array<T> $b): array<T> {/*..*/}

The type of fun('concat') must be (function <T>(array<T>, array<T>): array<T>), but that type can't be written. In fact, I can't write the function as a value:

$concat = function <T>(array<T> $a, array<T> $b): array<T> {/*..*/};
//                 ^ Expected (

TypeScript and Flow are good examples of languages which permit both writing anonymous generic functions and the types of generic functions. They have to because in JS functions defined with function foo(..) {..} are just local variables with types like any other.

Here is the original example written in Flow:

/* @flow */

class Foo {}
class Bar {}

interface Producer<+T> {
    fromFoo(foo: Foo): T;
    fromBar(bar: Bar): T;
    fromXmlString(xml: string): T;
    fromJsonString(json: string): T;
}

type producer_input = <T>(p: Producer<T>) => T;

const xml_input: producer_input = <T>(p: Producer<T>): T => p.fromXmlString('<foo />');
acrylic-origami commented 7 years ago

Bump for this, and I hope to color the request with a use-case with Awaitable which I hope gives it more tangibility. I have an async block, and I want to keep track of certain critical awaits, to enforce ordering rules down the road. A natural option is to intervene with an identity function with a side effect that pushes the dependency Awaitable to some collection in the same object. So:

async {
  await $awaitable;
}

with the help of:

class Collector {
  private IndexAccess<string, Awaitable<mixed>> $some_collection;
  // construction and misc.
  public function collect<T>(Awaitable<T> $incoming): Awaitable<T> {
    $this->some_collection->set(spl_object_hash($incoming), $incoming);
    return $incoming;
  }
  public function collect_from((function((function<T>(Awaitable<T>): Awaitable<T>): mixed) $block): void {
    // ^ untypeable
    $block(inst_meth($this, 'collect'));
  }
}

becomes:

$collector = new Collector();
$collector->collect_from(async ($collector) {
  await $collector($awaitable);
});

Although pushing to $some_collection destroys type information, it preserves time information, so the call-time binding of the type T has obvious value. Of course, the identity function $collector is untypeable outside of the method declaration at the moment.

dlreeves commented 7 years ago

We are currently in the process of changing a lot of the foundational code for the type checker, including migrating to the full fidelity parser. During this transition we want to limit changes to the grammar. Once that work is done we can examine the merits of this request.

Thank you for providing a motivating use case!

acrylic-origami commented 5 years ago

Chiming in after the reified generics release in 4.17 to propose another use case: generic programming. Now that we have the is T type check, we can refine arbitrary types into each other, like a -> ?b. The only other thing that's needed to implement a framework like Scrap Your Boilerplate are rank-2 types, which is where this feature would come in (i.e. function((function<Ta>(Ta): Tb), ...): Tc is the rank-2 type (∀a. a -> b) -> ... -> c).

Generic mutation is not possible without this feature, in a similar way to logging Awaitables above: the mutation return type needs to match the type of any value being mutated. In other words, it's required for the generalization of a type-specific transformation (e.g. ($v ==> $v * 2) with type (function(int): int)) into a generic one (i.e. (function<T>(T): T)).

The transformation itself is already possible using the is T check, it's just not possible to give it to another function:

function mkT<<<__Enforceable>>reify Ta, T>((function(Ta): Ta) $transform, T $v) {
    return ($v is Ta) ? $transform($v) : $v;
} // this typechecks fine

class Something {
    public int $i = 42;
    public string $s = 'Oy';
    public function gmapT((function<T>(T): T) $transform): void {
        $this->i = $transform($this->i);
        $this->s = $transform($this->s);
    } // it's *this* function that we can't type
}

function foo(): void {
    // example usage, which would multiply Something::$i by 2, but leave Something::$s alone
    $g_transform = <T>(T $v) ==> mkT<int, T>($w ==> $w * 2);
    (new Something())->gmapT($g_transform);
}

Interestingly, generic queries are possible in the language now without this feature, by using mixed (e.g.); it just happens to be the way the subtyping works out. In theory though, the query and mutation are just dual forms of a special folding function that requires this feature because it's a second-rank type.