Adjective Noun

Raku on Rails

2017-06-16

I saw this interesting article titled Railway Oriented Programming by Scott Wlaschin. Initially I just clicked through the slides and the gist, as I understand it, is to define "chainable" functions that also encapsulate error handling. I later watched the video of the talk and highly recommend it. It's interesting and engaging, and Scott has a good sense of humour.

Ultimately it's about allowing you to write your programs focusing on the "happy path", ie. the code path when everything goes right. We often think about our code along the happy path. Unfortunately, you then have to add additional code for handling errors, typically by throwing if/else or try/catch blocks everywhere and making your code ugly in the process. An alternate error handling methodology is a concept he refers to as "two-track functions".

These functions can accepts 2 different types of input, and return 2 different types of output... like a railway station with two tracks, ya see! In the example, these types indicate either Success or Failure, and if several of your functions can accept and return both, you can chain them together easily. The talk actually covers some oft' confusing functional concepts like "monads" (scary quotes!) in an approachable way. Just go and watch the talk.

The language used is F#, which is billed as a functional language. Now, I'm no functional programmer. My vocabulary extends primarily to the so-called "scripting" languages: Perl, Python, Powershell, Bash... but Raku is kind of a distant relative of another functional language, Haskell. One of the initial Raku compilers (called Pugs) was implemented in Haskell.

Although Pugs is no longer actively developed, it was an important step in the path towards Rakudo, which is currently the most developed Raku compiler. Pugs helped solidify a lot of the ideas in Raku, and as the language was being implemented by people familiar with Haskell, there was a cross-pollination of some functional ideas from Haskell to Raku... but enough with the history lesson. I decided to see how I could implement something similar in Raku, just for a bit of fun.

I don't want to build and entire app here that receives input from a browser, validates an email address, updates a database, etc. I'm just going to do the first thing - validate an email - and I'm going to define some very banal checks on my email validator to make the code simple. Email addresses must contain an @, must be in lowercase, and mustn't be a .io... because I said so! This is not an effective way to validate an email address.

I start by defining my validator functions, which look like this.

sub contains-at(Str $s) {
    when not $s.contains('@') {
        fail("Address is missing '@' symbol")
    }
    default { $s }
}

sub is-lower(Str $s) {
    when $s ne $s.lc {
        fail("Address is not lower-case")
    }
    default { $s }
}

sub not-io(Str $s) {
    when not $s.ends-with('.io') {
        fail("I don't like '.io' domains")
    }
    default { $s }
}

For the conditional check inside my validators I'm using a when block, which comes from the given/when/default construct, Raku friendlier sounding version of switch/case. I've used this in place of if/else because when automatically short-circuits. It doesn't really matter in this check for which there is only 2 branches... but if I later decide to throw in additional checks, I can just add more when blocks.

In isolation, these functions are fine. If I pass an email address to each one, it will perform a validation check and if it passes the check, the address is returned. That means if I input a "valid" email address, I could chain them together and it would pass through each validator. This is the happy path... but if one of the validations fails, it will pass a Failure object to the next function which is expecting a Str and die. These functions all have one-track input, but two-track output. Here's a slide from the talk.

So how to we convert it to two-track input? Scott - staying true to his train track analogy - builds a kind of adaptor block. He does this with a "bind" operation, which binds an additional function to his validators. As the slide above implies, this adapter block handles a possible Failure input, turning it into a true two-track function, which can compose (chain) nicely with other two-track functions... but can I achieve this in Raku? I think so?

I reiterate that I'm barely knowledgeable on functional programming concepts, so I could be wrong here, but it seems that this concept of "bind" is similar to "wrap" in Raku. wrap is method on a Routine (aka, a function) that allows you to execute additional code around a function call. The docs tell me this is similar to Decorators in Python, if that helps. I can use a wrapper function to check for Failure (and return if so) before doing my stringy checks.

sub adapter($input) {
    when $input ~~ Failure { $input }
    default { callsame() }
}

I can now wrap this "adaptor block" function around my validators. If the adapter receives a failure, it simply returns it. Otherwise, callsame() will call the function that it's wrapped around. To wrap my functions, I'll create a Trait which simplifies applying it to my validator functions. I'm calling my trait "validator" but you could name it anything.

sub trait_mod:<is>(Routine $r, :$validator) {
    $r.wrap(&adapter)
}

Alternatively, I can do this all in one step by defining the adaptor function anonymously inside the trait definition.

sub trait_mod:<is>(Routine $r, :$validator) {
    $r.wrap(-> $input {
        when $input ~~ Failure { $input }
        default { callsame() }
    })
}

Once the trait is defined all I need to do to wrap my functions is add two words, is validator, to the function definition.

sub contains-at(Str $s) is validator {
    when not $s.contains('@') {
        fail("No '@' in email address")
    }
}

I'm almost finished, but there one more thing I want to take care of first. Raku is a dynamic language with gradual typing, so I don't need to define types on all my variables... but imagine you were implementing this sort of code in much larger system, with code spread across multiple files. Once a code base gets large enough, enforcing types everywhere helps maintain correctness, but what type will I get back from validate?

The answer is I might get either a Str or a Failure. In the original article, Scott defines a sum type called Result that can indicate either Success or Failure. Raku can do something similar with Junctions. Typically you might use Junctions to compare a value against multiple values... but it can also apply to type subsets. I won't cover Junctions here. You can refer to the docs, or Zoffix has a blog post about them here. Here, I'm just going to define a new subset type that type that has a constraint of any(Str, Failure).

my subset Result where Str|Failure;

This new Result sub-type will be my return value, and it will also be the type signature for my adaptor block... and that's about it! My validators have all been adapted to handle two-track input, I'm ready to chain them together,

sub validate(Str $input) {
    $input
    ==> contains-at()
    ==> is-lower()
    ==> not-io()
}

my Result $res = validate('User@host.org');

say "Validation check completed";

Well, that looks quite Functional with a capital F. This syntax uses the Raku feed operator, that allows functions to be chained together (similar to method chaining) which "feeds" the result of the previous function into the next. So now I'm ready to call my validate() function.

Before I do, though, I quickly want to cover what a Failure actually is. Whereas an Exception throws when it occurs, a Failure does not. You can pass a Failure around - or assign it to a variable - but if you try to evaluate it, it will be promoted to an Exception and dump a traceback to where it came from. This allows errors to be handled using standard error handling instead of try/catch.

Due to how a Failure works , I should always get the Validation check completed message regardless of success or failure. Note that my address has a capital letter, so if I simply tried to say $res, the Failure would be promoted to a full-blown Exception and throw... but the whole point of using a Failure is so I can handle it with simple error checking, thus avoiding an ugly traceback.

given $res {
    when Failure { say "FAILURE: $res.exception()" }
    default      { say "SUCCESS: $res"             }
}

I ran it (with the purposely "invalid" address) and go my desired output.

Validation check completed
FAILURE: Email address is not lower-case

Groovy! I changed my input and tested all the fail conditions and the "happy path" and it all worked as I hoped, so I'd call this a success, but there one more thing that bothering me: I have an identical default block duplicated in each validator. I decided to push my adaptor further and get it to handle the default return as well. There might be a better way to do it, but in typical /me fashion, I came up with something that worked and left it that way. Full code incoming.

my subset Result where Str|Failure;

sub trait_mod:<is>(Routine $r, :$validator) {
    $r.wrap(sub (Result $input) {
        when $input ~~ Failure { $input }
        default {
            my $result = callsame();
            when $result ~~ Failure { $result }
            default { $input }
        }
    })
}

sub contains-at(Str $s) is validator {
    when not $s.contains('@') {
        fail("No '@' in email address")
    }
}

sub is-lower(Str $s) is validator {
    when $s ne $s.lc {
        fail("Email address is not lower-case")
    }
}

sub not-io(Str $s) is validator {
    when $s.ends-with('.io') {
        fail("I don't like '.io' domains")
    }
}

sub validate(Str $input) {
    $input
    ==> contains-at()
    ==> is-lower()
    ==> not-io()
}

my Result $res = validate('User@host.org');

say "Validation check completed";

given $res {
    when Failure { say "FAILURE: $res.exception()" }
    default      { say "SUCCESS: $res"             }
}

Forget about the trait, check out my validator functions! Talk about your single responsibility principle. Each one now just has a single when block, and the trait is handling the potential "else" clause. Obviously I could have thrown all of these checks inside a single given block, disable fallthrough with proceed and be done with it... but that's not the point! Remember this is a very simplified model of this concept of two-track functions.

"so... like, a model train set?"

Quiet you! So there you have it. I'm sure smarter people than I can think of ways to improve and extend these ideas, but for me at least, it has been fun using Raku to explore some interesting functional concepts. I can certainly see some benefits to handling error checking this way, particularly if you have a large system that runs various validation checks on data as it passes though your pipeline.

Finally, Scott also advocates for defining your errors in an Enum and then stringifying them later. He makes a good case for this - and it's trivial to do - but I won't do it here, this post is too long already. It's not strictly a functional idea, but a good idea none-the-less. Think of it as your homework assignment.