Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 5, 2023 03:21 pm GMT

Confessions from a Golfer Programmer

I love code. Not all code, just beautiful code.

One of my favorite activities is to refactor - transmuting a program that works into another that does exactly the same thing, but better: Making it run faster, changing it so that it's shorter or more idiomatic, whatever that means; removing some nasty duplication, etc.
It makes me happy in the inside to see commits that have more removals than additions, which is probably the opposite of what the product manager likes to see.

This is not a good quality to have, however. It's easy for me to get engrossed in the beauty of my own - or someone else's - code, and not get anything done.

"Look!" I conceitedly exclaimed. "I've reduced the size of the function to one third its size"
"Nice, have you added the feature already? we really need it" My coworker inquired
"On it..." Realizing that I spent half an hour and it's still unstarted.

I come from javascript, where every time you try to do something minimally declarative your javascript engine dies a bit on the inside, and the garbage collector wishes you very bad things.

Let's say that our backend has a function that requests IDs. These IDs can be empty (""), and we want to ignore those. It gets the users from a dictionary in memory, and then returns the ones that it has found that are validated.

function getValidatedUsers(userIDs: string[] ): Users[];

There are two ways to achieve this.

The imperative way:

function getUsers(userIDs: string[]): Users[] {    const ret = [];    for i in userIDs {        if (i === "") continue;        const user = UsersDictionary[i];        if (user !== undefined && user.isValidated()) {            ret.push(user);        }    } }

The declarative way:

function getUsers(userIDs: string[]): Users[] {    return userIDs        .filter(x => x !== "")        .map(x => UsersDictionary[x])        .filter(x => x !== undefined)        .filter(x => x.isValidated());}// Which could even be written more // concisely with arrow function syntax.

To me the declarative way more beautiful and clear, the steps look simple and ordered...

...But you would be a fool to use it.

The engine copies the array 4 times which involves a heap allocation, a memory copy and later on a garbage collection pause that is guaranteed to happen at the worst possible time, all of the time. Not to mention the closures generated too. You won't see me writting javascript that looks like that in code that needs to be even minimally performant, especially not in node, which is single threaded and your pretty little declarative function is slowing down the rest of the server.

I love Rust (I promise this is not another blog of Rust propaganda), which probably doesn't come as a surprise given that I'm your stereotypical nerd. Rust is great, but it's also my greatest demise.

My first contact with the expresiveness of Rust blew me away. You can use the declarative way, you can use beautiful declarative code, all without it slowing anything down (it even has the potential to get optimized into being faster!).

Rust is famous for its error handling. It does not support exceptions, instead opting for returning the errors to the caller. The first time I heard of it I thought that it was a step backwards, a return to C and its error code returning. But I was wrong, rust has exceptional tools for working with errors.

Here's an example from selenium-manager, maintained by the selenium project and a contribution I made

This calls three functions that can fail: set_timeout, set_proxy and resolve_driver

If any of them fails the program exits with a return code.

match selenium_manager.set_timeout(cli.timeout) {    Ok(_) => {}    Err(err) => {        selenium_manager.get_logger().error(err);        flush_and_exit(DATAERR, selenium_manager.get_logger());    }}match selenium_manager.set_proxy(cli.proxy()) {    Ok(_) => {}    Err(err) => {        selenium_manager.get_logger().error(err);        flush_and_exit(DATAERR, selenium_manager.get_logger());    }}match selenium_manager.resolve_driver() {    Ok(driver_path) => {        selenium_manager            .get_logger()            .info(driver_path.display().to_string());        flush_and_exit(0, selenium_manager.get_logger());    }    Err(err) => {        selenium_manager.get_logger().error(err);        flush_and_exit(DATAERR, selenium_manager.get_logger());    }};

Image description

This is where my golfer self comes in, I want to make this code as concise as I can.

First, let's use if let Err, which could translate to javascript's if (my_fn() instanceof Error) and extract the error to a function

let err_out = |e| {    selenium_manager.get_logger().error(e);    flush_and_exit(DATAERR, selenium_manager.get_logger());};if let Err(err) = selenium_manager.set_timeout(cli.timeout) {    err_out(err)}if let Err(err) = selenium_manager.set_proxy(cli.proxy) {    err_out(err)}match selenium_manager.resolve_driver() {    Ok(path) => {        selenium_manager            .get_logger()            .info(path)        flush_and_exit(0, selenium_manager.get_logger());    }    Err(err) => {        err_out(err)    }};

But Rust has so much more to offer. Notice that there's a happy path (continue) and a gutter, where all of the errors go.

Image description

This is a common pattern, and Rust has really nice tools for working with these in a declarative way. We can use all of the expressiveness of rust to golf this down

selenium_manager    .set_timeout(cli.timeout)    .and_then(|_| selenium_manager.set_proxy(cli.proxy))    .and_then(|_| selenium_manager.resolve_driver()    .and_then(|path| { // If all of them succeed        let log = selenium_manager.get_logger();        log.info(path);        flush_and_exit(OK, &log);    })    .unwrap_or_else(|err| { // If any of them fail        let log = selenium_manager.get_logger();        log.error(err);        flush_and_exit(DATAERR, &log);    });

This is reminiscent of the code we saw earlier in javascript, this time with errors, which in my eyes makes all of it much easier to read and understand, and most importantly: More beautiful. The only metric that matters.

All of these changes are cool and all, but they provide no value. The end user doesn't care, the compiler doesn't care either (they're equivalent), but I care. I'm in for the aesthetics, but don't tell my boss.

When I have to make functional changes they break the perfect aesthetic, but given the time to refactor it will become something even more beautiful than before, complexity that has been tamed into a coherent piece of machinery.

With no time given to making it look beautiful it will become an ugly mess, but an ugly mess that works. There's value in that.

One time when I read the code of a junior coworker of mine, I physically shivered, horrified at some of the things I saw, and I felt an urge to fix it (this is one example of such):

public isDebug(): boolean {    if (window.localStorage.getItem("debug") == "true") {        return true    }    else {        return false    }}

I'm not kidding, that's real code. I emotionally had the need to rewrite it to

public isDebug(): boolean {    return localStorage.getItem("debug") === "true";}

I doesn't make me happy that they save it to localStorage, that's part of a bigger bodge that I don't even want to look at.

But that was a part of the code that didn't affect me in any way, I just tumbled with it and couldn't look away.
It might not be pretty but their code worked, and at the end of the day that's what matters. I have so much to learn from them :)

That was it, I hope you liked my first post.
Remember when I said I wasn't going to make Rust propaganda? I kind of lied a bit there :)


Original Link: https://dev.to/devardi/confessions-from-a-golfer-programmer-3fb5

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To