-
Notifications
You must be signed in to change notification settings - Fork 2
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
Comments
To me this looks very promising. Even thinking about JSX. To better understand the first example, I see you use const result = if (cond) {
yield 100;
}
else {
yield 10;
} Sounds better? Digging deeper, the |
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 |
This would be awesome. |
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 const firstLetterIsA = _ => _.name.startsWith( 'A' )
function* iterable()
{ yield* users.filter( firstLetterIsA ) } |
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.) |
here come the imperative vs functional again.. /rel https://github.com/tc39/proposal-pattern-matching |
I think that better to put an asterisk like 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 |
That makes very much sense to me if the * indicates an implicit 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 (just You could then also generate an 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. |
Yes. I believe that it is implicit do expression, but may be confusing indeed. |
Please don't add Since inception JavaScript Anyway, it isn't too hard to just build one's own 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 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 // 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 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 |
No asterisks! :-) |
@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`? |
@babakness I think that noise is generated due to too many functions names and function applications. 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
}
})) |
@ljharb Hey Jordan, this must be a warning from a linter, in Node I get
|
@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. |
@tasogare3710 This syntax would really confuse a newcomer to JavaScript. The asterisk is taking too many personalities. 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 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 ) |
@ljharb Hey Jordan index.js if(true) {
[1,2]
}
no errors |
Your example of “fib” only works when you can compute the yielded value in advance. |
@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 |
Give me an example how it breaks old code if |
Give me an example of how the functional fib example doesn't work. It works, tested. |
Your fib example works - im saying that your overall approach wouldn’t work to eliminate all the use cases of generators. |
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. |
@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. |
@TehShrike True... anyway, the point is that the syntax is confusing especially with |
As has been stated, they can't all return an implicit value - that would break old code. Either there's an explicit marker (like |
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 |
Where |
@babakness again you're missing the point. Obviously if an |
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 // this is invalid code, because of context
do {
if(true) {
function doBadThings(){}
}
}
(1) should not happen. () => 1 is implicitly () => return 1 Because of context. 1; doesn't become return 1; Likewise an implicit do is only applied where an |
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 (if(true) {
function doBadThings(){}
})
(1) which becomes (do {
if(true) {
function doBadThings(){}
}
})
(1) Again, we are opening up the |
Note that in existing (old) Javascript this is valid (function foo() {
return 1
})() but this is not function foo() {
return 1
}
() Context matters. |
So its not that we're wrapping Instead we are adding an implicit 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 |
@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
}
}
}
}
} |
@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 Consider Now consider CoffeeScript or 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. |
My biggest problem with the 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. |
@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.
in the end, if explicit do expressions can return generators, does'nt it need generator expressions? |
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 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 |
@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, Apparently, you seem to be in a different world from us. |
@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. |
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. |
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 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 |
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. |
Lol. |
@ljharb I didn't propose making parenthesis always express expression context. Parenthesis already group expressions. We can however group |
@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. |
@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. |
@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 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 { if( true ) {
function badStuff() {}
} else {
function reallyBadStuff()
}
}
() Raises an error. Because Let's not forget we're helping improve a language where [] == ![]
'0' == !'0' evaluate as '0' + 0
// '00'
'0' * 0
// 0 Anyway, back to 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. |
Anyway if being super insecure about breaking some crazy edge case that isn't even possible is what's bothering some... instead of a For example why not a const foo = do if (true) {1} else {2} Then there could be a Looks much better than 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 So |
@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 you don't add a star to 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 |
I think you’re misunderstanding do expressions; Braceless do expressions are indeed a possibility as well; implicit do expressions (ie, lacking the |
Let's move most of this discussion to the relevant issue in the relevant repo: 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;
}; |
It is a conditional compilation and the 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. |
In your first example, did you mean |
@ljharb Are you sure its I who misunderstands See link: This has been my point for a few discussions regarding context. Let there be an implicit Instead of seeing this, its "you don't understand" or "context is confusing" or "its not functional" all not true. All said, |
Just wanted to point out that although |
@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(){}) }) |
@clemmy that's right. @ljharb Then, 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 |
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
would be equivalent to:
Similarly, the following
would be equivalent to
The text was updated successfully, but these errors were encountered: