Sunday 22 January 2023

Throw as en Expression

Thanks to learning Kotlin I've got introduced to some features and techniques that I had never thought about. I guess it's because Kotlin is the most funtional programming friendly language that I've used. I've read somewhere that in Kotlin every statement is either a declaration or an expression. Thanks to this we have "if-else expressions", "switch expressions", "try expressions", "throw-retur-break-continue expressions"... The idea of a throw expression seemed odd to me, until that I saw an example:


fun fooOffensive(value: String) {
     val number = value.toIntOrNull() ?: throw IllegalArgumentException("The given argument is not a valid number string")
    // do something with number
}


It's nice, really nice. Neither JavaScript nor Python (this is obvious, as while Python has many, many strengths, it's, among modern languages, one of the less functional programming friendly ones) has this feature, but I've realised that we can easily get the same effect of throw expressions. Just by defining a throwException function we can write code using the ternary operator in JavaScript like this:


function throwException(ex){
    throw ex;
}

function processMessage(msg){
    msg = msg.startsWith("Msg-") ? msg.toUpperCase() : throwException(new Error("Wrong format"));
    console.log(`Processing message: ${msg}`);
}

let msgs = ["Msg-Connect", "aaa"]
for (let msg of msgs){
    try{
        processMessage(msg);
    }
    catch (ex){
        console.log(`Error!!! - ${ex}`);
    }
}

// Processing message: MSG-CONNECT
// Error!!! - Error: Wrong format


And code using optional chaining like this:


let europe = {
    countries: {
        France: {
            population: 65000000,
            cities: {
                Paris: {
                    population: 2000000,
                    code: 75
                }
            }
        }
    }
}

function getCityInfo(city){
    let code = europe?.countries?.France?.cities?.[city]?.code ?? throwException(new Error("Missing City"));
    console.log(`getting city info for ${code}`);
}

let cities = ["Paris", "Lyon"]
for (let city of cities){
    try{
        getCityInfo(city);
    }
    catch (ex){
        console.log(`Error!!! - ${ex}`);
    }
}


// getting city info for 75
// Error!!! - Error: Missing City


In Python we can leverage an equivalent raise_exception function when using the (sort of) "ternary operator":


def raise_exception(ex: BaseException):
    raise ex

def process_message(msg):
    msg = msg.upper() if msg.startswith("Msg-") else raise_exception(Exception("Wrong format"))
    print(f"Processing message: {msg}")


msgs = ["Msg-Connect", "aaa"]
for msg in msgs:
    try:
        process_message(msg);
    except Exception as ex:
        print(f"Error!!! - {ex}")
        
# Processing message: MSG-CONNECT
# Error!!! - Wrong format


As we know Python lacks so far a "safe navigation - optional chaining" operator (there are renewed discussions, and the usual absurd oppositions based on "it's unpythonic, it makes code hard to read (for non real programmers)..." and so... As we saw in this post, we can get similar safe navigation effect with a rather clear syntax, just by using a function. I've adapted such function to throw exceptions if requested. It receives a lambda (already trapping in its closure the variable to "navigate") and an optional second parameter. If the safe navigation fails, if the second parameter has not been provided the function returns None, if provided, if it's an instance of Exception it throws it, otherwise returns the value.



def raise_exception(ex: BaseException):
    raise ex
    
def safe_access(fn, default = None):
    """ returns None by default if something in the chain fails. 
        If the default value is provided and it's an Exception, it throws it, otherwise it returns it
    """
    try:
        return fn()
    except (TypeError, KeyError, IndexError, AttributeError):
        return raise_exception(default) if isinstance(default, BaseException) else default


class Person:
    def __init__(self, name, age, profession):
        self.name = name
        self.age = age
        self.profession = profession

class Profession:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def format_profession(self, st1, st2):
        return f"{st1}-{self.name}-{st2}"

p1 = Person("Francois", 4, Profession("programmer", 50000))

print(safe_access(lambda: p1.name))
print(safe_access(lambda: p1.city))
print(safe_access(lambda: p1.city.name, "Paris"))
try:
    print(safe_access(lambda: p1.city.name, Exception("Missing city")))
except Exception as ex:
    print(f"Error: {ex}")
print(safe_access(lambda: p1.profession.salary))
print(safe_access(lambda: p1.profession.format_profession("¡¡", "!!").upper()[:6]))
print(safe_access(lambda: p1.profession.format_profession("¡¡", "!!").upper()[:6]))

# invoking a non callable
print(safe_access(lambda: p1.profession.salary()))


No comments:

Post a Comment