Tag: Python

Function Composition with Error Handling

I’ve been reading about the functional style of programming and wanted to give it a thought on my favorite language, Python. The exercise is pretty simple, we just want to calculate ( or compute ) a result with a bunch of functions. The problem is that each of these functions might throw an error for certain inputs, making the task of straight composition difficult. Here’s an imperative attempt:

def occupation(name):
    if name == 'Kim Tae-Yeon':
        return 'K Pop Singer'
    raise ValueError('Cannot find occupation of %s' % name)
    

def salary(occupation):
    if occupation == 'K Pop Singer':
        return 25000
    raise ValueError('Cannot find salary of %s' % occupation)

def tax(salary):
    if type(salary) is not int:
        raise ValueError('Cannot calculate tax on non number')
    if salary < 12000:
        return 100
    return 200

def process(name):
    try:
        occ = occupation(name)
        sal = salary(occ)
        print(tax(sal))
    except ValueError as e:
        print (e)

process('Kim Tae-Yeon')
process('GopiKrishnan Ganesan')

That ugly intermediate variables! Since the functions throw errors, they can’t be composed directly. So, let’s bring in some concepts from the functional space to solve this problem. Instead of passing in just the input to the function, we can pass in an object that may contain an input value or an error value, but not both. Yes, I’m talking about theĀ EitherĀ ADT monad from Haskell. For simplicity, let’s use python’s tuple for our box: the first element refers to the value, second element is the error. We’ll have functions to pack and unpack the value into / from the box. The rest of logic is placed in a decorator. Here’s what I’m saying:

def combinable(f):
    def wrap(pack):
        try:
            if type(pack) is not tuple or len(pack) != 2: return (None, 'Input error')
            if pack[1]: return pack
            return (f(pack[0]), None)
        except ValueError as e:
            return (None, e)
    return wrap

@combinable
def occupation(name):
    if name == 'Kim Tae-Yeon':
        return 'K Pop Singer'
    raise ValueError('Cannot find occupation of %s' % name)
    

@combinable
def salary(occupation):
    if occupation == 'K Pop Singer':
        return 25000
    raise ValueError('Cannot find salary of %s' % occupation)

@combinable
def tax(salary):
    if type(salary) is not int:
        raise ValueError('Cannot calculate tax on non number')
    if salary < 12000:
        return 100
    return 200

def pack(x):
    return (x, None)

def unpack(pack):
    if pack[0]: return pack[0]
    return pack[1]

print(unpack(tax(salary(occupation(pack('Kim Tae-Yeon'))))))
print(unpack(tax(salary(occupation(pack('GopiKrishnan Ganesan'))))))

from functools import reduce

print(reduce(lambda x,y: y(x),['Kim Tae-Yeon', pack, occupation, salary, tax, unpack]))
print(reduce(lambda x,y: y(x),['GopiKrishnan Ganesan', pack, occupation, salary, tax, unpack]))

Problemo solved !

Advertisements