Coder Social home page Coder Social logo

macabeus / js-proposal-algebraic-effects Goto Github PK

View Code? Open in Web Editor NEW
174.0 8.0 4.0 5.91 MB

๐Ÿ“Let there be algebraic effects in JS

JavaScript 99.33% Makefile 0.29% Shell 0.27% HTML 0.11%
algebraic-effects proposal js babel babel-plugin

js-proposal-algebraic-effects's Introduction

js-proposal-algebraic-effects

๐Ÿ“Let there be algebraic effects in JavaScript

Why?

No more function color! Yes one-shot delimited continuation!

What the hell?! Well... I really recommend that you read this blog post by Dan Abramov explaining algebraic effects - and how it could be very useful on our JavaScript code!

This project is a runnable POC with a Babel's "plugin", so you could write some code and taste this new concept in JavaScript. Its features, syntax, and goals are very inspired by Dan Abramov's blog post mentioned above. In short, with algebraic effects, you could separate what from the how and have fewer refactors.

Related:

It's a very simple proof of concept! There is a lot of work to improve this idea, spec, and implementation.

What does it add to the language?

Again, the syntax needs to be improved. But at this moment we have:

  • Expression perform <value>

You could use this keyword inside of any function (not arrow functions!) in order to launch an effect.
Similar to throw, it'll search for the closest try/handle at call stack to perform an effect passing <value> as the effect name. Unlike throw, perform is an expression and will return a value to continue running the code.

if (name === null) {
 name = perform 'ask_name'
 console.log(name) // after evaluate perform, will run this line
}
  • Block handle at try

Just like the catch block, you should use it to handle the effect launched inside of the try block.
And again as like catch block, this block has a parameter to bind the <value> used at perform.

try {
 ...
} handle (effect) {
 if (effect === 'ask_name') {
   ...
 }
}
  • Statement resume

It should be used inside of the handle block in order to resume the perform expression, returning a value.
It must be used inside of handle block and must be in handle block directly.

try {
 ...
} handle (effect) {
 if (effect === 'ask_name') {
  resume 'Arya Stark'
 }
}

One of its most powerful features is to use inside a block with an async operation so you could call an async operator without the need to use an async/await operators on the function - that is, less refactors and a function could be sync and async at the same time!

if (effect === 'ask_name') {
 await wait(1000)
 resume 'Arya Stark'
}
  • operator @@

Unfortunately, we still can't inject implicitly the effects inside of a function call outside of the try block, so you should use @@ at a function call that could launch effects.

function getName(user) {
 let name = user.name;
 if (name === null) {
   name = perform 'ask_name';
 }

 return name;
}

function displayNameCapitalized(user) {
 const name = getName@@(user) // need to use @@
 console.log(name.toUpperCase())
}

const arya = { name: null };

try {
 displayNameCapitalized(arya); // doesn't need to use @@
} handle (effect) {
 ...
}

Edge cases

  • no resume

Since perform is an expression, it always return a value. If resume wasn't called after a perform, it'll be undefined:

function sayName () {
 const name = perform 'a_typo_error'
 console.log(name) // undefined 
}

try {
 sayName()
} handle (effect) {
 if (effect === 'ask_name') {
  resume 'Arya Stark'
 }
}
  • no try/handle block

If a perform is called without a try/handle, an expection will be launched.

function sayName () {
 const name = perform 'ask_name' // will throw Error('Unhandled effect')
 console.log(name)
}

sayName()
  • nested try/handle (not implemented yet)

If you have two or more nested try/handle, you'll need to call resume on handle block

function sayNameAndAge () {
 const name = perform 'ask_name'
 const age = perform 'ask_age'
 console.log(name, age) // 'Arya Stark 25'
}

function wrapperAgeEffect () {
 try {
  sayNameAndAge()
 } handle (effect) {
  if (effect === 'ask_age') {
   resume 25
  }

  const result = perform effect
  return result
 }
}

try {
 wrapperAgeEffect()
} handle (effect) {
 if (effect === 'ask_name') {
  resume 'Arya Stark'
 }
}

How to run

1 - Clone this repo:

> git clone [email protected]:macabeus/js-proposal-algebraic-effects.git

2 - Run some commands on Babel:

> cd babel
> make bootstrap
> make watch

3 - So you could compile the sample with:

> cd babel-algebraic-effects
> yarn start

TODO

Check Issues and TODOs.

js-proposal-algebraic-effects's People

Contributors

dependabot[bot] avatar evaporei avatar macabeus avatar ythecombinator avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

js-proposal-algebraic-effects's Issues

When we could use perform expression?

At this moment, we could use perform only in three situations:

  • ExpressionStatement: perform 'foo'
  • AssignmentExpression: a = perform 'foo'
  • VariableDeclarator: let a = perform 'foo'

So we can't do something like: (perform 'foo') + 1, because it's a BinaryExpression.

But do we really need to maintain this behaviour?
If not, how would be the precedence?

make failed

