Cosmophile's Blog: Home

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

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())