Default wrappers

For functions with default parameters and properties, it'd be useful to be able to build on top of those defaults. For example, let's say there's a library function implemented like this:

const publish = ({ url = 'http://example.com/publish', body = `Published at ${new Date().toISOString()}` }) => {
  return someApi.post('/publish', { url, body }))
}

If you're not able to access the library or refactor its implementation, you can't do much with those default parameters. You can either use them or not. Quite often though, you'd want to add something to the default body. So I'm imagining a syntax like this:

const publishWithComment = comment => {
  return publish({ body: default(x => `${x}. Comment: ${comment}.`) })
}

Where default is a pseudo-function that calls the callback passed to it with whatever the property it's bound to uses as a default value. In this case, it'd be roughly equivalent to:

const publishWithComment = comment => {
  return publish({ body: (x => `${x}. Comment: ${comment}.`)(`Published at ${new Date().toISOString()}`) })
}

As far as I know, there isn't a way to do this right now, short of persuading library authors to expose the default value logic in a separate function.

A risk of introducing a way to do this is that if the original code hasn't specified that the default value is 'interceptable' then the language is taking something that was previously 'private' and making it 'public'.

It seems like it would be less risky to work with the library to include this as part of their API is it's a pattern they want to support. Rather than the language adding it on their behalf without a way for them to stop it.

3 Likes

Python introduced this feature in v3.something: inspect — Inspect live objects — Python 3.10.2 documentation

C# and similar languages, I believe can access in private methods via reflection: ParameterInfo.DefaultValue Property (System.Reflection) | Microsoft Docs

Ruby has it too: Class: Method (Ruby 2.2.0)

And it's not impossible to get this information by fn.toString(), so I'm not sure exposing this is a real risk.

And, in general, it's not possible to work with all libraries. Some are unmaintained or impractical to fork.

But maybe rather than adding syntax there could be a static function like Function.signature(...), similar to python's inspect.signature(...). Even with another to create an uninspectable function if people really want to keep their default parameters private for some reason?

