A couple of days ago while doing code review I saw a snippet like this:
React.useEffect(() => {
someCondition && doSomething()
}, [someCondition, doSomething])
You don't need to know what React.useEffect
does or anything, I just want to focus on the body of the callback function.
I suggested my colleague to use an if
statement instead of the short-circuit expression. When asked why it was preferable to use an if
instead, I didn't have a reasonable answer. It just felt better to me.
But feeling better is not an acceptable reason, specially because it is highly subjective. What feels better to me won't necessarily feel better for the rest of the team.
So I just did what any other person would do: I obsessed about it (😅), trying to rationalize why it felt better.
Disclaimer: This article just describes my reasoning around this topic. Like it or not, that's completely up to you. I'm not suggesting this as a "best practice" or anything else.
One thing I learned from watching Rich Hickey's talks is to always start with a definition:
In computer science, an expression is a syntactic entity in a programming language that may be evaluated to determine its value.
Here's a bunch of expressions in JavaScript:
42 // 42
'foo' // 'foo'
false // false
const nums = [1, 2, 3] // ??
nums // [1, 2, 3]
Values in JavaScript evaluate to themselves, and variables holding values evaluate to whatever they hold. Notice the fourth line in the snippet above: in JavaScript assignments are also expressions. What do you think the expression const nums = [1, 2, 3]
evaluates to?
Well, it evaluates to undefined
.
In other programming languages (like Python) a variable assignment is not an expression, but a statement. Here's the definition for statement:
In computer programming, a statement is a syntactic unit of an imperative programming language that expresses some action to be carried out.
The important word here is action. Keep that in mind for now.
Here's a bunch of statements in JavaScript:
for (let n of nums) { /*...*/ }
while (true) { /*...*/ }
if (nums.length) { /*...*/ }
Ignoring the fact that assignment is an expression (a useless expression, if I do say so myself) it would be reasonable to think that expressions are to values as statements are to actions.
Short-circuit evaluation
More definitions, yey:
Short-circuit evaluation, minimal evaluation, or McCarthy evaluation (after John McCarthy) is the semantics of some Boolean operators in some programming languages in which the second argument is executed or evaluated only if the first argument does not suffice to determine the value of the expression.
Here's an example:
true || false // true
In the previous snippet of code, the right-hand side expression of the OR operator is not evaluated since the first argument is enough to determine the value of the whole expression.
It's kinda weird to think about it like this using literals, since literals evaluate to themselves. We'll write this differently so it's easier to reason about:
const aCondition = true
const anotherCondition = false
aCondition || anotherCondition // true
Since aCondition
is true
, there's no need for looking up the value of anotherCondition
, whatever that is.
Let's try with another example:
const person = {
get name() {
console.log('Bayum!')
return 'Bodoque'
}
}
true || person.name // true
If you run this code, you'll notice 'Bayum!'
is not logged to the console, since the left-hand side of the ||
operator is already true
, which is good!
But what's the deal with this?
Side effects, functional programming & Haskell
We'll take a brief detour and continue with, guess what, another definition:
Haskell is a general-purpose, statically typed, purely functional programming language with type inference and lazy evaluation.
Let's write a little function with Haskell that prints "42"
to the console:
doSomething = putStrLn "42"
Using ghci
, which is the Glasgow Haskell Compiler interactive environment (think of a REPL), we can check the type of our doSomething
function:
Prelude> doSomething = putStrLn "42"
Prelude> :t doSomething
doSomething :: IO ()
doSomething
is a function that takes no arguments and its return type is IO ()
, or IO
of unit (an empty set of parentheses is called unit and it's similar to void
in JavaScript). In Haskell all functions with side effects have a return type of IO
of something. Pure functions can't call effectful functions. If you want to have a side effect, the return type should always be IO
of something.
Although not mandatory, we can explicitly write type annotations:
doSomething :: IO ()
doSomething = putStrLn "42"
-- Here's another function that takes two Ints
-- and returns another Int, just for contrast
add :: Int -> Int -> Int
add a b = a + b
Alright, detour is over, enough Haskell, let's get back on track.
Short-circuit expressions and flow control
A function invocation can always be replaced by its return value if it depends only in its inputs. Another way to phrase it, is that a function invocation can only be replaced by its return value if the function has no side effects.
This property is called referential transparency. Referentially transparent functions are also known as pure functions.
When doing functional programming, our goal is to maximize the surface area of code that's written with pure functions: they are easier to test and easier to reason about. So for most of your functions in a program, you're going to be interested in their return values:
const whatIsThis = someCondition && doSomething()
If we're not interested about the result of doSomething
, then it's probably worthless to store the value of the expression into whatIsThis
, but the expression will still have a value, whether it is used or not:
function doSomething() {
console.log("42")
}
someCondition && doSomething() // `false` when `someCondition` is `false`
// `undefined` when `someCondition` is `true`
If we don't care about the value of the expression, then doSomething
is most likely an effectful function. But JavaScript is no Haskell so there's no way to tell if doSomething
is effectful or not without looking at its implementation. And even then, it wouldn't be necessarily something straightforward to figure out.
I think this is why I prefer to use an if
statement instead of a short-circuit expression for flow control in effectful functions: for me it makes it completely unambiguous that we don't care about the return value, hence it's a side effect.
But what about effectful functions that DO return something?
We don't have a compiler like GHC to enforce purity in our functions, but we can still follow a similar convention that only effectful functions can call other effectful functions. Haskell does this using monads.
Instead of writing an explanation about this topic, let me point you to this really straightforward video that makes a wonderful job:
Top comments (6)
Imho I prefer ? : instead of &&, much more cleaner way to declare what get if someCondition is undefined | false | null | 0 | ""
Yes, for sure, as long as you're interested in the result of the expression. But I've seen people do stuff like this, which is what I'm advocating againts:
Yes you right. But what are you think about react reducer switch like this? :
Looks like a regular reducer to me... how is this related to what we were talking?
reducer is looks same as ternary with two function, just case use one condition with many pure function.
Again, this is because you care about the result of the function. My take is: use statements for side effects and expressions for pure functions.