I think candied_orange's answer explains why there's a lot of begging the question on your concept of clarity with respect to the particular problem you've chosen, however I think this user went "x ⟶ y" problem in the wrong direction some ways (too honed in on a Y behind the very specific example you gave).
If I understand correctly, to your core issue, away from the particular piece of code you've provided above:
All aspects of code exist somewhere on the continuum of common-to-obscure. So the right approach to what I'm calling "clarity vs. confidence" is probably somewhere on a continuum as well. But developers who write and review code in the real world need to know where to place themselves and their teams on that continuum.
You're talking about using and not necessarily abusing language features, and with respect to their clarity, when you should feel the need to redundantly comment or write code that's unnecessary in a given context, i.e. overexplain what you do in fear that the user won't understand what is going on.
This is complicated, but I've crafted roughly three criteria (of which none is objective) which can aid in helping you understand when to put in-code helpers for certain usage of features.
- Discoverability - how easy is it for users to figure out the language feature you're using.
- How complicated what you're doing is.
- How confusing it can be.
Discoverability
I use C++ a lot, and C++ has a lot of features people who regularly use C++ are often simply not familiar with. For example, C++ has lambdas/anonymous functions, in the syntax
[reference or value, captures...](parameters,...){
definition.
};
I've had co-workers who simply did not know about the existence of this feature. However simply asking me "what is this thing" will result in me saying "That's a C++ lambda". I may or may not need to go into more specifics on what that is, but they now have enough information, by the mere fact of me giving a name to this concept, for them to figure the rest out on their own. This knowledge is also self proliferating as in just because I wrote code that used this feature, doesn't mean I'm the sole proprietor of information on how to use it. If, lets say, co-worker Alice, who just learned this feature, is asked by Bob, what this is, Alice can confidently explain what this is to Bob. I wasn't needed.
This applies to a lot of things in JavaScript, such as 'Promises' or the DOM, closures even. If it doesn't require you to do anything other than point them to the feature and you aren't using in non-obvious ways, than it is at least discoverable.
Complexity
Complex problems sometimes require complex solutions. Even if those solutions use fairly simple constructs built into a language, their usage, even in seemingly obvious in their application, may still need to be explained.
C++ has the concept of "const", where, given a simple variable, if declared const Type foo
the type is not allowed to have non const operations applied to it, simplified, you cannot mutate a simple const variable.
Except there are several escape hatches to this rule. One escape hatch I've actually used, is mutable
. What mutable
does is effectively say "this member variable, in this object? Yeah you can just use that in a const context". Now, why would I do this? Some times, objects have the semantics of things that are not modified, but the underlying implementation actually does modify said object. The biggest reason I use this are for network socket API objects, such as ZMQ. Even sending data technically modifies the socket. Yet semantically, I'm not really modifying anything the user cares about. This prevents users from doing things they ought to be able to. Similarly, multithreaded primitives, such as mutexes, and atomic variables used for signalling, often have similar "yeah, technically this is mutating the object, but what I'm doing is not semantically a mutation that the user cares about".
But I can't just slap a few mutable
s on some network sockets and std::mutex
and call it a day. Technically they could just look it up. But no one would understand why I'm doing this. Alice, in the above example, wouldn't be able to explain to Bob what purpose mutable is serving just by me telling her what it is. I'd have to come in and explain it. This would confuse even experienced programmers.
So instead of letting that scenario play out, I comment on mutable variables justification for why they are mutable (and often what mutable does). What I'm doing is complicated, it's not immediately obvious what is happening just by virtue of using this feature and understanding it.
Confusion
A feature may exist in the language, may be discoverable, and may be easy to use. But that feature (or the allowance of something being written) may still lead to confusion.
By confusion, I mean a lot of things, but particularly I mean when you write one thing, and even given simplicity, and discoverability, it still could be interpreted to mean something else. This is particularly pertinent when you are relying on the language to do something implicitly for you with out clearly expressing intent.
Here's an example of what I'm talking about.
I've already introduced you to the idea of const objects. In C++ particular, we like to make objects const whenever we can. We can also make references to objects const (aka semantic immutability), so that when I pass a non-const object to a function, I don't have to copy its data, and I still can enforce the invariants of whether or not that object is changed.
But even with out the escape hatches, you can still accidentally have mutability exposed in const objects. If you have a pointer to an object, in a C++ object, say:
struct Foo{
int* my_int_ptr;
//despite mutating bar, I can still apply const to this function.
void baz() const{
//de referncing the pointer, and adding to it.
(*my_int_ptr) += 1; //mutates the object.
}
void mut_qux(); //won't work when Foo is const.
void const_qux() const; //will work when Foo is const.
}
you can actually have const
member functions/methods that mutate the underlying object in the pointer. The reason C++ allows this, is that the object only stores the address of my_int_ptr
, not the object, so technically, you are not editing the top level object at all, only the value with in the address. But most people, including myself, expect the semantics of const to cover this use case, to prevent such types of mutability.
Instead of calling this method const, when it technically is in C++, I instead don't use const. I could, and it would be right, and it wouldn't even be difficult to understand why I could do that. Every one should understand const doesn't apply to modifying values within a pointer right? But it's confusing. We we say const, we would like there to be no interior mutability either.
Confusion Part 2: Sometimes the Language is Wrong.
One concept I want to introduce here is that languages can also be wrong, and that can lead to confusion. What I mean is there can be features that were ill thought out, shouldn't be there in the first place, and not just non applicable like the above. As a JavaScript programmer, you should be more intimately familiar with these kinds of failures than I, but here's an example of failures in C++.
In C++, we can write brace-less versions of many types of statements, like if
, for
, etc... The immediately following statement becomes the block. However this is... confusing at best. Many code bases explicitly outlaw this kind of thing. It's not always obvious what a statement even is, and it's easy to accidentally edit it such that it is no longer a single statement.
In C++, we also have "simple" type automatic integer/float promotion rules. However... they are implicit, and they interfere with the type system in some very dangerous ways. Relying on this system is also confusing, because even if you understand that most things promote to int
when they are smaller than that, and floats are by default doubles. It's almost never clear what type will end up what, even when you use the same types, let alone different ones, compared to what makes sense. These rules are discoverable, often simple enough, yet lead to a lot of confusion.
Your example.
If we go ahead and come back around to your actual example, and apply these principles, there's a problem with every point I bring up, especially on confusion and complexity.
First, the rules surrounding negation in JavaScript on variables are not easy to find, and ironically are harder to find than !!
because of how easy it is to discern for Google "What does !! mean in JavaScript", versus the former (my search results did not include anything useful in the first page until I changed my query to include "bang operator" instead of !, but that could just be my personalized results). But this isn't nearly as big of a deal as the next issue.
Second, it's really confusing. Most people, even in JavaScript, expect !
to correspond to boolean logic (even MDN shows describes it as "logical not" and shows a boolean example immediately). This isn't about knowledge, its' about communication. So when you're using it for multiple things, even if you can, people don't understand what's going on. If you were to explain to Alice what !
means in JavaScript, before throwing up, she would not be able to explain to Bob why that line of code's intent was. This is really similar to my integer promotion thing. Like !
, integer promotions rules are ubiquitous and not new. But they still lead to all sorts of confusion and problems for people down the line when you rely on the feature instead of explicitly casting types.
The linked answer goes over what you can do. Comment, etc... Additionally I would argue that there's some other things at play here. First I would argue that JavaScript using !
like it does is not necessarily the best language decision. I'm not sure we should be relying on JavaScript's mistakes in the first place. JavaScript has an ecosystem of very basic libraries that exist for the sole reason to correct for these mistakes. Regardless, let's say you're using this in order to figure out if a parameter is really missing or not, this is still functionality often taken from a library.
Instead of using this behavior directly, I would either use a library, or push this behind another function (such as parameterOptional(your_var, false)
or parameterExists(your_var)
or parameterMissing(your_var)
) and then comment the code appropriately in that function such that no one really has to deal with it.
Third, What you are doing may be complex. If you're actually testing for existence of a value you may not be trying to test for !x
, but literally everything except if x
is true. If you're relying on the fact that say, !x
evaluates true/false as well as multiple types of null-ability, then that should be explained. There are two complex semantic ideas at play despite the code being looked at being incredibly tiny. If you tried to explain this in a comment, what would it look like? If it turned out really big, that might give you a clue on the issues with this piece of code.
Some times code can be truly miniscule, but have a lot of implications, and rely on a lot of behavior unsaid.
!myVar
in the future then would it cause issues for your code? Likewise for your code being executed by an older engine.if (A || B)
seems to communicate to me, the reader, that if!A
thenB
is still possible (otherwise, why bother mentioningB
at all?). Writing!A || A === null
violates this maxim and leaves me confused, double-guessing my knowledge of JS semanticsif (!something)
being unclear.myVar == null
, which covers bothnull
andundefined
, and is much clearer and safer, in my opinion, than!myVar
which can quickly trip you up with also filtering out0
and''
, which might be valid values. Personally, I like to enable the strict-boolean-expressions rule in my eslint-config for this reason.