sebmarkbage / ecmascript-generator-expression

Proposal for do Generator Expressions in ECMAScript. Work in progress. Edit Add topics
96 stars 2 forks source link

About implicit do or do* expressions #1

Open kasperpeulen opened 7 years ago

kasperpeulen commented 7 years ago

I was also posting this in the do proposal: https://github.com/tc39/proposal-do-expressions/issues/9

To me, it sounds most logical that if any of the if, switch, for, while, try keywords are used in an expression context, then it will be an implicit do expression construction:

So the following

const num  = if (cond) {
  100; 
} else {
  10;
};

would be equivalent to:

const num  = do {
  if (cond) {
    100; 
  } else {
    10;
  }
}

Similarly, the following

const iterable = for (let user of users) {
  if (user.name.startsWith('A'))
    yield user;
}

would be equivalent to

const iterable = do* {
  for (let user of users) {
    if (user.name.startsWith('A'))
      yield user;
  }
};
leonardfactory commented 7 years ago

To me this looks very promising. Even thinking about JSX.

To better understand the first example, I see you use if (cond) { 10; }, without yielding. Is this intentional? Maybe something like:

const result = if (cond) {
  yield 100;
}
else {
  yield 10;
}

Sounds better?

Digging deeper, the yield solves some odd behaviours shown in CoffeeScript about nested list comprehensions. Wondering if CS history (considering differences) would help in understanding this proposal eventual downsides/upsides.

sebmarkbage commented 7 years ago

If you put any of these in an outer generator function things start getting subtle.

function* generator() {
  if (users[0].name.startsWith('A'))
    yield user;
  }
  const iterable = for (let user of users) {
    if (user.name.startsWith('A'))
      yield user;
  }
  const result = if (cond) {
    yield 100;
  } else {
    yield 10;
  }
  yield 123;
}

That's something that the extra * { } helps avoid since there is another boundary to indicate that something special is going on and where the yield will end up.

babakness commented 7 years ago

This would be awesome.

ghost commented 7 years ago

im not sure, but is what you trying to propose is a substitute for Arrow Function?

const num
= params => params.cond1
         && params.cond2
          ? 100
          : 10

generators dont mix, but direct yield* of map, filter or reduce would probably be sufficient

const firstLetterIsA = _ => _.name.startsWith( 'A' )

function* iterable()
  { yield* users.filter( firstLetterIsA ) }
babakness commented 7 years ago

I don't think so...

const num = params => params.cond1
         && params.cond2
          ? 100
          : 10

console.log(num) // [Function]
console.log(num()) // 100 or 10
const num2 = if (params.cond1 && params.cond2) {
  100
} else {
  10
}

console.log(num2) // 100 or 10 

A programming example that really exemplifies the utility of this is Ruby. In fact one reason arrow function offer so much utility is that single line arrow function have the implicit return. It leads to better readability. Consider this


const hasNoRemainder = n => d => !n ? n : n % d === 0
const fizzBuzz = fizzNum => buzzNum => num => {
  const numHasNoRemainder = hasNoRemainder( num )
  return if ( numHasNoRemainder( fizzNum * buzzNum ) ) { 'fizzBuzz' }
    else if ( numHasNoRemainder( fizzNum ) ) { 'fizz' }
    else if ( numHasNoRemainder( buzzNum ) ) { 'buzz' }
    else { num }
}

There is a single return statement. Rather than this example, where each condition has an return statement.


const hasNoRemainder = n => d => n % d === 0
const fizzBuzz = fizzNum => buzzNum => num => {
  const numHasNoRemainder = hasNoRemainder( num )
  if ( numHasNoRemainder( fizzNum * buzzNum ) ) { return 'fizzBuzz' }
    else if ( numHasNoRemainder( fizzNum ) ) { return  'fizz' }
    else if ( numHasNoRemainder( buzzNum ) ) { return 'buzz' }
    else { return num }
}

The first example conveys the idea in a much clearer way, the function returns the value resolved from the condition check; this versus each condition check returns some value (or maybe not, resulting in bugs, etc.)

ghost commented 7 years ago

here come the imperative vs functional again..

/rel https://github.com/tc39/proposal-pattern-matching

tasogare3710 commented 7 years ago

I think that better to put an asterisk like if*, switch*, for*, while*.

const num  = if* (cond) {
  100; 
} else {
  10;
};

const iterable = for* (let user of users) {
  if (user.name.startsWith('A'))
    yield user;
}

but, I feel that extra "let" and "yield" appear compared to the comprehensions.

[for(n of "1234567890") if(n != "0" && n != "1") for(op of "+-/*") eval(`${n} ${op} ${n}`)]

[for(let n of "1234567890") if (n != "0" && n != "1") for(let op of "+-/*") yield eval(`${n} ${op} ${n}`)]

can let and yield be omitted?

kasperpeulen commented 7 years ago

I think that better to put an asterisk like if, switch, for, while.

That makes very much sense to me if the indicates an implicit `doexpression. And without, is an implicitdo` expression.

That would also solve the problem that @sebmarkbage describes above.

So then we would write this:

const iterable = for* (let user of users) {
  if (user.name.startsWith('A'))
    yield user;
}

Writing without a star (justfor) would then raise an error, as yield is not defined in that context.

You could then also generate an iterable like this:

const numbers  = if* (cond) {
  yield* [100,200];
} else {
  yield* [10,20];
};

The usecase is probably very little for the last example, but it would make this all very consistent and predictable.

tasogare3710 commented 7 years ago

@kasperpeulen

That makes very much sense to me if the indicates an implicit do expression.

Yes. I believe that it is implicit do expression, but may be confusing indeed. Also, there may be problems that I overlook.

babakness commented 7 years ago

Please don't add * to each if* etc.. that is, in my opinion, clunky and ugly.

Since inception JavaScript if returns void. So if it started to return the last evaluated value, no old code will break, since that code is just ignoring that value.

Anyway, it isn't too hard to just build one's own ifElse. For example, here is one that behaves similarly to Ramda's ifElse

const ifElse = testCondition => trueCondition => falseCondition => input => (
   testCondition(input) ? trueCondition(input) : falseCondition(input)
)

example:

const halveOnlyEvenValues = ifElse
   ( num => num % 2 === 0 ) // test condition
     ( num => num / 2 ) // true condition
     ( num => num )  // false condition

const val1 = halveOnlyEvenValues(10) // 5
const val2 = halveOnlyEvenValues(11) // 11

To emulate the if / else discussed here, one could do

const noInputIfElse = testCondition => trueCondition => falseCondition => 
  ifElse(testCondition)(trueCondition)(falseCondition)()

For example this

const num1 = 5
const val3 = if(num1 % 2 === 0) {
  num1 / 2
} else {
  num1
}

is essentially just

const num2 = 5
const val4 = noInputIfElse( _ => num2 % 5 === 0 )( _ => num2 / 2 )( _ => num2)

Which, upon looking over, probably isn't as nice as the way Ramda does it. Also, it isn't hard to build a variadic version of our ifElse.

// first, some standard helper functions
const always = a => b => a
const head = arr => arr[ 0 ]
const tail = arr => arr.slice( 1 )
const pipe = ( ...funcs ) => input => tail( funcs ).reduce( (a,b) => b(a), head( funcs )( input ) )
const compose = ( ...funcs ) => pipe( ...funcs.slice().reverse() )
const chunk = n => arr => !arr.length 
  ? [] 
  : [ arr.slice( 0, n ) ].concat( chunk( n )( arr.slice(n) ) )

// group list items into pairs
const pairListItems = chunk(2)

// multiIfElse 
const multiIfElse = ( ...conditions ) => defaultCondition => compose(
  ...pairListItems( conditions )
      .map( pairs => ifElse( pairs[ 0 ] )( pairs[ 1 ] ) )
)( defaultCondition )

Side note, this mulitIfElse can also serve as the basis to create a pretty neat switch.

Now the fizz-buzz example is just


const hasNoRemainder = n => d => !n ? n : n % d === 0
const fizzBuzz = fizzNum => buzzNum => num => 
  multiIfElse(
    numHasNoRemainder => numHasNoRemainder( fizzNum * buzzNum ),
      always('fizzBuzz'),
    numHasNoRemainder => numHasNoRemainder( fizzNum ),
      always('fizz'),
    numHasNoRemainder => numHasNoRemainder( buzzNum ),
      always('buzz'),
)( always(num) )(hasNoRemainder( num ))

or perhaps

const hasNoRemainder = n => d => !n ? n : n % d === 0
const numHasNoRemainder = num => hasNoRemainder( num )
const fizzBuzz2 = fizzNum => buzzNum => num => multiIfElse(
     always( numHasNoRemainder( fizzNum * buzzNum ) ),  always('fizzBuzz'),
     always( numHasNoRemainder( fizzNum ) ),  always('fizz'),
     always( numHasNoRemainder( buzzNum ) ),  always('buzz'),
)( always(num) )()

Anyway this


const hasNoRemainder = n => d => !n ? n : n % d === 0
const numHasNoRemainder = hasNoRemainder( num )
const fizzBuzz2 = fizzNum => buzzNum => num => 
  multiIfElse(
     always( hasNoRemainder( num )( fizzNum * buzzNum ) ),  always('fizzBuzz'),
     always( hasNoRemainder( num )( fizzNum ) ),  always('fizz'),
     always( hasNoRemainder( num )( buzzNum ) ),  always('buzz'),
)( always(num) )()

Would be awesome. It just feels like what JavaScript should do...

I just don't see the advantage to adding an unnecessary asterisks


const hasNoRemainder = n => d => n % d === 0
const fizzBuzz = fizzNum => buzzNum => num => {
  const numHasNoRemainder = hasNoRemainder( num )
  return if* ( numHasNoRemainder( fizzNum * buzzNum ) ) { 'fizzBuzz' }
    else if* ( numHasNoRemainder( fizzNum ) ) { 'fizz' }
    else if* ( numHasNoRemainder( buzzNum ) ) { 'buzz' }
    else { num }
}

Seriously why do this? (yuck!) What an eye sore! From the return function it is pretty clear what is going on. I'm not a huge fan of the generator syntax in Javascript. I pretty much avoid them. But at least there the function behaves quite differently. Here the if just implicitly returns. Why add line noise?

babakness commented 7 years ago

No asterisks! :-)

ljharb commented 7 years ago

@babakness you might be wrong about “no old code breaking”:

if (true) { [1,2] }
[1] // is this an array with `1`, or the index 1 on the array literal, `2`?
tasogare3710 commented 7 years ago

@babakness I think that noise is generated due to too many functions names and function applications. If do not need reusability, arrow function feels noisy.

Btw, may not need to put an asterisk in else if.

/*
 * code not to reuse
 */

function* fizzBuzz(max) {
    for (let i = 1; i < max; i++) {
        if ( (i % 3 == 0) && (i % 5 == 0) ) {
            yield 'FizzBuzz'
        } else if (i % 3 == 0) {
            yield 'Fizz'
        } else if (i % 5 == 0) {
            yield 'Buzz'
        } else {
            yield i
        }
    }
}

console.log(...fizzBuzz(1000))

console.log(...(for* (let i = 1; i < 1000; i++) {
    if* ( (i % 3 == 0) && (i % 5 == 0) ) {
        yield 'FizzBuzz'
    } else if (i % 3 == 0) {
        yield 'Fizz'
    } else if (i % 5 == 0) {
        yield 'Buzz'
    } else {
        yield i
    }
}))
babakness commented 7 years ago

@ljharb Hey Jordan, this must be a warning from a linter, in Node I get

> if (true) { [1,2] } [ 1, 2 ]

ljharb commented 7 years ago

@babakness that’s not in node, that’s in the node repl - repls already have the behavior you want; as such, testing in a repl is often useless.

babakness commented 7 years ago

@tasogare3710 This syntax would really confuse a newcomer to JavaScript. The asterisk is taking too many personalities. function* is a generator. With for* it returns an implicit

First off, I'm going to argue that the entire generator syntax is unnecessary on top of being ugly. Take a n iterative solution for the Fibonacci sequence


function* notElegantFibGen() {
  let current = 0;
  let next = 1;
  while (true) {
    yield current;
    [current, next] = [next, current + next];
  }
}

Which is used as follows

const iterativeFibGen = *notElegantFibGen()
iterativeFibGen.next() // {value: 0, done: false}
iterativeFibGen.next() // {value: 1, done: false}
iterativeFibGen.next() // {value: 1, done: false}
iterativeFibGen.next() // {value: 2, done: false}
....

Yet this is not necessary. Its also a bad idea. The algorithm to generate Fibonacci numbers is not re-usable. What if we just want the 100th number in the sequence? Here is a functional approach that gives both a "generator" like function using a state machine as well as a ready to use, efficient, standalone Fibonacci function.

const fibCalculator = (a, b, n) => n 
   ? fibCalculator(b, a + b, n - 1) 
   : a
const fib = index => fibCalculator(0,1,index)
const getFibGen = () => {
  let index = 0
  return () => fib(index++)
}

The "generator" part can be used as follows

const genFib = getFibGen()
genFib() // 0
genFib() // 1
genFib() // 1
genFib() // 2
...

Now if we want the 100th Fibonacci number directly, its just

fib(100) // 354224848179262000000

One could argue that a generator function can be used with a function as well. Which is true, but just compare the functions side by side

function functionalStyle() {
  let index = 0
  return () => fib(index++)
}

Versus

function* generatorStyle() {
  let index = 0
  while(true){
    yield fib(index++)
  } 
}

No strange syntax, less code.

As for the whole for* business and making it assignable or return values. One could just abstract the for loop away. For example

const times = callback => numberOfTimes => {
  let returnValues = []
  for( let index=0; index < numberOfTimes; index++ ){
    returnValues.push( callback( index ) );
  }
  return returnValues
}

Now if we want the first 15 Fibonacci numbers its just

const getFibSequence = times( getFibGen() )
getFibSequence( 15 )
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]

And we get a fib sequence generator for free 😄

With our fizzBuzz examples it's simply

const threeFizzFiveBuzz = fizzBuzz( 5 )( 3 )
const sequenceFizzBuzz = times( threeFizzFiveBuzz )
sequenceFizzBuzz( 16 )
babakness commented 7 years ago

@ljharb Hey Jordan

index.js

if(true) {
  [1,2]
}

$ node index.js $

no errors

ljharb commented 7 years ago

Your example of “fib” only works when you can compute the yielded value in advance.

ljharb commented 7 years ago

@babakness right, there’s no errors - if suddenly it yielded a value, and it was followed by another set of square brackets, it would start to throw errors. That’s what’s meant by “breaking old code”, and that’s why making if suddenly start implicitly returning a value would break the web.

babakness commented 7 years ago

Give me an example how it breaks old code if if starts returning values

babakness commented 7 years ago

Give me an example of how the functional fib example doesn't work. It works, tested.

ljharb commented 7 years ago

https://github.com/sebmarkbage/ecmascript-generator-expression/issues/1#issuecomment-343636788

Your fib example works - im saying that your overall approach wouldn’t work to eliminate all the use cases of generators.

babakness commented 7 years ago

Ah, ok, give me an example where a generator function does something that can't be done using functions.

Anything iterative can be done using recursion, this is a proven mathematical fact. Generators translate into some machine code that can be implemented in a Turing machine and Lambda Calculus is Turing complete. Ergo, it works.

But you may have a point if you can find an example where the generator example that is more elegant. Maybe there are cases out there. So, to support your statement, please produce this example. Thanks.

TehShrike commented 7 years ago

@babakness I don't think this is the place to argue that generator functions shouldn't have been added to the language, that ship has sailed already.

babakness commented 7 years ago

@TehShrike True... anyway, the point is that the syntax is confusing especially with for* switch* being proposed here. If they all return an implicit value, great, sound good to me. Let's not add more ugly syntax.

ljharb commented 7 years ago

As has been stated, they can't all return an implicit value - that would break old code. Either there's an explicit marker (like do { }), or there's no new feature.

babakness commented 7 years ago

If you don't inject new syntax into old code that doesn't expect a value, it doesn't break old code. Again, please enlighten me on how this breaks existing code.

Just detect assignment (or return) then return const foo = if(true){ [1,2] }. Nobody wrote old working JavaScript that does this. So writing this moving forward can't break old code.

babakness commented 7 years ago

Where const foo = if(true){ [1,2] } breaks so does const foo = if*(true){ [1,2] }. So why add * ?

ljharb commented 7 years ago

@babakness again you're missing the point. Obviously if an if statement is used in expression position, that's new code. I'm talking about ASI issues around it being used for decades now in statement position. Please re-read https://github.com/sebmarkbage/ecmascript-generator-expression/issues/1#issuecomment-343636788 until you understand it before objecting again.

babakness commented 7 years ago

@ljharb No points are being missed on this end. So are you saying that

if (true) { [1,2] }
[1]

The old javascript, now in a new parser would become something like

if (true) { [1,2] }[1]

If so, as I said above, there is no return or assignment so that example stays the same as before

if (true) { [1,2] }
[1]

in the new parser. Nothing is changed because no value is utilized. But this


const foo = if (true) { [1,2] }
[1]

Because const foo = if (true) { [1,2] } in old JavaScript is a syntax error, this would never happen in old code. In new code we detects assignment. Which should become

const foo = do {
  if (true) {
    [1,2]
  }
}
[1]

In the new code foo is 2 which it should be. If the user wants to rely on ASI they need to prefix [...] and (...) with a semicolon. Again, this can't happen in old code. The proposal posted at the top of this thread proposes turning this:

const num  = if (cond) {
  100; 
} else {
  10;
};

into:

const num  = do {
  if (cond) {
    100; 
  } else {
    10;
  }
}

Which is rad, lets do this. Forgive the pun. 😄


Summary:

  1. Assigning or a return before if is a syntax error in old code. It doesn't occur there.
  2. No need to return a value where void is expected. Where if is not assigned or returning any value, nothing changes. Consequently, old code works just fine.
  3. Where assignment or a value is expected or returned, we make the above transformation. No ugly * is necessary.

Cheers. B

TehShrike commented 7 years ago

@babakness this specific issue is about implicit returns. That means that this issue is about changing if (true) { [1,2] } from evaluating to void, into evaluating to [1, 2], without any return/yield statements.

babakness commented 7 years ago

@TehShrike Right. No yield should be necessary. The behavior of the existing yield inside a generator would be the same.

The main concern @ljharb has is with old code. Old code can't assign to if or switch statements. No need to return a value where void is expected. For the implicit do statement you have to either return or make an assignment.

I think a more interesting discussion is the explicit return inside the if assignment.

// bad style but worth discussion
function foo(bar) {
  const whatever =  if(bar <= 0 ) {
    'too little'
  } else if (bar >9000) {
    return over9000(bar)
  } 

 return `${ whatever } is just enough`
}

The line return over9000(bar) is returning from foo the value returned by said function and not the value ${ whatever } is just enough. It is bad style to write code like his but what this does is worth discussion. My notion of what this does is borrowed from ruby which also has implicit returns

ljharb commented 7 years ago

It would be absurd to only detect assignment or returns; there's statement position and expression position, and if can't appear in expression position. Allowing it to return a value anywhere would mean it could appear anywhere an expression can, including foo(if (true) { [2] }); for example.

babakness commented 7 years ago

@ljharb Look at the original post at the top of this page.

Even if you did it everywhere there is no conundrum. In your example it in fact would become

do {.... } [1]

which if we assumed you could start a line with do is 2 and equivalent to the number 2 sitting in a line of JavaScript not assigned, not returned, and doing nothing at all. It does nothing.

In Babel stage-0, a do expression not being assigned result in an error.

https://babeljs.io/repl/#?babili=false&browsers=&build=&builtIns=false&code_lz=MYewdgzgLgBAZiEMC8MAmSDeAoGMCWcAFFAE4CuApgJQyYwDaAjADQBMAugNwwC-X2Xsw7ZsAejEwAtiFKUYAG3xhKEdCGUBzGGBBQAFluxtjpk-Mm7YAQwUKQAd0poY1sAE8H199gx1cBMRkVLT0zOzcfAK8QA&debug=false&circleciRepo=&evaluate=false&lineWrap=true&presets=es2015%2Creact%2Cstage-0%2Cstage-2&targets=&version=6.26.0

So we are only putting in an implicit do in accordance to stage-0 where it is acceptable syntax at the moment. Again even if it were acceptable everywhere, still, no issue. The end result is that you don't even need do or ugly * symbols polluting Javascript code from now onward. It isn't absurd. We disagree there.


edit:

I am not suggesting doing this everywhere and was only responding to the notion that context sensitivity is absurd. It is not correct that blindly returning everywhere is harmless (see below) and my previous summary was correct. To jackhammer a * into Javascript is not necessary when context does matter

babakness commented 7 years ago

As a follow up to my last comment, I stick by saying that only where a value is expected should there be an implicit do. It is possible to concoct code that would work before and break now if there is no assignment in the form of


if(true) {
  function doBadThings(){}
}
(1) // not sure why someone would do this in OLD javascript but there you have it.

Not sure why someone would do that in old code; however, sticking with how do works today, it isn't valid on a standalone line. So this


// this is invalid code, because of context
do {
  if(true) {
    function doBadThings(){}
  }
}
(1) 

should not happen. do is not implicit where it can't be and it can't be there (see above link). I see no problem with implicit statements. For example

() => 1

is implicitly

() => return 1

Because of context.

1;

doesn't become

return 1;

Likewise an implicit do is only applied where an if, switch, or for statement is being assigned to or returned. It is worse to include a cryptic and ugly * everywhere than the do have implicit do statements when assigning directly to an if, switch etc....

babakness commented 7 years ago

If someone wanted this effect


if(true) {
  function doBadThings(){}
}
(1) // not sure why someone would do this in OLD javascript but there you have it. In this context, `if` does what it would do under any case

In NEW Javascript, they could do what would make do valid here, wrap it in parenthesis


(if(true) {
  function doBadThings(){}
})
(1) 

which becomes


(do {
 if(true) {
  function doBadThings(){}
 }
})
(1)

Again, we are opening up the if statement to a new context use. Old cases do not allow this context and so there is no issue. To object please provide a clear example that would cause problems in the old Javascript and that only a modifier could solve.

babakness commented 7 years ago

Note that in existing (old) Javascript this is valid

(function foo() {
    return 1
})()

but this is not

function foo() {
    return 1
}
()

Context matters.

babakness commented 7 years ago

So its not that we're wrapping if statements in do when used in assignments.

Instead we are adding an implicit do when a statement is used in an expression context. The validity of which is differed to the validity of do itself. Do is evaluating a statement as an expression.

So this we are actually proposing here that this

myAwesomeFunct( 
  (if (x > 10) {
   true
  } else {
   false
  })
)

becomes

myAwesomeFunct( 
  (do{(if (x > 10) {
   true
  } else {
   false
  })})
)

which in turn becomes just

myAwesomeFunct( x> 10 ? true : false)

However the example given by @ljharb in https://github.com/sebmarkbage/ecmascript-generator-expression/issues/1#issuecomment-343636788

is not a problem because the statement is not inside of an expression. Currently that is not allowed in Javascript, which is as the heart of this proposal.

(if (true) { [1,2] })
[1]

is not valid in Javascript as it currently stands. So old code can't do this. Moving forward that would become

( do { if (true) { [1,2] } })
[1]

which is in turn

(true ? [1,2] : undefined)[1]

Assignments to statements should therefore be in expression context

const foo = (if ( x > 10 ) { true } else { false })

In a second part of the proposal, everything on the right side of an assignment is implicitly an expression or in an expression context moving forward. Old code can't do this. But new code see this

const foo = if ( x > 10 ) { true } else { false }

as this

const foo = (if ( x > 10 ) { true } else { false })

which is this

const foo = ( do { if ( x > 10 ) { true } else { false } })

and becomes (in this case) simply

const foo = x > 10 ? true : false
tasogare3710 commented 7 years ago

@babakness Is * so ugly? I think that it is not much different from =>.

I can understand your idea, but explicit do expr is too nested. At least, if we do not adopt *, I think that we need other suggestions.

// explicit do expr too nested
function* gen(arr) {
    yield* do* {
        if (cond) {
            yield* do* {
                for(let v of arr) {
                    yield v * v
                }
            }
        } else {
            yield* do* {
                for(let v of arr) {
                    yield v + v
                }
            }
        }
    }
}
babakness commented 7 years ago

@tasogare3710 this same issue can happen today if a regular function with many nested if statements the return a value for various conditions. The only remedy is breaking the code into smaller composable functions.

Why reinvent the wheel? Let's look at something that already exists. The ruby language already does this and the community has its own style guide and best practices. I see no advantage in adding superfluous line noise into JavaScript. A language has already gone the way of line noise, called perl and many people jumped ship to python. A language with minimal syntax noise.

Consider php. A language that borrowed for perl It adds $ everywhere. Methods are called using ->. It's ugly. Super ugly.

Now consider CoffeeScript or coffee. It tries to combine elements of ruby and python and move JavaScript forward in this philosophy. This is where the fat arrow syntax of es6 comes from. CoffeeScript already does implicit returns and it works. Writing deeply nested if statements is often a sign that the code can be refactored. If anything, it should not be encouraged; and certainly not with line noise as manditory syntax.

We can add implicit return too and do it in a way that doesn't break old code by focusing on statement context as described above. Let's not litter symbols into JavaScript. We have an opportunity to make JavaScript a cleaner language. Just as fat arrow functions and implicit returns have done. The new pipe operator now moving to stage-0 helps remove the noise of nesting function calls, etc.

Here too we can improve readability. Some people act like it's too good to be true that we can do implicit returns without add noise.

We can.

We are adding the ability for the parser to evaluate statements as expressions in contexts where not before possible. And if statement in a standalone line is not in an expression context. Wrapping it in parenthesis is and that doesn't work in old JavaScript. Since old code Would break if it did this, we can safely move forward as a clean and natural extension of the language.

sebmarkbage commented 7 years ago

My biggest problem with the * isn't the esthetics per se. It's that it is very unusual coming from other languages. No one else does that.

It looks like something weird that you have to learn to understand.

I'm concerned about people coming from other languages being turned off. JavaScript has the nice property that once you read it you can mostly figure out what's going on.

Without the star the edge cases are weird but if you read the code you can more or less understand what's going on. If you've seen the yield keyword in other languages.

With the star, it becomes something exotic and strange.

tasogare3710 commented 7 years ago

@babakness Again, I can understand your idea, but I think that idea can say the same for generator expressions as well as asterisked implicit do expressions.

You before, said generator expressions that there is unnecessary. I no longer know whether generator expressions is really necessary.

@sebmarkbage

It's that it is very unusual coming from other languages. No one else does that.

It looks like something weird that you have to learn to understand.

With the star, it becomes something exotic and strange.

* {Yield 2}; also I think can say the same thing. I no longer know more and more.

in the end, if explicit do expressions can return generators, does'nt it need generator expressions?

babakness commented 7 years ago

I originally stated generators themselves were not particularly necessary when doing functional programming. I was illustrating that we should help JavaScript become more expressive and grow into its functional roots rather than keep changing it to conform to a procedural way of thinking. This way we can stop adding line noise to the language. Javascript is a misunderstood language. It stems from Scheme not Java. It is often taught incorrectly.

Consider for example the "callback hell" problem that async/await and Promises solve. Essentially async/await is operating like a promise then/catch pattern. Yet you could do the same thing by currying functions that take callbacks and passing the input of one function from the output of another. Indeed my multiIfElse example above is sort of like how promises work under the hood. The multiIfElse function is basically nested ifElse functions. The reason they can be expressed in a more linear fashion is because we are composing curried functions. And compose is basically a glorified reduce! So async/await is ultimately syntax sugar for reduce :-p

