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

(Extended proposal?) Ghost Methods #13

Closed
gilbert opened this issue Dec 7, 2015 · 20 comments
Closed

(Extended proposal?) Ghost Methods #13

gilbert opened this issue Dec 7, 2015 · 20 comments

Comments

@gilbert
Copy link
Collaborator

gilbert commented Dec 7, 2015

So I got this idea while fixing the promises example in response to #10. In short, here's a truncated example of the code I was editing:

fetchPlayers()
  .then( games => Promise.all(games) )
  |> catchError( ProcessError, err => [] )
  |> then( forEach(g => console.log(g)) )

function then (handler) {  return promise => promise.then(handler)  }
function forEach (fn) {  return array => array.forEach(fn)  }

I noticed the bottom boilerplate functions had something in common – they both received a parameter, just to call that parameter on a future object. Then I thought, what if you could do this concisely?

fetchPlayers()
  .then( games => Promise.all(games) )
  |> catchError( ProcessError, err => [] )
  |> .then( .forEach(g => console.log(g)) )

// No more boilerplate functions!

In other words, .then(x) is equivalent to obj => obj.then(x).

Is this crazy? Useful? Grammatically possible?

Just had to throw it out there.

@raganwald
Copy link

Creating an ad-hoc function that sends a method to its argument is a very common need. In the allong.es library, for example, there is a send function that removes those boilerplate functions. If you used send, it would look like this:

const send = allong.es.send;

