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

Naming bikeshed #1

Open
bakkot opened this issue Nov 11, 2021 · 40 comments
Open

Naming bikeshed #1

bakkot opened this issue Nov 11, 2021 · 40 comments
Labels
bikeshedding Discussion about naming or similar

Comments

@bakkot
Copy link

bakkot commented Nov 11, 2021

Never too early for a naming bikeshed! (This need not be resolved until going for stage 3, of course.)

I've always called this uncurryThis, which I think is a more precise description of what it does than unThis. That's also what I've seen it called elsewhere: x, x, etc.

@js-choi
Copy link
Owner

js-choi commented Nov 11, 2021

(See also prior discussion to tc39/proposal-function-helpers#17.)

I would be more comfortable with using the word “uncurry” if “curry” was a term used anywhere else in the core language, but it’s not used anywhere else in the language, and so that’s at least two steps for a learner to understand: (1) what function currying is and (2) what “uncurrying” therefore is. Someone cannot understand “uncurrying” without understanding “currying”, which is a concept that is present nowhere else in the core language. Yes, currying is common enough in a lot of (but not all) functional-programming styles, but, as you know, several TC39 members are against introducing currying elsewhere in the core language (due to performance concerns, ecosystem-schism concerns, aesthetic concerns, etc.); see tc39/proposal-pipeline-operator#221. And unless currying is present elsewhere in the core language, I am reluctant to introduce uncurrying to the core language too. 🍛

@ljharb’s “callBind” library is also often used, but the “callBind” name comes from how callBind(fn) is equivalent to fn.call.bind(fn), which is also pretty esoteric.

“unThis” is the simplest name I can think of right now that describes what it does: it gets rid of this from functions that previously used this; it un-this-ifies functions.

But I’m open to whatever name. 🎨🚲🏠

@ljharb
Copy link

ljharb commented Nov 11, 2021

I very much do not like "uncurryThis", because currying is a confusing advanced concept that we shouldn't be promoting, and understanding "uncurry" requires one understands "curry".

I also don't like unThis, it doesn't get rid of this from the function it's applied to - it just unshifts the receiver into the first argument position.

I certainly prefer "callBind" - the reason I named the package that way is because it's literally Function.call.bind(f).

@js-choi js-choi added the bikeshedding Discussion about naming or similar label Nov 11, 2021
@js-choi
Copy link
Owner

js-choi commented Nov 11, 2021

I also don't like unThis, it doesn't get rid of this from the function it's applied to - it just unshifts the receiver into the first argument position.

@ljharb: Your point here is well taken, but I would say that insofar that we can call a function that uses the this binding a “this-using function”, then any function returned by unThis or whatever is always a function that never uses the this binding (a “non-this-using function”). So I think we can really think of it as a function in which the this binding used to be involved but now is no longer involved (as with all bound functions).

The original function doesn’t change, and a new function is returned, but we do often use verbs to denote pure non-mutating functions as if they were mutating its input in place, instead of returning a new copy. (We could take a page from proposal-change-array-by-copy and call it withoutThis or withThisAsArg, I guess.)

But if you think that callBind is easier to teach and reason about, then I certainly will take that into consideration.

  • fn.callBind()
  • fn.uncurryThis()
  • fn.unThis()
  • fn.withoutThis()
  • fn.withThisAsArg()
  • fn.thisAsArg()

@ljharb
Copy link

ljharb commented Nov 11, 2021

if you think that callBind is easier to teach and reason about

I do, because of the way it'd be naively polyfilled, as Function.call.bind(f) or f.call.bind(f).

@theScottyJam
Copy link

theScottyJam commented Nov 11, 2021

I don't like callBind that much. It's based on the "call" function with already provides no descriptive value for what it does, and is easily confused with the apply function. Then we're tacking on "bind" after it, and we're left with an even worse name.

I hope the bind-this syntax proposal can help people forget that .call() and .apply() was ever a thing, and we can provide a better name from the start for a function like this, instead of using a name that harkens back to functions that'll become almost useless once bind-this syntax is in.

Edit: I'll also add, the first time I saw a callBind definition, I almost had to set down with a pencil and paper to figure out what was going on. (the .call() make the "this" arg the first param, but then the bind is binding a this arg, but wait, it's being called on .call(), not on the instance, so the "this" value we're dealing with in .bind() is a different one then what .call() is dealing with, and... now I'm lost)

@bakkot
Copy link
Author

bakkot commented Nov 11, 2021

I strongly dislike callBind. It's a.) naming it after an implementation detail and b.) requires users to know what call and bind do.

@bakkot
Copy link
Author

bakkot commented Nov 11, 2021

A problem with unThis is that it implies it would only work once, and then be a no-op (and would be a no-op on an arrow or any other function which already doesn't use this). But that's not actually what's proposed (I assume): every time you call it uncurries this again, pushing the original parameters out by one.

@theScottyJam
Copy link

theScottyJam commented Nov 11, 2021

unThis does feel like a name I could get used to, but I agree that it's a little confusing on first site. It sort of reads as "take this function, and make it so it doesn't accept a 'this' anymore", which is only half correct.

The word "uncurry" doesn't feel like it belongs here either. I see the relationship, but it sort of stretches its definition.

thisAsArg() is so far my favorite. It's a bit of a mouthful, but it's still short, and it's clear. But, I do still like unthis().

@ljharb
Copy link

ljharb commented Nov 12, 2021

imo it's not an implementation detail - "binding Function.prototype.call" is the semantics it has. It does require users know what call and bind do, but I don't see why that's a problem - this proposal already requires users to know pretty intimately how function calling and this works.

@theScottyJam
Copy link

theScottyJam commented Nov 12, 2021

Another problem with callBind is that it doesn't even do a great job of explaining the semantics either. Someone who has an intuition of .call() and .bind(), and who sees .callBind() could very well assume that the underlying implementation went something like this:

Function.prototype.callBind = function(thisArg, ...args) {
  return this.call(thisArg, ...args).bind(thisArg)
}

or some other similar implementation. There's lots of ways to combine those two actions.

The name "callBind" sounds like you're first using .call(), then you're using .bind(). Similar to how .flatMap() has you first use .flat() then use .map(). In reality, you're not actually calling .call(), you're just binding to it.

@ljharb
Copy link

ljharb commented Nov 12, 2021

The actual semantics of flatMap are "map, then flat".

@theScottyJam
Copy link

Whoops, you're right. Still though, you're performing both of those actions, while a callBind is not doing both a .call() and a .bind().

@js-choi
Copy link
Owner

js-choi commented Nov 12, 2021

A problem with unThis is that it implies it would only work once, and then be a no-op (and would be a no-op on an arrow or any other function which already doesn't use this). But that's not actually what's proposed (I assume): every time you call it uncurries this again, pushing the original parameters out by one.

@bakkot: If unThis that strongly implies an in-place mutation of the original function object, then how do you feel about .thisAsArg() (or .withThisAsArg())? I think they do a better job avoiding that mutating-the-original feeling.

@bakkot
Copy link
Author

bakkot commented Nov 12, 2021

strongly implies an in-place mutation of the original function object

Sorry, I didn't mean to say it implied in-place mutation. I don't think it does. Rather, the problem is that it implies that f.unThis().unThis() will behave the same as f.unThis(), or that arrow.unThis() will behave the same as arrow, neither of which are true.

@js-choi
Copy link
Owner

js-choi commented Nov 12, 2021

Right, I see now. Do you feel that fn.thisAsArg().thisAsArg() would have the same problem?

@bakkot
Copy link
Author

bakkot commented Nov 12, 2021

Not to the same extent, no.

@VitorLuizC
Copy link
Contributor

VitorLuizC commented Nov 13, 2021

What about uncouple, decouple or even disengage?

I've always think in this approach as transforming a method "coupled" to a class/prototype in a function that can be used with any instance of that class. Like uncoupling map from Array.prototype to use with any ArrayLike

@jridgewell
Copy link

I'd throw in devirtualize for method devirtualization.

@zloirock
Copy link

I used unbind for a name of a such method.

@theScottyJam
Copy link

@jridgewell - could you explain the devirtualize one? I'm not sure how that term fits, probably because there's a meaning to "virtual" that I'm unaware of.

@zloirock - I don't think unbind would work. It implies it'll do the opposite of .bind(), e.g. fn.bind(x).unbind() should bring you back to fn, which it doesn't.

@shuckster
Copy link

const silo = {
  launch: function() {
    this.launchTheMissiles();
  }
}

// Half serious
const launchFrom = silo.launch.solo()
const launchFrom = silo.launch.lifted()
const launchFrom = silo.launch.extracted()
const launchFrom = silo.launch.aside()
const launchFrom = silo.launch.descoped()
const launchFrom = silo.launch.pigeon()

// Quarter serious
const launchFrom = silo.launch.pulled()
const launchFrom = silo.launch.detached()
const launchFrom = silo.launch.elevate()
const launchFrom = silo.launch.privilege()
const launchFrom = silo.launch.appropriated()
const launchFrom = silo.launch.freed()
const launchFrom = silo.launch.spared()
const launchFrom = silo.launch.loosed()
const launchFrom = silo.launch.extricated()
const launchFrom = silo.launch.unbuckled()

// Not very serious, and also made up
const launchFrom = silo.launch.flomp()
const launchFrom = silo.launch.grunk()
const launchFrom = silo.launch.tream()
const launchFrom = silo.launch.yump()
const launchFrom = silo.launch.wat()
const launchFrom = silo.launch.bliggit()
const launchFrom = silo.launch.skriff()
const launchFrom = silo.launch.rontered()
const launchFrom = silo.launch.loffle()

I like "pigeon", because silo.launch is stuck in its pigeon-hole, and by directly accessing the pigeon you can pop it into different holes. 🐦

@jridgewell
Copy link

could you explain the devirtualize one? I'm not sure how that term fits, probably because there's a meaning to "virtual" that I'm unaware of.

Virtualized methods are methods which have to lookup the function value from an object at runtime:

const foo = { name: 'foo', method() { console.log('bar', this.name); } };
const baz = { name: 'baz', method() { console.log('qux', this.name); } };

function test(obj) {
  return obj.method();
}
test(foo);
test(baz);

The actual .method() call is dependent on the object we attempt to call on. Devirutalized methods are static, with a known function value being used:

const method = function(context) {
  console.log('static', context.name);
}

function test(obj) {
  method(obj);
}

test(foo);
test(baz);

Devirtualized methods generally take the context object they're working on as the first argument. But the important part is that they no longer require an object lookup to determine function value at runtime, the function is known statically. This is exactly what this proposal accomplishes, we can take a virtual method and make it devirtualized:

const method = foo.method.devirtualize();

method(baz);

@ljharb
Copy link

ljharb commented Nov 13, 2021

That nomenclature seems even less clear than "uncurry" to me. I'd also not consider virtualization unique to functions; your "devirtualized" example there still has .name depending on the passed object.

@theScottyJam
Copy link

But the important part is that they no longer require an object lookup to determine function value at runtime, the function is known statically.

Ok. I see the relationship, but I think this is actually an orthogonal principle to what this proposal achieves.

Take this scenario:

function doSomething(obj) {
  obj.fn()
}

the fn() function is virtual, but only because it's found within an arbitrary object. The function (fn()) could take an implicit receiver object, or it could be "un-thised", etc, it doesn't matter. By virtue of the fact that it's on obj, it's virtual.

Likewise, in these code snippets:

function fn() {
  console.log(this.x)
}

fn.call({ x: 2 }) // 2
fn.devirtualize()({ x: 2 }) // 2

the function is not really virtual. But, that's simply because it's not found on an arbitrary object.

@zloirock
Copy link

@theScottyJam

I don't think unbind would work. It implies it'll do the opposite of .bind(), e.g. fn.bind(x).unbind() should bring you back to fn, which it doesn't.

The word unbind is good enough to explain what this method does, in a sense it's opposite to the .bind method. Yes — it does not bring back .bind changes, but I don't think it's a key moment.

@ljharb
Copy link

ljharb commented Nov 14, 2021

"unX" definitely must be the opposite of "X" in every sense, or it would confusing. unbind isn't viable imo.

@zloirock
Copy link

It's not possible to make something opposite to something else in all sense, it all depends solely on the point of view.

@ljharb
Copy link

ljharb commented Nov 14, 2021

I agree, which is why a negated name (“un” anything) is a poor choice.

@shuckster
Copy link

Can the word "lexical" be appropriated, or is it too much of a stretch of the definition?

obj.fn.lexicalThis();
obj.fn.lexicalize();
obj.fn.thisLuthored();

🤔

@ljharb
Copy link

ljharb commented Nov 14, 2021

“Lexical” would refer to where the function body was created; i don't see how it applies here.

@shuckster
Copy link

unshiftThis ? 🤔

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Nov 23, 2021

@Jamesernator
Copy link

Jamesernator commented Feb 1, 2022

Is this expected to be so common that a short name is preferable to a long one? Because long names could just be like:

fn.useFirstArgumentAsThis();
// OR
fn.prependThisAsArgument();
// OR
fn.useThisAsFirstArgument();

There could be some nice symmetry if there's common need for appending as well, i.e. having a fn.useLastArgumentAsThis().

@theScottyJam
Copy link

My guess is that a number of projects won't use it at all, but there will also be some projects that use it a whole ton (especially those which try and protect against prototype pollution).

That said, the way this method gets used will be on relatively simple lines of repeated code, so I don't think it would be that bad to have a slightly longer method name, even in projects where it would get used a lot.

Another potentially longer name, that I think fits the behavior well.

fn.addReceiverParam()

And here's how this might look in the wild

// With long name
const ArraySlice = Array.prototype.slice.addReceiverParam()
const StringSlice = String.prototype.slice.addReceiverParam()
const ArrayMap = Array.prototype.map.addReceiverParam()

// With short name
const ArraySlice = Array.prototype.slice.unThis()
const StringSlice = String.prototype.slice.unThis()
const ArrayMap = Array.prototype.map.unThis()

The longer name doesn't really add that much noise IMO.

@bathos
Copy link

bathos commented Jul 9, 2022

I’ve called the helper I use for this “topicalize” for a long time. No strong opinion, just tossing it out there since I didn’t see it mentioned.

@ljharb
Copy link

ljharb commented Jul 10, 2022

Both "demethodize" and "topicalize" don't make sense to me. I have no idea what "topic" means, and "methodize" isn't a thing (and "method" doesn't have a universal definition anyways).

@theScottyJam
Copy link

I do like @VitorLuizC's idea of decouple(). Perhaps we could do something like decoupleThis().

@js-choi
Copy link
Owner

js-choi commented Jul 10, 2022

Both "demethodize" and "topicalize" don't make sense to me. I have no idea what "topic" means, and "methodize" isn't a thing (and "method" doesn't have a universal definition anyways).

To me, “topic” means the topic that the pipe operator |> would bind. We probably should avoid any confusion with the concept of “topic”.

“Demethodize” seems understandable insofar that developers might understand “method” to mean “function that uses this”, and “methodized” functions to be functions that are, well, methods.

However, it is definitely true that “method” does not have a universal definition anyways (e.g., mdn/content#11237). That is a more compelling reason to me why “demethodize” would be undesirable.

I actually am becoming a little more positive towards callBind, since a bound function really is being created, and the “action” is being bound on is the act of calling a function. It’s still a mouthful to explain to any developer, but at least it’s consistent with what it does.

All of these choices have at least some undesirable characteristics, and we’re just picking the least bad one. If we reach Stage 1, it’ll probably be worth reviewing what our goals with naming should be, in order of priority.

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jul 10, 2022

FYI, there exist community implementations of methodize as an inverse of demethodize, for example

methodize https://github.com/node4good/lodash-contrib/blob/master/docs/_.function.combinators.js.md#methodize
Signature: _.methodize(func:Function)
Takes a function and pulls the first argument out of the argument list and into this position. The returned function calls the original with its receiver (this) prepending the argument list.

Function#methodize http://api.prototypejs.org/language/Function/prototype/methodize/index.html
Signature: Function#methodize() → Function
Wraps the function inside another function that, when called, pushes this to the original function as the first argument (with any further arguments following it).

Aside: I am not suggesting methodize be part of the current proposal; I mention the above only to indicate existing usage.

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jul 10, 2022

I suspect, if this proposal were accepted with a name (as opposed to syntax) that people would want to write the inverse and name it in a clear way. If this proposal chooses demethodize the inverse is simply named methodize. If this proposal chooses callBind it might be less clear what the inverse would be named (unCallBind?) and its meaning might be less obvious.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bikeshedding Discussion about naming or similar
Projects
None yet
Development

No branches or pull requests