Function toString is both something that authors can block (via binding methods, or making a forwarder like function () { return realFunction.apply(this, arguments); }, and also not something that can be reliably parsed and reflected over, so its existence doesn't change anything

Exposing this information in an easily automatable way is indeed a very real risk.

…and that's exactly the right thing to do. If there is logic implemented as part of the function that is valuable in its own, it should be exposed by the library as a separate function.

@ljharb Do you have an example where it’d be a risk?

@bergus I respect if you disagree with this idea. But asserting that you disagree with the premise, without making any effort to explain why, isn’t really contributing to the discussion.

Agreed toString is not reliable, but it does in a large number of cases expose implementation details that the author might otherwise have expected to be ‘private’.

Re being blockable - if following the static function approach, like Python as mentioned, I think it’d be pretty easy to add a helper to disable.


More generally - I’m curious whether those who think this would be a bad choice for JavaScript but wasn’t for Python? Or do you think it was a mistake in Python? Of course languages don’t need to have the same features, but the costs and benefits of this feature seem directly comparable in this case, so curious what the rationale is.

Yes, it would immediately mean that ever changing the names or order or defaults of arguments must automatically be treated as a breaking change. Worse, it would silently become one, despite decades of programming experience telling practitioners something different.

I don’t know enough about python to comment; my sense is that all reflection features in all languages that authors haven’t explicitly consented to (or aren’t inherently a property of the language) are always a mistake, because it forces implementation details to become part of the public api, with no palatable recourse.

Python is a different language, and follows a different philosophy. In Python, absolutely everything is public. You can't even hide variables in a closure like you can do in JavaScript, because in Python there's reflection methods to pull stuff out of a closure. This puts full power into the hands of the API users to (ab)use your library however they want. This also means every update a library makes will effectively be a breaking-change update (though, there's different levels of "breaking change", some updates will break code that was being bad and reaching into the library's internals when it shouldn't, others updates will break what's supposed to be the public API).

JavaScript follows a much more rigid philosophy, which I appreciate. When something is private in JavaScript, it's truely private, and I don't have to worry about something using reflection to get the value of my private data. I know with certainly that I can update how I store my private data, and know there's a 0% chance that I'm breaking other people's code. I can't do that in Python.

Now, in relation to the specific feature request you're proposing, you're basically asking for default parameters to become part of a function's public API. This means, if I have older code written like this:

function doThing(value) {
  var value = value_ == null ? DEFAULT_VALUE : value_
  ...
}

I can't later refactor it to look like this, without doing a major-version update.

function doThing(value = DEFAULT_VALUE) {
  ...
}

Or, as an alternative example, say we want to add a new parameter to the function, which, when present will change the default value of another parameter. Like this.

// before
function doThing({ useSpecialMode = false } = {}) {
  if (typeof useSpecialMode !== 'boolean') throw new Error('Bad param')
  // ...
}

// after
function doThing({ useSpecialMode = null, specialModeKey = null } = {}) {
  useSpecialMode ??= !!specialModeKey
  if (typeof useSpecialMode !== 'boolean') throw new Error('Bad param')
  // ...
}

This normally wouldn't be a breaking change, but if we make default parameters part of the public API, it would be.

There's also the fact that sometimes people are trying to run JavaScript in a locked-down secure enviornment (SES). The controller of this enviornment might expose an API in which others can use, but that's the only way they can communicate with the outside world. If that API had private, implementation details as a default parameter, and we suddenly made those default parameters public knowledge, then we may be providing these locked-down scripts a gateway to get out of their box. Other languages don't always have to support this ability to lock down a script the same way JavaScript needs to support it.

1 Like

Depends on your definition of a breaking change. Because of .toString() almost anything could break code like:

if (fn.toString() !== '(a,b) => a+b')
  throw Error('foo')

And changing the order or defaults of a function's arguments is in most cases a breaking change already.

Usually, reflection is considered beyond the scope of semver rules and similar. At least in other languages.

But @theScottyJam I suppose you are right about secure environments. Maybe an example is:

export function hitAPI(password = getDefaultPassword()) {
  …
}

Maybe the default password needs to stay private, even if it’s allowed to be used by the internals of that function. Personally I’d never consider that bullet proof anyway, but I suppose there might be some code like that which is currently secure, which this would make un-secure. My sense is that anything “secured” like this is very brittle already.

toString is a special case, that isn’t trivially programmatically parseable, and that the spec doesn’t mandate match the actual source. toString is never part of the api.

Your suggestion, however, would be. Reflection is in no way beyond the scope of semver rules, at least in js - every observable thing (except toString) can be part of your api. Making things newly observable is a massive risk that is almost never going to be worth it.

Oh, there's another issue with this, which is a more practical issue. This idea is assuming that default parameters are just going to be straight literals, which isn't always the case. What happens, for example, if you want to get the default parameter value of the second parameter in this snippet?

const add1 = x => x + 1

function fn(x, y = add1(x)) {
  ...
}
1 Like

@theScottyJam would it need to be any different?

fn(3, default(val => val * -1))

val would be 4 in this case, because it would be whatever the function signature evaluates with nothing was passed in. Maybe I’m misunderstanding you though.

This is a fair stance, I guess. Out of curiosity, doesn’t Proxy fall into this category? Before it, something like this (contrived example) wouldn’t leak the password value:

const f = options => {
  const password = getPassword()
  options[password] = password
  usePasswordSomehow(options[password])
  delete[password]
}

But when Proxy was introduced, it enables adding a catch all set handler which could capture the password when it’s added to options. Because of this kind of thing, my feeling was this level of non-privacy is the expectation in js, but maybe Proxy is different somehow.

@ljharb what about call stacks? If you publish yourFunction, which takes a function as a parameter, and calls it, then I write something like

yourFunction(() => {
  if (parseCallStack(Error().stack)[2].line = 13) {
    throw new Error(‘unlucky')
  }
  …
})

Couldn’t that make adding a comment or rearranging lines a breaking change? That’s the kind of thing I mean when I say reflection shouldn’t be considered part of the official API surface. Anyway, IMO the API surface and the definition of a breaking change is whatever the library author says it is. Undocumented behaviour should really be considered to work only by luck and changes to it don’t need to be considered breaking.

Ah, yes, that would work.

Sure it would; setters existed before Proxy (you’re right tho that Proxy would be needed here; and yes, this was an observability increase in practice also)

Call stacks aren’t in the language, and when they are, they’ll be in the same category as toString.