fetchPlayers()
  .then( games => Promise.all(games) )
  |> catchError( ProcessError, err => [] )
  |> send('then', send('forEach', x => console.log(x))

A first-class syntactic alternative to using a library function is obviously "sweet."

This is a special case of partial application. It's kind of the inverse of JavaScript's .bind: You want to bind all of the arguments to a function except this. I like it, personally, and use it often.


One thing that is going to come up when exploring your syntactic sugar (beyond the question of whether this is compatible with JavaScript's existing parsing rules) is the question of why have a special rule just for the receiver of a method? If this is just partial application, why can't we leave whatever we want 'blank' to be filled in later?

In some languages, there is a "hole" character that turns any expression into a template function. It's usually _, but that has another meaning in JavaScript, so let's use ⬛️, the "black large square" emoji. Your example becomes:

fetchPlayers()
  .then( games => Promise.all(games) )
  |> catchError( ProcessError, err => [] )
  |> ⬛️.then(⬛️.forEach(console.log(⬛️)))

This transforms trivially into:

fetchPlayers()
  .then( games => Promise.all(games) )
  |> catchError( ProcessError, err => [] )
  |> __hole1__ => __hole1__.then(__hole2__ => __hole2__.forEach(__hole3__ => console.log(__hole3__)))

It's a little more flexible than a special syntax for the "Thrush," because it can handle a missing argument or arguments in any position.

Underscore implements a variation of this feature for partial application: _.partial.

@apaleslimghost
Copy link

This is a really useful construct, and something I really miss since moving away from LiveScript, which has this (as (.then (.foreach console.log))) as well as more general partially applied operators (plus5 = (+ 5), what Haskell calls operator sections).

@raganwald
Copy link

(.foreach console.log) is tricky, because console.log accepts multiple arguments, and .foreach supplies multiple arguments (the element and its index).

@RangerMauve
Copy link

Also, you can't call console.log with something other than console as the this arg. I think the es-function-bind syntax would be needed for that part.

@apaleslimghost
Copy link

Heh, I was being brief for the sake of clarity. (Node's console.log works fine without binding btw)

@dralletje
Copy link

Well, we don't really need a new syntax for this right?
This would be doable with an helper function like

const method = prop => (...args) => subject =>
  subject[prop](...args)

So the first example would become

fetchPlayers()
  .then( games => Promise.all(games) )
  |> catchError( ProcessError, err => [] )
  |> method('then')( method('forEach')(g => console.log(g)) )

Or am I totally missing the point here?

@dralletje
Copy link

Smaller 'syntax' would be

const m = (prop, ...args) => subject =>
  subject[prop](...args)

fetchPlayers()
  .then( games => Promise.all(games) )
  |> catchError( ProcessError, err => [] )
  |> m('then', m('forEach', g => console.log(g)) )

@raganwald
Copy link

@dralletje:

Absolutely, that's more-or-less exactly what send is/does.

@dralletje
Copy link

@raganwald Ahh dang, should have read your comment more thoroughly :)

@raganwald
Copy link

@dralletje:

Yes, but it's far more elegant in ECMAScript 2015. Win-win!

@barneycarroll
Copy link

FTR, this is how bind operator and arrows kinda solve this.

fetchPlayers()
  ::Promise.prototype.then( games => Promise.all(games) )
  ::Promise.prototype.catch( ProcessError, err => [] )
  ::Promise.prototype.then( players => 
    players::Array.prototype.forEach( x => console.log( x ) ) 
  )

@raganwald it's worth pointing out that arrows & the spread operator allow you to be much more this-free. For example, send in ES6 would be written as follows:

export default (methodName, ...args) =>
  (receiver, ...remainingArgs) =>
    receiver[ methodName ]( ...args, ...remainingArgs)

But really, arrows and spread make a lot of the low-level utilities defined in allong.es redundant. Javascript Allongé was an illuminating read — easily my favourite programming book — but using allong.es as a library didn't sit right for me: I always wanted to invoke the useful patterns inline rather than referencing the pre-defined functions. ES6 makes it incredibly easy to do that.

@raganwald
Copy link

Javascript Allongé was an illuminating read — easily my favourite programming book — but using allong.es as a library didn't sit right for me.

allong.es was extracted from the original versions of JavaScript Allongé, and it was written in/for ES5. So naturally, it solves many problems that don't exist in ES6. No surprise there. It also addressed some generaI programming ideas that weren't about circumventing ES5's accidental complexity, and those are just as interesting today. But if you prefer to use patterns inline over library functions... They are both fine approaches.

I rewrote JavaScript Allongé for ES6, and were I to write allong.es6, it would have a complete different set of utilities, like some of the class decorators. And then ECMAScript 2016 or whatever would come along, and it would be time to write allong.es7. But I don't expect to rewrite allong.es, so it's fair to say that if you're using ES6 or better, allong.es is deprecated.

@KiaraGrouwstra
Copy link

Something like this was implemented as a sweet.js macro in lambda-chop, using λ (edit: it's actually customizable) as an operator rather than @raganwald's ⬛️:

// Shorthand property lambdas
var names = arr.map(λ.name);

@KiaraGrouwstra
Copy link

Update: in the new, ES6-compatible version 1.0 of SweetJS, such a lambda macro could be implemented as follows:

syntax λ = function (ctx) {
  return #`x => x`;
}

... I'm sorry to say ⬛️ will not parse currently, but yeah, this is working for me.

One benefit of this macro is you no longer need to think about ensuring you keep using differently named variables so as to prevent name clashes; the macro hygiene takes care of this for you.

Edit: in this new version of Sweet, the pipeline operator itself cannot currently be implemented yet. This would require infix operators, which it seems are on the current todo-list.

@michaelmesser
Copy link

const λ = new Proxy({}, {
  get: function(r, prop){
    return function(...myArgs){
      return function (object) {
        return object[prop].apply(object, myArgs)
      }
    }
  }
})

λ.map(_ => _ + 1)([1,2,3])

This works for λ.x(y) but not x(y, λ)

@nmn
Copy link

nmn commented Oct 10, 2017

How about a simple helper to just extract all prototype methods for an object.

// this could probably be re-written
const thisify = (prototype) =>
  Object.keys(prototype)
  .filter(key => typeof prototype[key] === 'function')
  .map(key => ([key]: (...args) => (ctx) => prototype[key].apply(ctx, args)))
  .reduce((result, current) => Object.assign(result, current), {});

/*
fetchPlayers()
  .then( games => Promise.all(games) )
  |> catchError( ProcessError, err => [] )
  |> .then( .forEach(g => console.log(g)) )
*/

Promise._ = thisify(Promise.prototype);
Array._ = thisify(Array.prototype);

fetchPlayers()
  .then( games => Promise.all(games) )
  |> Promise._.catch( err => [] )
  |> Promise._.then( Array._.forEach(g => console.log(g)) )

@ljharb
Copy link
Member

ljharb commented Oct 11, 2017

You wouldn't want to mutate the globals, though; even in example code.

@littledan
Copy link
Member

Would it be OK to leave this for a follow-on proposal, or is it important to address this use case in this proposal?

@gilbert
Copy link
Collaborator Author

gilbert commented Nov 10, 2017

We should hold off and see what happens with the partial application proposal first.

@gilbert gilbert closed this as completed Nov 10, 2017
@trustedtomato
Copy link

Using the partial expression proposal, the .length could be written as #?.length, and the rest of those examples too. The partial application proposal is for functions, so I don't see that as a way to reduce these problems.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 24, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests