Skip to content

Instantly share code, notes, and snippets.

@dherman
Last active June 30, 2021 14:21
Show Gist options
  • Save dherman/1c97dfb25179fa34a41b5fff040f9879 to your computer and use it in GitHub Desktop.
Save dherman/1c97dfb25179fa34a41b5fff040f9879 to your computer and use it in GitHub Desktop.

Motivation

  • expression-oriented programming one of the great advances of FP
  • expressions plug together like legos, making more malleable programming experience in-the-small

Examples

Write in an expression-oriented style, scoping variables as locally as possible:

let x = do {
  let tmp = f();
  tmp * tmp + 1
};

Use conditional statements as expressions, instead of awkward nested ternaries:

let x = do {
  if (foo()) { f() }
  else if (bar()) { g() }
  else { h() }
};

Especially nice for templating languages like JSX:

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

Tennant's Correspondence Principle

  • key refactoring principles:
    • do { <expr>; } equivalent to <expr>
    • (do { <stmt> };) equivalent to { <stmt> }
  • this semantic transparency is demonstrated by the semantics:
    1. Return the result of evaluating Body.

Further considerations

How to avoid either parsing conflict in statement context with do-while, or dangling-else type of ambiguity:

do do f(); while (x);

I have several alternatives I intend to explore here.

@topaxi
Copy link

topaxi commented Jan 29, 2017

Would it be possible to drop the block for a single statement?

Example:

let something = do if (...) {
  // something
} else if (...) {
  // something else
}
else {
  // fallback
}

or

let something = do try {
  // something
} catch (e) {
  null;
};

This is obviously a minor difference, but I guess most use-cases might consist of a single statement.

@caesarsol
Copy link

@topaxi that's an awesome idea! Would be a giant step towards FP.

@dfischer
Copy link

Love it @topaxi

@Jamesernator
Copy link

Will there also be an async variant in this proposal, or would that be a consideration for later proposals? e.g.

const file = do async { // or async do or something
    const response = await fetch('some-resource.txt') // the main purpose of a async do would be to allow await
    response.text()
}
// file is the promise response response.text()

@cvoege
Copy link

cvoege commented Feb 28, 2017

@Jamesernator As far as I can tell, for async/await you would simply have to be doing your do block in an async function:

async function getFile() {
  const file = do { // or async do or something
      const response = await fetch('some-resource.txt') // the main purpose of a async do would be to allow await
      await response.text()
  }
  // file is the promise response response.text()
}

You could technically have async do blocks, but they would end up just passing back a promise (so in your example, file would get set to a promise). This wouldn't be what you wanted, so just putting it in an async function is the much more direct solution.

@Jamesernator
Copy link

@cvoege actually I would actually want the promise, not the text in the file, that way I can use it with Promise.all/Promise.race later.

As an example of how you'd use it:

// in some async function

const file = async do {
    const response = await fetch('some-resource.txt')
    response.text()
}

const text = await Promise.race(file, timeout(4000)).catch(_ => {
    // display 'this seems to be taking a while maybe the server is down...'
    return file
})
// set content to text or whatever

Without async do expressions it would wind up something like this:

// in some async function

const file = async function() {
    return do {
        const response = await fetch('some-resource.txt')
        response.text()
    }
}()

const text = await Promise.race(file, timeout(4000)).catch(_ => {
    // display 'this seems to be taking a while maybe the server is down...'
    return file
})
// set content to text or whatever

Which while not terrible it feels unnecessary to still require IIFE with do expressions when it seems like the idea of do-expressions is do be able to do away with the semantics of IIFE so instead of this:

const res = function() {
    const scoped = true
    if (scoped) {
        return 3
    } else {
        return 4
    }
}()

I can have this:

const res = do {
    const scoped = true
    if (scoped) {
       3
    } else {
       4
    }

Not having the same for async feels like unnecessary symmetry breaking. Now while I understand eval doesn't actually consider await statements there's no reason it couldn't be created for the context of async do

@dead-claudia
Copy link

dead-claudia commented Mar 4, 2017

@Jamesernator Your async + do expressions could be refactored to something much more concise with the help of async arrow functions:

const file = (async () => do {
    const response = await fetch('some-resource.txt')
    response.text()
})()

Not saying that this is preferable, though. I'd still rather have something like this instead:

const file = async do {
    const response = await fetch('some-resource.txt')
    response.text()
}

@dead-claudia
Copy link

Here's an idea for avoiding the dangling while in do ... while: let's make loops not allowed as the completion statement in a do block. That will solve the problem quickly. Also, what would be the logical completion value of a loop? I feel the best way out of it is by just disallowing it as a possible completion statement via an early error.

@iddan
Copy link

iddan commented Mar 7, 2017

@morsdyce and I think it could be better to use explicit return in the do blocks.

let color = do {
    if (1 + 2 === 3) {
           return 'yellow';
    }
}

@remmycat
Copy link

remmycat commented Mar 15, 2017

I'd also love to see things like do switch. fav would be without explicit return but i guess then you'd need break again?

const color = do switch(id) {
    case 0: return 'blue';
    case 1: return 'black';
    case 2: return 'yellow';
    default: return 'white';
}

if explicit return is used, the do would really just be syntax sugar for IIFEs

@Jamesernator
Copy link

Personally I feel like it should be that do expressions introduce a new function-like scope thing but also still give eval result e.g.:

const x = do {
    3
}
// Still 3

function foo() { 
    const x = do {
        return 3
    }
    // x is 3 and this code is reached feels more useful than either returning 3 from foo or outright banning control flow in do expressions
}

@Jamesernator
Copy link

Jamesernator commented Mar 16, 2017

Also I was playing around in the d8 shell with their experimental version of do expressions, and I realized default function arguments is a case that really needs considering when deciding the semantics for example currently what d8 does for a few cases:

function foo(x=do { var y = 3 }) {
    print(y)
}

foo() // prints 3
function foo(x=do { return 'Early return' }) {
    return 'Normal return'
}

print(foo()) // Early return
function* foo(x=do { return 'Early return' }) {
    yield 1
}
print(foo().value) // Early return, obviously this isn't what we'd want
print(foo().done) // true
function* foo(x= do { yield 'Early value' }) {
    yield 1
}
foo() // SegFault, obviously v8 doesn't have any notion of a parameters environment yet 
        // which isn't surprising as arbitrary statements were never able to be put into default parameters position
async function(x=do { await 3 }) {
    print(x)
}
foo() //SegFault again

So this proposal will need to define what a parameter scope environment looks like in addition to the semantics of what happens inside a do expression block.

@yongxu
Copy link

yongxu commented Mar 26, 2017

Would it be a good idea to convert some statements such as if switch block into ExpressionStatement if they are on the right side of the AssignmentExpression? So that we can write functional code easily and omit do in most cases.

@Alexsey
Copy link

Alexsey commented Mar 27, 2017

@isiahmeadows your approch to use do inside immediatly invoked function is making a little sense because most point to use do is not to use immediatly invoked function

I have run in to the same issue as Jamesernator and I'm also think that async do will fit very well. Like do is a replacement of () => and async do is a replacement of async () =>

@andyearnshaw
Copy link

@remhume return inside a do expression would return the outer function (or throw if the do expression is at the top level). You would need break statements instead of the return.

@Jamesernator
Copy link

Jamesernator commented Apr 11, 2017

do is not to use immediatly invoked function

@Alexsey That's not entirely true, essentially I'd like semantics similar to arrow functions but that currently don't exist in the language essentially I'd want the following to happen:

const num = do {
    if (true) {
        3
    } else {
        4
    }
}
// I'd like to see num === 3 here

const num = do {
    return 3
}

// I'd also like num to be 3 here

for (const x of [1,2,3,4]) {
    const square = do {
        break
    }
}
// I'd like to be a syntax error about invalid break

The only things I'm undecided on is how I'd want to see yield / await in a plain do expression:

One: Simply transfer outer control of await / yield to the do expression:

function* foo() {
    console.log(do {
        yield null
    })
}
const g = foo()
g.next()
g.next(10) // Would print 10

let x = do { yield 1 } // SyntaxError

async function foo() {
    console.log(do {
       await fetch('foo.json').then(res => res.text())
    }) // Contents of foo.json
    console.log(async do {
        await fetch('foo.json').then(res => res.text())
    }) // Promise eventually resolving to contents of foo.json
        // unlike previous form doesn't pause the async function
}

Two: do is like an iife in almost every way except with special returning rules (although =>* doesn't have any proposal linked so I'd wait for this behaviour):

function foo() {
    // This would be identical to one's behaviour
    console.log(yield* do* {
       yield null
    })
}

const values = do* { yield 1; yield 2; 3 }
values.next() // { value: 1, done: false }
values.next() // { value: 2, done: false }
values.next() // { value: 3, done: true }

async function foo() {
    console.log(do {
        await fetch('foo.json').then(res => res.text())
     }) // SyntaxError await outside of async function
     console.log(await async do /* That's some keyword chaining that is */ {
         await fetch('foo.json').then(res => res.text())
      }) // Promise resolving to contents of foo.json
}

Three: Neither of the above, simply shorthand for immediately invoked normal arrow functions:

const values = do { yield 1; 3 } // SyntaxError
const body = do { let res = await fetch('foo.json'); await res.text() } // SyntaxError
const body = async do { let res = await fetch('foo.json'); await res.text() } Promise resolving to contents of foo.json

@iddan
Copy link

iddan commented Apr 13, 2017

So maybe we can vote on "do return"?

@erights
Copy link

erights commented Apr 22, 2017

Please transfer proposal to tc39

See tc39/proposals#44

@trustedtomato
Copy link

If we are in an async function, would await work in a do expression?

(async function(){
    const foo = do{
        await anAsyncFunc()
   }
   // Error or the foo is the awaited anAsyncFunc()?
})();

@ivan-kleshnin
Copy link

ivan-kleshnin commented Jul 5, 2017

Ok, guys, should let x = do {do { true }} resolve to true and if not – why?
Current Babel does not parse it and other authors aren't sure as well.

@rubencodes
Copy link

Is there any argument for why it shouldn't? Seems intuitive to me that it would...

@Jamesernator
Copy link

Jamesernator commented Jul 8, 2017

@ivan-kleshnin This happens because do-expressions must strictly be in expression position, it's the same reason why you can't write an immediately invoked function expression without wrapping it brackets:

function() { console.log("Hello") }() // Does not work
(function() { console.log("Hello")()) // Does work

let x = do { do { true } } // Does not work
let x = do { (do { true } }) } // Does work

Of course there's no spec text yet so it could still happen, Babel's parser (and v8's experimental implementation) are thus just guesses as to what the exact grammar might be, if there's no async variant I don't really see the purpose of having it in statement position when you could just use a block:

do {
    const greeting = "Hello!"
    console.log(greeting)
}

// vs just

{
    const greeting = "Hello!"
    console.log(greeting)
}

Although if there's an async variant I think it'd be rather nice for top-level execution e.g.:

module.exports = function myCoolFunction() {

}

if (require.main === module) {
    do async {
        // Execute my cool cli tool
    }
}

@ivan-kleshnin
Copy link

@Jamesernator thank you for the explanation – you gave a lot of good points.

I don't really see the purpose of having it in statement position when you could just use a block:

The original question was caused by the use of do in JSX.

@ecbrodie
Copy link

@dherman or anyone: I have a question about whether it is valid to use the do expression for an if statement WITHOUT AN else CONDITION. When I use a do expression without else, I get an eslint error for no-unused-expressions. I am using this code in my JSX for a React component, something like this (illustrative purposes only):

export default function myComponent(props) {
  return (
    <div>
      {do {
        if (true) {
          <p>If statement is true</p>
        }
      }}
      <p>I am always rendered</p>
    </div>
  );
}

I prefer to use the do construct over the unnatural, confusing usage of &&, as suggested by Facebook for React rendering. Is this a valid use case?

Note that babel/eslint-plugin-babel#13 may be related to this issue, but I don't know for sure yet until I understand more about the design of the do expression.

Thank you.

@ecbrodie
Copy link

@Jamesernator
Copy link

@ecbrodie There's no reason that wouldn't work, the completion value of if is already well defined, basically you see the result of anything a do-block could do just by eval-ing the body e.g.:

const x = eval(`
    if (true) {
        'banana'
    }
}
x // 'banana'

const y = eval(`
    if (false) {
        'banana'
    }
}

y // undefined

@streamich
Copy link

streamich commented Sep 9, 2017

Regarding this JSX example:

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

I got triggered so badly on this:

Especially nice for templating languages like JSX...

First, the ternary expression is million times better in this situation:

return (
  <nav>
    <Home />
    {loggedIn ? <LogoutButton /> : <LoginButton />}
  </nav>
)

Second, no need to use JSX, just use the HyperScript function h, which makes it another million times better:

return h('nav',
  h(Home),
  loggedIn ? h(LogoutButton) : h(LoginButton),
);

Or JSON-ML (hint: JSON is built into JavaScript by default; no need to use the defunct XML syntax in JavaScript):

return ['nav',
  [Home],
  loggedIn ? [LogoutButton] : [LoginButton],
];

Reported for this example.

just kidding ;)

@drhumlen
Copy link

Is there any activity on this? Or similar proposals elsewhere I could look at or try out?

I need this really badly -- especially when working with JSX.

@streamich A ternary is pretty nice when there's only 2 branches (true or false), but if there's 3-4-5 it quickly becomes completely unreadable and you're forced to make a helper function with return to avoid using a temporary variable.

One could argue that splitting things into functions is "right" anyway, but I want refactoring into functions/consts a conscious decisions, not something I have to do because of language restrictions. Inline is often easier to read than having to jump back and forth between intermediary helper functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment