Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

About implicit do or do* expressions #1

Open
kasperpeulen opened this issue Oct 11, 2017 · 63 comments
Open

About implicit do or do* expressions #1

kasperpeulen opened this issue Oct 11, 2017 · 63 comments

Comments

@kasperpeulen
Copy link

I was also posting this in the do proposal:
tc39/proposal-do-expressions#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
Copy link

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
Copy link
Owner

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
Copy link

This would be awesome.

@ghost
Copy link

ghost commented Oct 23, 2017

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
Copy link

babakness commented Oct 23, 2017

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
Copy link

ghost commented Oct 24, 2017

here come the imperative vs functional again..

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

@tasogare3710
Copy link

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
Copy link
Author

kasperpeulen commented Nov 7, 2017

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 do* expression. And without, is an implicit do 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
Copy link

tasogare3710 commented Nov 10, 2017

@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
Copy link

babakness commented Nov 10, 2017

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
Copy link

babakness commented Nov 10, 2017

No asterisks! :-)

@ljharb
Copy link

ljharb commented Nov 11, 2017

@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
Copy link

@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
Copy link

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

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

@ljharb
Copy link

ljharb commented Nov 11, 2017

@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
Copy link

babakness commented Nov 11, 2017

@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
Copy link

@ljharb Hey Jordan

index.js

if(true) {
  [1,2]
}

$ node index.js
$

no errors

@ljharb
Copy link

ljharb commented Nov 11, 2017

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

@ljharb
Copy link

ljharb commented Nov 11, 2017

@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
Copy link

babakness commented Nov 11, 2017

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

@babakness
Copy link

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

@ljharb
Copy link

ljharb commented Nov 11, 2017

#1 (comment)

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

@babakness
Copy link

babakness commented Nov 11, 2017

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
Copy link

@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
Copy link

@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
Copy link

ljharb commented Nov 11, 2017

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
Copy link

babakness commented Nov 11, 2017

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
Copy link

babakness commented Nov 11, 2017

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

@ljharb
Copy link

ljharb commented Nov 11, 2017

@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 #1 (comment) until you understand it before objecting again.

@babakness
Copy link

babakness commented Nov 11, 2017

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
Copy link

babakness commented Nov 11, 2017

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
Copy link

Note that in existing (old) Javascript this is valid

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

but this is not

function foo() {
    return 1
}
()

Context matters.

@babakness
Copy link

babakness commented Nov 11, 2017

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 #1 (comment)

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
Copy link

@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
Copy link

@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
Copy link
Owner

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
Copy link

@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
Copy link

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
Copy link

@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
Copy link

@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
Copy link

ljharb commented Nov 15, 2017

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
Copy link

babakness commented Nov 15, 2017

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
Copy link

ljharb commented Nov 16, 2017

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
Copy link

Lol.

@babakness
Copy link

@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.

@babakness
Copy link

babakness commented Nov 16, 2017

@ljharb I've fully addressed your concerns in the single comment where you had an actual example. If you have other actual examples please bring them forward. Until then there is no basis for adding syntax noise to the language, especially given the relative consistency of other language on this issue.

@ljharb
Copy link

ljharb commented Nov 16, 2017

@babakness I'm afraid that the burden of proof is on you to prove it won't break the web, not the other way around. I'm saying that as a TC39 member I would actively block such a change from landing in the language, citing web compat concerns as well as clarity/maintainability concerns.

@babakness
Copy link

babakness commented Nov 17, 2017

@ljharb it sounds like you are signaling power rather than reason.

Languages like Haskell, Scala, Ruby, and Kotlin allow exactly what is being proposed here. On the contrary, this would improve legibility by offering a cleaner alternative to the ternary pattern.

If this is a clarity concern

const foo = if ( a > b ) { true } else { false }

How is this not?

const foo = do {
  if ( a > b ) { true } else { false }
}

Because do makes it more clear? I just don't see how that make its "more clear". It does resolve compatibility issues. But you can't do this in old Javascript anyway

const foo = if ( a > b ) { true } else { false }

Then the concern revolved around

if( true )  {
  function badStuff() {}
} else {
  function reallyBadStuff()
}
()

breaking old code. Really edge case here. This however, this isn't in the context of an expression. This context is already defined in the sense that do itself is only valid in such context. For example

do { if( true )  {
    function badStuff() {}
  } else {
    function reallyBadStuff()
  }
} 
()

Raises an error. Because if-else is not in an expression, this super edge case isn't an issue.

Let's not forget we're helping improve a language where

[] == ![]
'0' == !'0'

evaluate as true. Why? Because the language is very context sensitive. For example

'0' + 0 
// '00'
'0' * 0
// 0

Anyway, back to do and context. This does work

0 + do {
  if(true) {1} else {2}
}

So then so should this

0 + if(true) {1} else {2}

Simply not possible in old code.

(if(true) {1} else {2})

Neither is that. Parenthesis just happen to be a cleaner way to signal an expression.

@babakness
Copy link

babakness commented Nov 17, 2017

Anyway if being super insecure about breaking some crazy edge case that isn't even possible is what's bothering some... instead of a * a new alternative to the ternary operator can be created that uses plain language.

For example why not a do if?

const foo = do if (true) {1} else {2}

Then there could be a do switch, etc...

Looks much better than if* and old JavaScript can't possibly have done this. The concept is similar to

async function  ...

a keyword instead of something unattractive like a bizarre asterisk symbol which until generator functions pretty much just meant multiplication. If only generators could be

gen function

Then there could be

async gen function() { yield await...}

Anyway, yeah that ship has sailed. But its not too late to offer gen function as a nice alternative ;-p

Sodo if () {} could be equivalent to do { if () {} } all the existing context rules apply.

@kasperpeulen
Copy link
Author

kasperpeulen commented Nov 17, 2017

@babakness I will go through all your message another time, but in the meantime there seem to be some miscommunication. What I meant was that if meant implicit do expression and if* and for* meant implicit do generator expression. I think no one here proposed to put an asteriks on if* to mean a normal do expression, that would be a super inconsistent.

If you don't add a star to for*, then:

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

Would be translated to a normal do expression, and give a syntax error, because yield was not expected in this context.

@ljharb
Copy link

ljharb commented Nov 17, 2017

I think you’re misunderstanding do expressions; do { if (true) { function foo() {} } }() would work fine.

Braceless do expressions are indeed a possibility as well; implicit do expressions (ie, lacking the do) are not.

@kasperpeulen
Copy link
Author

Let's move most of this discussion to the relevant issue in the relevant repo:
tc39/proposal-do-expressions#9

That is this issue about implicit do expressions.

I think the only thing relevant to discuss in this repo is the below example:

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

And if this could be written like this:

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

@tasogare3710
Copy link

@ljharb

do { if (true) { function foo() {} } }() would work fine.

It is a conditional compilation and the foo block level function. In other words,

The following works well

var _ = do {
     if (true) {
         function foo () {
             return 1;
         }
     }
}

foo ();

However, the following is ambiguous.

(do {
     if (true) {
         function foo () {
             return 1;
         }
     }
}) ()

It can not distinguish between conditional compilation or function expression.

@ljharb
Copy link

ljharb commented Nov 17, 2017

In your first example, did you mean _()? Because foo is not in scope there. If you did mean that, then i don’t see how the second example is ambiguous at all.

@babakness
Copy link

@ljharb Are you sure its I who misunderstands do expressions?

See link:

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

This has been my point for a few discussions regarding context. Let there be an implicit do where it is possible. It is not possible precisely where we want it to be and therefore old code won't break. Where do is not possible all the old context rules stay the same and so your code example wouldn't break.

Instead of seeing this, its "you don't understand" or "context is confusing" or "its not functional" all not true.

All said, do if is fine too, its just not necessary.

@clemmy
Copy link

clemmy commented Nov 17, 2017

@babakness @ljharb

Just wanted to point out that although (do { function foo (){ return 1 } }()) is syntactically valid, it's going to throw a runtime error since the completion value of a function declaration is empty. I opened an issue here: tc39/proposal-do-expressions#13

@babakness
Copy link

@clemmy Interesting, I noticed that while

(function(){})

is valid javascript

(do{ function(){} })

is not, but using a named function

(do{ function foo (){} })

is and so is this

(do{ (function(){}) })

@tasogare3710
Copy link

@clemmy that's right.

@ljharb
If conditional compilation is enabled, the function is added to VariableEnvironment. When conditional compilation is disable, a block level functions are added to LexicalEnvironment.

Then, do {if (true) {function foo () {}}} () returns a function and can only be called if both conditional compilation and block level function are disable.

First, conditional compilation becomes invalid when the interpreter does not support it or when it is in strict mode.

Next, the block-level-function is disable only when the interpreter does not support it.

It is extremely rare if both are disable. That's because many interpreters have implemented these two correctly since these two were mentioned in chapter B.3 of the spec.

If you write ParenthesizedExpression, do { (function foo(){}) } always returns a functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants