Feature Idea: In place identifier for Magic Numbers

Hi! this is my first time here, really enjoying this place.

When debugging code, you may encounter a lot of magic numbers in your code base, when writing code yourself you may just want to get something working without having to declare a variable as such

const NAMED_ID = "some-value"

or perhaps worry about where to place said variable. However, you may want to leave a hint for yourself.

A magic number may be self explanatory depending on the context, they are certainly not most of the time.

I always taught, especially for readability and debugging purposes, defining an in place name for a magic number based on context would be an awesome thing to have in the language.

For example:

if (value > 3) return 'done';

would become

if (value > IN_PLACE_NAME_3) return 'done';

The in place name would be a valid identifier concatenated to the magic value. That could be done the following ways:

  • IDENTIFIER_value (note the underscore)
  • IDENTIFIER=value (in place assignment, I don't necessary like it)
  • IDENTIFIER_(value) (too close to a function, maybe)

Additionally, when debugging a function, class, module and such, you may find magic numbers that are the same value, but may not know what they all mean individually. They may be indicating the same thing, they may not. An in place name allows for "quick per context naming" that does not involve having to declare constants that have the same value or one constant with a name that may not inform much in different contexts.

This is roughly my idea. Would appreciate any help to formulate it better.
Feel free to break it apart and provide feedback.

This reminded me of Numeric Literal Suffixes, though with the required setup, you'd likely be better off just creating a const.

2 Likes

how is this better than "a comment" or "an actual variable"?

2 Likes

Actually, this idea does intrigue me a bit. Let me generalize it a bit.

A \\ character can be used to provide a token-level comment. The comment can only contain alpha-numeric characters (and underscores), and can be placed between any tokens. This sort of comment would help bring the comment even closer to the thing it's commenting, and would help prevent issues with comments becoming outdated, because it's pretty hard to forget a comment that's right in the middle of the code.

const area = 3.14\\PI * r**2

onClick(\\createUser () => {

})

addEnemy(\\position { x: 2, y: 3 })

return\\user getResultFrom(someOperation())

(yes, I realize block comments can do the same thing. But, block comments are just a bit more clunky)

It's an interesting and new idea. I don't think it's actually worth introducing into the language (the syntax cost just isn't worth it, and these token-level comments do make the code feel a bit... bumpy), but it's interesting. After all, all of these could be trivially fixed with variables (and if wanted, these can just be local variables instead of variables at the top of a file)

const PI = 3.14
const area = PI * r**2

// A normal function can let us provide a name here.
onClick(function createUser() {

})

const position = { x: 2, y: 3 }
addEnemy(position)

const user = getResultFrom(someOperation())
return user

The do-block proposal will also help, in that it'll let us limit the scope of these variables, so we don't have to pollute our scope with extra variables.

const area = do {
  const PI = 3.14
  PI * r**2
}
1 Like

Great question. I suppose if you are working on a personal project or you have the flexibility of editing a code base as you wish; just setting a "const" or a comment may be enough for debugging purposes. You may also have time for all necessary improvements to your code, so magic numbers are not a thing in the project.

The in place identifier will always be just a quick way to keep up with all magic number in a code base when you are trying to decipher context without commenting what you think is happening here vs there.

In a team keeping such comments to a minimum (in my opinion) is a must; also we have massive deficiencies when it comes to comments as it is, "an in place name" would bring good practices (specific to magic numbers) to the fore front of your code.

Additionally, something I did not mention in the post (in which I will edit and improve at a later time) is that, an in place name will serve to inform as much as a comment would and ultimately be syntatic sugar for a "const" declaration.

For example: a line like this, that you may come across anywhere on your code

total = itemsInCart * 2

would than be written as, an in place edit with some context

total = itemsInCart * CART_MULTIPLIER_2

and resolve to

const CART_MULTIPLIER = 2;
total = itemsInCart * CART_MULTIPLIER;

Thank you for pointing this repo to me. Very intriguing indeed.

I appreciate the feedback. I will definitely take a look at the do-block proposal.
For all the points you made, I do agree that the token-level comments as shown here are not ideal.

What I am thinking is something more straight forward. That would "kill" magic numbers forever (lol)

Allowing developers to add an identifier in place, we are eliminating two steps

  • a comment
  • a const declaration

An in place name would transpile to a const declaration, and provide enough context where a comment would not fit. Meaning even someone debugging the code, would not need to stress too much about naming and declaring the value, the new syntax would simply take care of things.

For example: a line like this, that you may come across anywhere on your code

total = itemsInCart * 2

would than be written as, an in place edit with some context

total = itemsInCart * CART_MULTIPLIER_2

and resolve to

const CART_MULTIPLIER = 2;
total = itemsInCart * CART_MULTIPLIER;

Here you have your context and your const. Eventually keeping track of all your magic numbers.
Just like how:

this

1_000_000_000

transpiles to this

1000000000

Why do you care that it gets transpiled into a literal variable? Are you thinking that this transpiled constant could be reused in other locations in the code? Like this? (I hope not, because this is a little gross)

const total = itemsInCart * CART_MULTIPLIER_2
console.log(CART_MULTIPLIER) // logs 2

If this shouldn't be allowed, then wouldn't this identifier+number idea be exactly the same as the token-level comment idea? What's the difference between writing CART_MULTIPLIER_2 and 2\\CART_MULTIPLIER? (Also note that we can't actually use the CART_MULTIPLIER_2 syntax for your idea, as this already means something in JavaScript, it's simply a variable name, so we're going to need to add some sort of special character to distinguish your number+identifier from normal numbers).

Edit: Really, if you don't like the idea of being able to place these comments anywhere, that's fine by me. But, conceptually, I think we should be thinking of these things as numbers plus comments, because that's all they really are. It's a number, plus a small explanation about what that number is doing. That explanation provides no runtime behavior, and changing what the explanation is won't effect runtime at all.

I agree, I think we are on the same page. My point with the idea was to see if we could tackle two things at once. I actually have nothing against the idea of token-level comments, in this context they seem to do exactly what I was proposing.

Thinking of them as number + comments as you mentioned works. It keeps things tight and simple, and do not need the extra leg work with variables. And in terms of DX a design guide would probably kill the need for such notation.

Sure. Now, I want to switch this to somewhat of a survey.

  • In your work, have magic numbers been a problem? (in any context)
  • How do you handle magic numbers? do you, or are they passable?
  • Is a design guide enough to eliminate (strong linting rules and more) magic numbers/things in the code?
  1. They haven't really been a problem for me. I usually only encounter one or two magic numbers, max, per module, and many don't have any, so they're not that hard to deal with. Other projects may be different though.
  2. By putting them in a constant at the top of the module. Or, sometimes at the top of the function if they only relate to the logic within that function. The same thing applies for any magic value, like magic strings as well.
  3. We don't have any official design guide that talks about magic numbers, but we all seem to be on the same page on how to handle them. And, I would very much be against a linter rule for this sort of thing.

The problem with linter rules, is that they'll just flag way too many numbers as "that's magic, put it into a constant to make it easier to understand", when really, those numbers can be understood just fine. Some examples that it would flag, that really don't need go into constants:

const secondsToMinutes = sec => sec/60
const radiansToDegrees = rad => rad * 180/Math.PI

initSessionTimeout({
  minsOfAllowedInactivity: 5,
})

And these are examples that really arguably don't need to be put into a constant, unless you realize that more than one place needs these values. You could put them in a constant if you think that's likely to happen, but there's often no real reason to do so until it's needed. Perhaps there's other ways to improve this code (like, adding a comment explaining what's going on, refactoring out a chunk, etc), so it's also important to keep in mind that simply putting the number into a constant isn't always the best solution, it's just one possible solution to clean up code.

myElement.style.marginLeft = '5px'

function countdownInSeconds({ startingAt, onTick }) {
  let secLeft = startingAt
  const intervalId = setInterval(() => {
    secLeft--
    onTick(secLeft)
    if (secLeft <= 0) clearInterval(intervalId)
  }, 1000)
})

// If everyone on your team is familiar with node's process.argv
// then this logic should feel pretty natural. If you're writting the
// code for people who might not be familiar, then a comment
// explaining what's going on would do much better then putting
// the `2` in a constant.
if (process.argv.length === 2) {
  console.error('Please provide a required parameter')
} else {
  const requiredArg = process.argv[2]
  ...
}

// You could create a constant for the length of the id, but if the length really
// isn't important to your code, then that's just unecessary noise.
const randomId = Math.random().toString().slice(2, 7)

Linter rules should be about making your code style consistent, and flagging real bugs that it finds (or the use of unnecessary footguns). This sort of linter rule falls out of both of those categories, and instead is trying to do a really bad job at helping you decide what code is unreadable and needs fixing up. In some cases, this will just lead you to placing a number of // eslint-disable-line comments around to let the linter know "thank you for that helpful hint, but I don't think it's going to help here". In other cases, you'll just extract a constant to make the linter shut up, even though it's really not needed, so all you're doing is busy work.

I know some people feel like the most important aspect of magic numbers are about making sure you're not repeating that number (i.e. keeping your code DRY), and yes, it can be important to DRY up your numbers, but again, that feels like something that's better left for the programmer to decide when to DRY their code and when not to (like they have to do whenever they decide it's appropriate to DRY up any other logic).

Anyways, there's my rant on magic numbers. If you find that you're having to extract a ton of constants in your own code (perhaps because of a linter, or team rules), maybe take a second look at those constants you're extracting and ask yourself if that really needed to be done. Because, it's very possible that a lot of those "magic numbers" would have worked just fine if you simply left them as-is in your code, without turning them into a constant. Perhaps, this is the reason why you're finding magic numbers cumbersome to work with, because you're dealing with an overly strict policy on turning them into constants, and are needing to turn far too many numbers into constants.

1 Like

Are you familiar with the no-magic-numbers eslint rule?

Thank you for the feedback. I agree we should have a notion of how our code flows and stick with that, even as a team. I would add that a design guide would come in handy for new comers, so they adapt to best practices quicker.

Sidenote, as a developer I usually champion setting up proper documentation and best design practices on any code base to help improve the quality of contributions to the same. I find myself caring about having set rules even for the minutiae in the efforts to minimize startup time on a ticket or PR.

Usually the joke is that developers don't care about that sort of things, but if you are dealing with refactoring, and debugging and constant addition of new features, a guide usually comes in handy.

I was not. Just read about it now, and I will toy more with it, to see how I would rate it in terms of dev experience.

This answer has an interesting point.

Why can't this just be done by an implementation? It could literally be done in O(n) during bytecode generation - some engines already optimize for effectively constant variables and you already need to map variables to offsets as part of code generation, so they already have the information needed to do it. This could be as simple as just evaluating constants set to primitive literals to their values and, when all operands are primitive literals, evaluating the operation immediately instead of generating it later. They could even do it for other constants like null/undefined and logical expressions like optional chaining, nullish coalescing, and logical AND and OR.

Thanks for the feedback. Let me see if I understand, and correct me if I am wrong.

You're saying, that if we had something like NAMED_2, just like null and undefined, the operation would be evaluated immediately instead of generating later, right? That would be cool actually, but what do you think of such thing/named magic number?

Yes, provided the engine detects it's a constant. Note that this wouldn't carry over to methods, only basic operators.

As a language feature, not a fan as it offers nothing on its own. As an implementation feature, I'd be all over it as it'd provide a massive perf boost.

That's exciting, would love to do a demo for something like that. Now admittedly, I don't know much about low-level js implementations. Could you point me in the right direction?

I'd suggest reaching out on an engine's mailing list or similar. (V8's the easiest as it's also often used as a research engine: Contributing to V8 ยท V8)

1 Like