Open jesseschalken opened 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.
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 />');
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 await
s, 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.
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!
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 Awaitable
s 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.
HHVM Version
Standalone code, or other way to reproduce the problem
Expected result
No errors.
Actual result
Syntax error in definition of
producer_input
:Expected (
.This was originally posted on Stack Overflow here.