Special Practice Challenges
Coding Without If-statements
When I teach beginners to program and present them with code challenges, one of my favorite follow-up challenges is: Now solve the same problem without using if-statements (or ternary operators, or switch statements).
You might ask why would that be helpful?
Well, I think this challenge forces your brain to think differently and in some cases the new solution might be better.
There is nothing wrong with using if-statements, but avoiding them can sometimes make the code a bit more readable to humans. This is definitely not a general rule as sometimes avoiding if-statements will make the code a lot less readable. You be the judge.
Avoiding if-statements is not just about readability, there is some science behind the concept. As the challenges below will show, not using if- statements gets you closer to the code-as-data concept, which opens the door for unique capabilities like modifying the code as it is being executed!
In all cases, it is always fun to try and solve a coding challenge without the use of any conditionals.
Here are some example challenges with their if-based solutions and if-less solutions.
Challenge #1: Odd Integers in an Array
Let’s say we have an array of integers and we want to count how many of these integers are odd.
Here is an example to test with:
const arrayOfIntegers = [1, 4, 5, 9, 0, -1, 5];
Here is a solution using an if-statement:
let counter = 0; arrayOfIntegers.forEach((integer) => { const remainder = Math.abs(integer % 2); if (remainder === 1) { counter++; } }); console.log(counter);
Here is a solution without if-statements:
let counter = 0; arrayOfIntegers.forEach((integer) => { const remainder = Math.abs(integer % 2); counter += remainder; }); console.log(counter);
the examples above use forEach and mutate the counter variable. It is cleaner and safer to use immutable methods instead. However, this article is not about that topic. Also note that the if-less solution would only work for integers while the if-based solution has the advantage of handling any kind of numbers, including decimals.
|
In the if-less solution, we took advantage of the fact that the result of the modulus 2
operation is always either 1
(for odd) or 0
(for even). This result is data. We just used that data directly.
What about counting even integers? Can you think of a way to do that without using an if-statement?
Challenge #2: Weekend or Weekday
Write a function that takes a date object argument (like new Date()
) and returns the string “weekend” or “weekday”.
Here is a solution using an if-statement:
const weekendOrWeekday = (inputDate) => { const day = inputDate.getDay(); if (day === 0 || day === 6) { return 'weekend'; } return 'weekday'; // Or, for ternary fans: // return (day===0 || day===6) ? 'weekend' : 'weekday'; }; console.log(weekendOrWeekday(new Date()));
Here is a solution without if-statements:
const weekendOrWeekday = (inputDate) => { const day = inputDate.getDay(); return weekendOrWeekday.labels[day] || weekendOrWeekday.labels['default']; }; weekendOrWeekday.labels = { 0: 'weekend', 6: 'weekend', default: 'weekday' }; console.log(weekendOrWeekday(new Date()));
Did you notice that the if-statement condition has some data in it? It tells us which days are weekends. What we did to avoid the if-statement is extract
that data into an object and use that object directly. That also enables us to store the data on higher, generic levels.
The || trick above technically creates a conditional branch. I have opted to use it so that I do not repeat “weekday” five times. We can easily get rid of it if we put all the 7 days in the labels object.
Challenge #3: The doubler Function
Write the doubler
function which, based on the type of its input, would do the following:
-
When the input is a number, it doubles it (i.e.
5 => 10, -10 => - 20
). -
When the input is a string, it repeats every letter (i.e.
'hello' => 'hheelloo'
). -
When the input is a function, it calls it twice.
-
When the input is an array, it calls itself on all elements of that array.
-
When the input is an object, it calls itself on all the values of that object.
Here is a solution using a switch-statement:
const doubler = input => { switch (typeof input) { case 'number': return input + input; case 'string': return input .split('') .map(letter => letter + letter) .join(''); case 'object': Object.keys(input).map(key => (input[key] = doubler(input[key])) ); return input; case 'function': input(); input(); return; } }; // Tests console.log(doubler(-10)); console.log(doubler('hey')); console.log(doubler([5, 'hello'])); console.log(doubler({ a: 5, b: 'hello' })); console.log( doubler(function() { console.log('call-me'); }) );
Here is a solution without a switch-statement:
const doubler = input => doubler.operationsByType[typeof input](input); doubler.operationsByType = { number: input => input + input, string: input => input .split('') .map(letter => letter + letter) .join(''), function: input => { input(); input(); }, object: input => { Object.keys(input).map(key => { input[key] = doubler(input[key])); }); return input; }, }; // Tests console.log(doubler(-10)); console.log(doubler('hey')); console.log(doubler([5, 'hello'])); console.log(doubler({ a: 5, b: 'hello' })); console.log( doubler(function() { console.log('call-me'); }) );
Again, notice how the data (which operations should be done for which input type) was extracted out of the switch statement into an object. The object was then used to pick the right operation and invoke it with the original input.
Coding Without Loops
How about we explore some more challenges, but this time without using any loops?
Declarative Methods
By loops, I mean imperative loops like for, for…in
, for…of
, while,
and do...while
. All of these are similar in the way that they provide an imperative way to perform iterations. The alternative is to perform iterations in a declarative way using methods like map, filter,
and reduce
.
Immutability
Avoiding loops is not just about being declarative. It also allows us to treat our data immutably.
Data immutability is a large topic, but the big picture is to not modify data in variables and instance properties in order to represent application state. Instead, the state is stored in phases between function calls. The functions call each other sequentially to evolve the original starting point into other forms of data. No variables are mutated in the process.
Instead of abusing state to perform simple operations, staying immutable is a lot safer and cleaner. One of the biggest benefits of immutability is how it makes the code easier to maintain and extend. For example, when we manage any application state in an immutable way, we can inspect the state at any moment, undo a state change, or even travel back in time to previous states and inspect the application with them. Since the state is not changed, we can always make the application remember the evolving phases of it.
Code readability and performance might take a hit when we write code in an immutable way. However, there are many strategies we can use to get the best of both worlds.
Recursion
Another way we can avoid using imperative loops is through recursion.
Recursion is simple. Have a function call itself (which creates a loop) and design an exit condition out of that loop. I am not sure recursion can be classified as declarative, but it is certainly an alternative to using vanilla imperative loops. However, be careful with recursion as it might not perform as well as normal loops. I also do not think recursion offers good readability in most cases.
Sometimes recursion is naturally the easiest way to solve a challenge. Without recursion we would need to maintain and use our own Stack structure (but that is not too hard either).
In all cases, it is always fun to try and solve a coding challenge without the use of any imperative loops.
Here are some example challenges with their loop-based solutions and loop- less solutions.
Challenge #1: Sum a List of Numbers
Let’s say we have an array of numbers and we need to compute the sum of these numbers.
Here is an example to test with:
const arrayOfNumbers = [17, -4, 3.2, -1.3, 0, Math.PI];
Here is a solution using a loop:
let sum = 0; arrayOfNumbers.forEach((number) => { sum += number; }); console.log(sum);
Note how we had to mutate the sum
variable to accomplish the solution.
Here is a solution using the excellent reduce function:
const sum = arrayOfNumbers.reduce((acc, number) => acc + number); console.log(sum);
Nothing was mutated with the reduce-based solution. Instead, we made a bunch of calls to the callback function and the state was carried along between these calls until we arrived at our final sum state.
Here is the same challenge above solved with recursion:
const sum = ([number, ...rest]) => { if (rest.length === 0) { return number; } return number + sum(rest); }; console.log(sum(arrayOfNumbers))
The sum
function calls itself and on every turn it uses the rest operator to reduce the array it is summing. It stops when that reduced array is empty. While you might think this is a clever solution, I do not think it is as readable as using a simple reduce
call.
Challenge #2: Make a Sentence from Mixed Content
Let’s say we have an array consisting of strings and other types and we need to join all the strings and ignore the other types.
Here is an example to test with:
const dataArray = [0, 'H', {}, 'e', Math.PI, 'l', 'l', 2/9, 'o!'];
The desired output is “Hello!
”.
You can simply use the typeof
operator to check if a value is of type string.
Here is a solution using a simple loop:
let string = '', i = 0; while (dataArray[i] !== undefined) { if (typeof dataArray[i] === 'string') { string += dataArray[i]; } i += 1; } console.log(string);
Here is a solution using the filter
method combined with the join method:
const string = dataArray .filter(e => typeof e === 'string') .join(''); console.log(string);
Note how using the filter
method also allowed us to get rid of an if- statement! The conditional processing is now abstracted away inside the filter call.
Challenge #3: Convert Values to Records
Let’s say we have an array of book titles and we need to convert every title into an object and give that object a unique ID.
Here is an example to test with:
const booksArray = [ 'Clean Code', 'Code Complete', 'Introduction to Algorithms', ]; // Desired output newArray = [ { id: 1, title: 'Clean Code' }, { id: 2, title: 'Code Complete' }, { id: 3, title: 'Introduction to Algorithms' }, ];
Here is a solution using a simple loop:
const newArray = []; let counter = 1; for (let title of booksArray) { newArray.push({ id: counter, title, }); counter += 1; } console.log(newArray);
Here is a solution using a simple map
call:
const newArray = booksArray.maptitle, index) => ({ id: index + 1, title }; console.log(newArray);
Which programming paradigm are you liking the most so far?