make bootstrap failed on my computer, node9 and 12 both failed

Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.
[08:48:22] Compiling 'packages/babel-types/src/asserts/generated/index.js'...
[08:48:22] Compiling 'packages/babel-types/src/builders/generated/index.js'...
[08:48:23] Compiling 'packages/babel-types/src/constants/generated/index.js'...
[08:48:23] Compiling 'packages/babel-types/src/validators/generated/index.js'...
[08:48:24] Finished 'build-babel' after 9.47 s
[08:48:24] Finished 'build' after 9.48 s
/Library/Developer/CommandLineTools/usr/bin/make build-typings
yarn --silent node packages/babel-types/scripts/generators/flow.js > packages/babel-types/lib/index.js.flow
yarn --silent node packages/babel-types/scripts/generators/typescript.js > packages/babel-types/lib/index.d.ts
/Library/Developer/CommandLineTools/usr/bin/make build-dist
cd packages/babel-polyfill; \
	scripts/build-dist.sh
+ BROWSERIFY_CMD=../../node_modules/browserify/bin/cmd.js
+ UGLIFY_CMD=../../node_modules/uglify-js/bin/uglifyjs
+ mkdir -p dist
+ node ../../node_modules/browserify/bin/cmd.js lib/index.js --insert-global-vars global --plugin bundle-collapser/plugin --plugin derequire/plugin
+ node ../../node_modules/uglify-js/bin/uglifyjs dist/polyfill.js --compress keep_fnames,keep_fargs --mangle keep_fnames
cd packages/babel-plugin-transform-runtime; \
	yarn --silent node scripts/build-dist.js
/Users/xm/Documents/nongfu/js-proposal-algebraic-effects/babel/packages/babel-template/lib/parse.js:169
    throw err;

SyntaxError: Unexpected keyword 'resume' (26:13)
  24 |     }
  25 | 
> 26 |     function resume(key, arg) {
     |             ^
  27 |       try {
  28 |         var result = gen[key](arg)
  29 |         var value = result.value;

Nested effects

function nested2 () {
  perform 'foo' // should return 'bar'
  perform 'crazy' // should return 'default'
}

function nested () {
  try {
    nested2()
  } handle {
    if (effect === 'foo') {
      resume 'bar'
    }
  }
}

try {
  nested()
} handle {
  resume 'default'
}

Should we implicitly inject the effects? No @@?

Currently we need to use @@ whenever we want to inject the inhered effects on a function call.
Should we keep it?

If not, how to infer that we need to inject the effects on a function call?
If yes, what should do if we call a function that launches an effect and we didn't inject the effects?

Passing parameters to effects

Given the syntax perform String with a string identifying the effect to be run how do you pass arguments to the matching effect handler?

Testing

Hi there,

this is very nice work. The try/catch syntax seems elegant and not too counter-intuitive (hopefully it does not interfere with regular exception handling?)

There is one important thing though that I came to think while reviewing the proposal. What is great about algebraic effects (as understood from the freer monad) is that you can interpret those effects in any which way you want. That means in particular swapping the interpreter as you need. This in turn means the possibility of having one interpreter to run the program, and one to test the program. So say in the catch instead of only running the effects, you also log them and their results, After an effectful functions is ran, you can get back the logs and check it against expectations. This gives data structures which can be tested against with property-based testing.

The issue here is that you cannot swap a catch block (running effects) for another one (running + logging effects). Is there a syntax that would facilitate that? I can think of decoupling the catch block from the try block:

const fn = params => ${
  // effectful code
  ...
  perform this
  ...
  perform that
  ...
}

export {fn}

So you can have an exect script and a test script by using different interpreters:

// exec 
// someDataStructure  encodes the effects
const someDataStructure = fnUnderTest(some params);
const value = run (someDataStructure, execInterpreter);
... continue program
// test
const someDataStructure = fnUnderTest(some params);
const actual = run (someDataStructure, testInterpreter);
assert.deepEqual(actual, expected, `ok`);
// or with PBT
// assert.ok(property(actual, expected), `ok`);
// or with metamorphic testing
// const someDataStructure = fnUnderTest(some params);
// const anotherDataStructure = fnUnderTest(another params);
// assert.ok(property(some params, someDataStructure, another params, anotherDataStructure), `ok`);

Alternative Syntax

In an effort to reduce the amount of new keywords this would pose to introduce (effect, handle and resume), the following can serve as a possible alternative, piggybacking on the return keyword.

try {
	print(return < 'ask_name')
} return (type) {
	switch (type) {
		case 'ask_name': return > 'Arya Stark'
	}
}

Where < and > respectively represent the direction of data flow.

Statement "resume" really should be restrict to "effect" block?

This code works fine:

const foo = () => 1

try {
  ...
} effect {
  resume foo()
}

But this not:

const foo = () => { resume 1 }

try {
  ...
} effect {
  foo()
}

It happens because, at this moment, resume only work inside of effect block. But is it really good? Should we keep this behaviour or change it?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.