Many ways of conditionals
A language would not be Turing complete without conditional execution. Conditional execution of any type is necessary so that a language performs looping and control flow to vary based on a state held.
Conditionals are called branching at hardware level. Control flow shapes which branch a CPU will follow. CPUs therefore utilize branch prediction techniques so they can run faster. Wrong branch prediction means wasted work, pipeline stalls, more execution cycles and hence more energy burn.
Compiled languages can leverage optimizations. In this context, a compiler can replace branching operation to a branchless one, although this is not always possible. Compilers can also reorder conditional statements, remove dead branches and can become quite aggressive on optimization efforts. Signaling a compiler to follow a certain branch is also possible. All these matter because the output machine code translated from the code written by a developer will be smaller and any downstream inefficiencies will be eliminated.
In data-intensive pipelines, branching is considered blocking. CPUs and particularly GPUs and NPUs utilize vectorization and any branching attempt hinders them using advantages of this concept. This forces scalar execution and disables SIMD.
One way of conditional implementation is if statements. Conditionals are also part of loops, switch clauses, pattern matching and more. Here, I will use conditionals and ifs interchangeably.
I list different conditional methods here. Language is primarily Python, unless otherwise specified.
Table of contents
- The list
- Classical
- Ternary
- Builtins instead of boolean chains
- Conditional mapping
- Pattern matching
- Bitwise logic
- Functional conditional execution
- Boolean indexing
- Logical table
- Monkey-patching
- Decorators as condition injectors
- Dynamic function references for type branching
- Arithmetic if
- Lookup arrays
- Memoized predicate tables
- Metaclass-controlled behavior
- AST-level rewrites
The list
Classical
if user.is_active:
status = 'active'
else:
status = 'inactive'
Ternary
C, C++, Java, Javascript, Typescript:
status = is_active ? "active" : "inactive"
Python:
status = 'active' if user.is_active else 'inactive'
Rust:
let status = if is_active { "active" } else { "inactive" };
Builtins instead of boolean chains
Python has all([...]) and any([...]) functions.
if all([is_active, is_authenticated, has_permission]):
proceed()
Conditional mapping
Dictionaries/maps can be used for conditional mapping:
# Instead of this:
if action == 'create':
handle_create()
elif action == 'update':
handle_update()
elif action == 'delete':
handle_delete()
# Do this:
actions = {
'create': handle_create,
'update': handle_update,
'delete': handle_delete
}
actions.get(action, handle_default)()
Pattern matching
Available in Python 3.10+:
match status_code:
case 200:
handle_ok()
case 404:
handle_not_found()
case _:
handle_default()
Bitwise logic
Unix-style file permissions are a notable example. It has three bits for read, write and execute operations.
READ, WRITE, EXEC = 1, 2, 4
perm = READ | EXEC # 101
# Check flags efficiently
if perm & READ:
print('Can read')
if perm & WRITE:
print('Can write')
Functional conditional execution
nums = [1, 2, 3, 4, 5]
filtered = list(filter(lambda n: not n % 2, nums))
Boolean indexing
Used heavily in data science. We also call this mask-trick.
import numpy as np
x = np.array([1, 2, 3, 4, 5])
mask = x > 2
print(x[mask])
# [3 4 5]
Logical table
For complex condition trees, nested if can quickly become spagetti. Do use logical table:
# No:
# if a and b: do_x()
# elif a and not b: do_y()
# elif not a and b: do_z()
dispatch = {
(True, True): do_x,
(True, False): do_y,
(False, True): do_z,
(False, False): do_default,
}
dispatch[(a, b)]()
Monkey-patching
Complex for library developers, comfy for library users.
def feature_flag(flag):
def decorator(func):
def wrapper(*args, **kwargs):
if globals().get(flag):
return func(*args, **kwargs)
return wrapper
return decorator
beta_mode = True
@feature_flag('beta_mode')
def new_feature():
print('Beta feature active')
new_feature()
Decorators as condition injectors
def only_if(predicate):
def decorator(fn):
def wrapper(*a, **kw):
if predicate(): return fn(*a, **kw)
return wrapper
return decorator
@only_if(lambda: debug)
def log(msg): print(msg)
Dynamic function references for type branching
handlers = {
int: lambda x: x + 1,
str: lambda x: x.upper(),
}
value = 42
result = handlers.get(type(value), lambda x: None)(value)
Arithmetic if
One of the ways of removing conditionals, used in linear optimization too.
res = cond * a + (not cond) * b
Example:
# Classic branching:
if x > 0:
y = a
else:
y = b
# Arithmetic, branchless:
y = (x > 0) * a + (x <= 0) * b
Lookup arrays
Like conditional mapping, but for special boolean case:
funcs = [handle_false, handle_true]
funcs[condition]()
Memoized predicate tables
'Cache' conditional results:
cache = {}
def expensive_check(x):
if x not in cache:
cache[x] = some_heavy_condition(x)
return cache[x]
if expensive_check(data):
do_stuff()
Metaclass-controlled behavior
Complex, but removes if debug: lines:
class Mode(type):
def __new__(cls, name, bases, dct):
if dct.get('debug'):
dct['log'] = lambda self, m: print(f'[debug] {m}')
else:
dct['log'] = lambda self, m: None
return super().__new__(cls, name, bases, dct)
class App(metaclass=Mode):
debug = True
AST-level rewrites
Requires understanding on Abstract Syntax Trees. The use of exec is limited on many sandboxed environments though.
import ast, inspect
def strip_debug(func):
src = inspect.getsource(func)
tree = ast.parse(src)
# Walk and nuke any if with `debug`
new_tree = ast.fix_missing_locations(tree)
code = compile(new_tree, filename='<ast>', mode='exec')
exec(code, globals())