JogoShugh / SpaceMiner

SpaceMiner
http://spaceminer.azurewebsites.net
Other
4 stars 4 forks source link

Support Babel async / await on client via hosted Babel service #37

Closed JogoShugh closed 9 years ago

JogoShugh commented 9 years ago

Background

ES7 is going to (likely) provide C# style async/await support. This is syntactic sugar around Promises, but greatly reduces the amount of ugly code and handler functions needed.

To get a quick sense of the beneft, see this:

http://jancarloviray.com/blog/es7-async-await-javascript-simplified/

In short:

This code:

async function run(){
    if(Math.round(Math.random())){
        return 'Success!';
    } else {
        throw 'Failure!';
    }
}

Will really be doing this:

function run(){
    if(Math.round(Math.random())){
        return Promise.resolve('Success!');
    }else{
        return Promise.reject('Failure!');
    }
}

Take a look at another more in-depth article at

From there, we see that this code which chains Promises:

function doAsyncOp () {
    return asynchronousOperation().then(function(val) {
        return asynchronousOperation(val);
    }).then(function(val) {
        return asynchronousOperation(val);
    }).then(function(val) {
        return asynchronousOperation(val);
    });
};

Can be written this way in ES7:

async function doAsyncOp () {
    var val = await asynchronousOperation();
    val = await asynchronousOperation(val);
    val = await asynchronousOperation(val);
    return await asynchronousOperation(val);
};

Clearly this is a massive improvement at the language level!

Goals

First of all, the babel-compiler for Meteor has a limited, server-side-only approach to async / await currently. This is not what we need.

Visit this link to see that kind of client-side stuff we want to support:

http://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&code=(async%20function()%20%7B%0A%20%20async%20function%20run()%7B%0A%20%20%20%20return%20await*%20%5B%0A%20%20%20%20%20%20%24.ajax('https%3A%2F%2Fcommitstream.v1host.com%2Fhealth%2Fprojections')%2C%0A%20%20%20%20%20%20%24.ajax('https%3A%2F%2Fcommitstream.v1host.com%2Fhealth%2Fstatus')%2C%0A%20%20%20%20%20%20%24.ajax('https%3A%2F%2Festimably.com%2Fhealth%2Fpersistence')%0A%20%20%20%20%5D%3B%0A%20%20%7D%0A%20%20%0A%20%20try%20%7B%0A%20%20%20%20let%20val%20%3D%20await%20run()%3B%0A%20%20%20%20alert(JSON.stringify(val))%3B%20%20%20%20%0A%20%20%7D%20catch%20(err)%20%7B%0A%20%20%20%20let%20stringErr%20%3D%20JSON.stringify(err)%3B%0A%20%20%20%20alert(%60Error!%20%24%7BstringErr%7D%60)%3B%0A%20%20%7D%0A%7D())%0A%0A

That is, this code:

(async function() {
  async function getV1ServiceStatuses(){
    return await* [
      $.ajax('https://commitstream.v1host.com/health/projections'),
      $.ajax('https://commitstream.v1host.com/health/status'),
      $.ajax('https://estimably.com/health/persistence')
    ];
  }

  try {
    let statuses = await getV1ServiceStatuses();
    alert(JSON.stringify(statuses));    
  } catch (err) {
    let stringErr = JSON.stringify(err);
    alert(`Error! ${stringErr}`);
  }
}())

The code it gets transpiled into is complicated, and relies upon the runtime.js from regenerator by Facebook (https://github.com/facebook/regenerator/blob/master/runtime.js).

Here's a working JSFiddle that has the entire runtime.js embedded into it:

http://jsfiddle.net/e35qchga/2/

SpaceMiner Examples

For SpaceMiner, we want students to be able to do something like:


await move('5 left');
await move('6 down);
await move('7 right');

We already have support for move('5 l', '6 d', '7 r'). HOWEVER, if you want to use a for loop, you really end up having to get into recursive function calls, and that's less than optimal for a beginner, so having the ability to use await would do wonders for the teaching aspect without introducing the complications of Promise syntax formally -- and it will prepare them for ES7 features ahead of the game!

Parital support so far

I don't have a standalone Babel translator service running yet, but I've already added a run function which returns a Promise, and thus can be sugared over with await, and thus supported by Babel and Facebook's regenerator runtime.js which I already added. Here a sample:

Go to the Box Step training step at http://supersonic-box-113463.nitrousapp.com:3000/lesson?id=training

http://babeljs.io/repl/#?experimental=true&evaluate=false&loose=false&spec=false&code=(async%20()%20%3D%3E%20%7B%0A%20%20for%20(let%20x%20of%20range(3))%20%7B%0A%20%20%20%20x%20*%3D%207%3B%0A%20%20%20%20for%20(let%20y%20of%20range(3))%20%7B%0A%20%20%20%20%20%20y%20*%3D%204%3B%0A%20%20%20%20%20%20await%20run('move'%2C%20x%20%2B%20'%20'%20%2B%20y%2C%20'3%20r'%2C%20'3%20d'%2C%20'3%20l'%2C%20'2%20u')%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20alert('Done!')%3B%0A%7D())%3B%0A%0A

Code

You cannot directly type the following code in SpaceMiner yet. Instead, paste the transpiled code in from Babel above.

(async () => {
  for (let x of range(3)) {
    x *= 7;
    for (let y of range(3)) {
      y *= 4;
      await run('move', x + ' ' + y, '3 r', '3 d', '3 l', '2 u');
    }
  }
  alert('Done!');
}());

Clearly this code is way easier to learn, type, and explain! We won't need the outer shell, so we'll end up just having to teach and explain this part:

  for (let x of range(3)) {
    x *= 7;
    for (let y of range(3)) {
      y *= 4;
      await run('move', x + ' ' + y, '3 r', '3 d', '3 l', '2 u');
    }
  }
  alert('Done!');

Perhaps it will be better to have the API read more like this:

  for (let x of range(3)) {
    x *= 7;
    for (let y of range(3)) {
      y *= 4;
      await move(x, y, '3 r', '3 d', '3 l', '2 u');
      // Or perhaps this?
      // await move(x, y, {r:3}, {d:3}, {l:3}, {u:2});
      // I can't think of a shorter syntax other than have the parameters be all like:
      // await move(x, y, r, 3, d, 3, etc) ... but that looks klunky
    }
  }
  alert('Done!');

Here's another more complicated approach which uses the new game.world.setSprite() function to drop a tile into place where the ship moves.

(async () => {
  for (let x of range(3)) {
    x *= 7;
    for (let y of range(3)) {
      y *= 4;
      await run('move', pt(x, y));
      for (let direction of ['r', 'd', 'l', 'u']) {
        let dist = direction !== 'u' ? 3 : 2;
        for (let n of range(dist)) {
          await run('move', `1 ${direction}`);
          game.world.setSprite('t');
          if (dist === 2 && n === 1) {
            await run('move', '1 r');
            game.world.setSprite('t');
          }
        }
      };
    }
  }
 alert('Done!');
}());
JogoShugh commented 9 years ago

Heh....well the syntax could be slightly cleaner if little functions were in scope like r, l, d, u:

await move(0, 0, r(5), d(6)) etc..... And probably have right, down etc as aliases

Those functions can just return a '5 r' string or {r:5} object to be processed by the runner.

JogoShugh commented 9 years ago

OK, this is pretty much working!

After running the nitrous instance by doing . ./start.sh, you should be able to browse to:

http://supersonic-box-113463.nitrousapp.com:3000/lesson?id=training&sec=10

Then, directly paste this code that uses await in and then press execute:

for (let x of range(3)) {
  x *= 7;
  for (let y of range(3)) {
    y *= 4;
    await run('move', pt(x, y), r(3), d(3), l(3), u(2));
  }
}
alert('Done!');

This should pick up all the gemstones correctly, one group at a time top-to-bottom, left-to-right.

JogoShugh commented 9 years ago

Another idea would be to break stuff out with functions for:

await teleport(x, y);
await right(3);
await down(3);
await left(3);
await up(2);

The problem with that is that it starts to become await soup.

An alternative would be to use await*, but then...I'm not sure if this results in parellel execution or sequential.

await* [
teleport(x, y),
right(3),
down(3),
left(3),
up(2)
];
JogoShugh commented 9 years ago

Here's a fun one that produces new player sprites in the wake of the original leader. It still works all the way through hahaha. Students love "breaking" things like this:

for (let x of range(3)) { x = 7; for (let y of range(3)) { y = 4; await run('move', pt(x, y)); for (let direction of ['r', 'd', 'l', 'u']) { let dist = direction !== 'u' ? 3 : 2; for (let n of range(dist)) { await run('move', 1 ${direction}); game.world.setSprite('p'); if (dist === 2 && n === 1) { await run('move', '1 r'); game.world.setSprite('p'); } } }; } } alert('Done!');

JogoShugh commented 9 years ago

Note that now the run('move'...) syntax doesn't work. But this does:

 for (let x of range(3)) {
    x *= 7;
    for (let y of range(3)) {
      y *= 4;
      await move(pt(x, y), r(3), d(3), l(3), u(2));
      await move(point(x, y), {r:3}, down(3), {left:3}, '2 u'); // we got variety of ways...
    }
  }
  alert('Done!');