Anyway I agree that syntax sugar is helpful--so long as it makes the language become more beautiful, easier to read, and so on. Often times, form is function.

But if you write ES6 expressions today, yes then yield it is necessary as is the unfortunate * symbol. Generator functions yield. So you would yield instead of return if-else statements from do expressions. If we automatically wrap if-else statements in expression context with do then you don't need do since it's automatically there and can return or yield the evaluated result.

tasogare3710 commented 7 years ago

@babakness In this issue no one has argued that the function is wonderful (About implicit do or do* expressions). Javascript is not functional programming. Function is just a first class citizen.

Originally, to me it does not have to be asterisk, do,{and}instead marker.

Apparently, you seem to be in a different world from us.

babakness commented 7 years ago

@tasogare3710 hey there. So JavaScript doesn't have the functional tools that a language like Haskell offers, there is no lazy evaluation, enforcement of immutability, etc. but you can totally write in a functional style. There are also proposals and libraries to get it there. Libraries like Ramda and Lazyjs. Pattern matching proposals

https://github.com/tc39/proposal-pattern-matching

Pipe operators

https://github.com/tc39/proposal-pipeline-operator/blob/master/README.md

You can enforce immutability. You can even make JavaScript a typed language with TypeScript.

I like JavaScript. There is a growing movement in working with the language in an ever more functional style. There are now many libraries for handeling asynchronous events as streams, reactive programming libraries and so on. Also consider the growing popularity of Vue and React... So I'm definitely with "us". Consider joining the other "us" :-)

Back on topic, please explain why any marker or astrerisk is necessary. All that I see as needed is paranethesis to explicitly define an expression context when it not already implied.

ljharb commented 7 years ago

Because things without a marker already have a meaning, and conflating two meanings with the same syntax is unclear and hard to understand, and is also a very un-functional approach. Explicit > implicit.

babakness commented 7 years ago

Context driven behavior is already JavaScript and parenthesis indicate context

const foobar = a => b => a + b

The return is implicit. This is still declarative. You maybe confusing declarative for explicit.

Example from Haskell

a b c d = e f g h

What does this do in Haskell? Obviously you have to know some implicit syntax rules. How about this

a b c d = e f $ g h

More rules.

Back to the issue at the top of the page. In Haskell

isEven n = if mod n 2 == 0 then True else False

That's pretty explicit, one could also write

isEven n = mod n 2 == 0

Still declarative.

Back to Javascript, using the ifElse function I've defined in my previous post

const isEven = ifElse( n => n % 2,  always( true ),  always( false ) )

Looks pretty declarative and functional to me

// compare to Haskell example
// isEven n = if mod n 2 == 0 then True else False
const isEven = n =>  if (n % 2 ) { true } else { false }

Looks pretty declarative and functional to me. Would be nice to have this above example. Notice this mirrors the Haskell example. Its also more verbose and explicit.

// compare to Haskell example
// isEven n = mod n 2 == 0
const isEven = n => n % 2 === 0

Yep, still declarative and functional.

Look, I don't want to get caught up in a semantical debate. That would be fruitless. Many languages don't rely on line noise to make "clear" what's going on.

Check this out in Kotlin

val x = if (a > b) a else b

Check this out in Scala

val x = if (a > b) a else b

Check this out in Ruby

x = if a > b then true else false end

Check this out in Rust

let x = if a > b { true; } else { false; };

etc...

In short it's more clear and consistent by not adding line noise to JavaScript when assigning in expression context. Using parenthesis to explicitly declare an expression context, which currently is not allowed for statements in JavaScript like it is in many other language. This extends to each respective language's equivalent for doing things like switch which might be by using guards or otherwise. Point is, they let you assign directly.

ljharb commented 7 years ago

Using parens to declare an expression context is not possible in JS; it would break too much code - way more than "making certain statement-based control structures expressions" would.

babakness commented 7 years ago

Lol.

babakness commented 7 years ago

@ljharb I didn't propose making parenthesis always express expression context. Parenthesis already group expressions. We can however group if to imply it is an expression where you'd have to use a line noise characters instead.