motoko icon indicating copy to clipboard operation
motoko copied to clipboard

experiment: allow non-shared `async*` types (only)

Open crusso opened this issue 2 years ago • 7 comments

So that we can write parametric abstractions, in the absence of a shared type constraint.

By popular demand. Fixes (part of) #3892.

Example:

import Debug "mo:base/Debug";
import Array "mo:base/Array";

actor a {

  func expect<T>(a : () -> async* T) : async* T {
    await* a();
  };


  func mapPar<A,B>(args : [A], f : A -> async* B) : async* [B] {
     let vs = Array.init<?B>(args.size(), null);
     for (i in args.keys()) {
        vs[i] := ?(await* f(args[i]));
     };
     Array.tabulate<B>(args.size(), func i { let ?v = vs[i]; v });
  };

  public func f(a : Nat) : async Text { debug_show(a) };

  public func go() : async () {
     let x = await* expect<Nat>(func () : async* Nat {await async 1});
     assert x == 1;

     let vs = await* mapPar([0,1,2], func (a : Nat) : async* Text { await f(a) });

     Debug.print(debug_show vs);
  }
};

Pros: just a minor tweak

Cons:

  • still requires ugly eta-expansion of shared functions calls.
  • can't just abstract over async* T (must be a function U -> async* T) to avoid scope violations. (Perhaps async* T should be implicitly scope-polymorphic (since the computation is delayed anyway)).
  • can't use implicit lambda arguments because we don't insert scope parameters for those (a separate, but annoying, issue: our sugar to do so is (syntactically) type driven, which obviously doesn't work when types are omitted.

TODO

  • [ ] test run-drun/do-async-poly.mo needs revisiting
  • [ ] actually implement mapPar with parallel waiting. This version ain't that.

crusso avatar Apr 13 '23 20:04 crusso

Comparing from c33c06cb85874b10ac0c70c82702beda9d6b0cfa to c99bf6b1be8d13093bd48167966953083dc92346: In terms of gas, no changes are observed in 4 tests. In terms of size, no changes are observed in 4 tests.

github-actions[bot] avatar Apr 14 '23 12:04 github-actions[bot]

Oh crap, that's actually mapSeq (sequential map), not mapPar (parallel map), due to lazy semantics of async*. Maybe this doesn't give us what we want unless we also do non-shared async types after all. Will need to experiment some more first.

crusso avatar Apr 21 '23 19:04 crusso

That's annoying.

Does it have to be lazy?

nomeata avatar Apr 21 '23 20:04 nomeata

Does it have to be lazy?

I think it does, if you want await* to demarcate the effects. But I'm open to better suggestions. I think the way to solve this is to lift the restriction on vanilla async t. Still scratching my head here.

crusso avatar Apr 23 '23 09:04 crusso

I’m not thinking this through, but could async* { e } eagerly start evaluating e until the first actual await? A bit like async { e } “eagerly” (but in a separate threat) starts evaluating e?

nomeata avatar Apr 23 '23 10:04 nomeata

I’m not thinking this through, but could async* { e } eagerly start evaluating e until the first actual await? A bit like async { e } “eagerly” (but in a separate threat) starts evaluating e?

That would be the semantics of the 'do async e' I originally proposed, except with a separate type. I guess the problem then is that the effects happen even without 'await*'. With 'async e' nothing happens unless you 'await',

crusso avatar Apr 24 '23 06:04 crusso

Ah, right

nomeata avatar Apr 24 '23 06:04 nomeata

Just doing some repo cleanup for better focussing on active work: Closing PR as it seems inactive for a longer time. Please re-open if it is still relevant and after it has been updated with latest master changes.

luc-blaeser avatar Dec 03 '24 09:12 luc-blaeser