How to define and work with a Rust-like result type in NuShell

Friedrich Kurz - Jun 21 - - Dev Community

Motivation

Rust—among other modern programming languages—has a data type Result<T, E> in its standard library, that allows us to represent error states in a program directly in code. Using data structures to represent errors in code is a pattern known mostly from purely functional programming languages but has gained some traction in programming languages that follow a less restrictive language paradigm (like Rust).

The idea of modelling errors in code is that—following the functional credo—everything the function does should be contained in the return value. Side effects, like errors, should be avoided wherever possible.

This is a nice pattern, since it forces you to address that code may not succeed. (If you don't ignore the return value, of course. :D) Combining the use of Result return types with Rust's pattern matching also improves code legibility, in my opinion.

Error handling in NuShell

NuShell is a very modern shell and shell language that draws some heavy inspiration from Rust and that I really enjoy writing small programs and glue code in (especially for CI/CD pipelines).

In contrast to Rust, NuShell has a try/catch control structure to capture and deal with errors. There is no result type in the standard library at the time of writing.

NuShell's try/catch, moreover, has the major downside, that you cannot react to specifics of an error, since the catch block doesn't receive any parameter like an exception object, that would allow us introspection on what went wrong.

So what can we do? Well, we may just define a Result type ourselves and use it. Since NuShell also has pattern matching using the match keyword, we can write some pretty readable code with it.

Consider, for example, malformed URLs when using the http command (in this case the protocol is missing):

nu> http get --full www.google.com
Error: nu::shell::unsupported_input

  × Unsupported input
   ╭─[entry #64:1:1]
 1 │ http get --full www.google.com
   · ────┬───        ───────┬──────
   ·     │                  ╰── value: '"www.google.com"'
   ·     ╰── Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com
Enter fullscreen mode Exit fullscreen mode

The code above will crash. As mentioned before, we could use try/catch. But the problem remains, how do we enable the calling code to react to errors?

use std log 

try {
    return http get --full www.google.com # Missing protocol
} catch {
    log error "GET \"www.google.com\" failed."
     # Now what?
}
Enter fullscreen mode Exit fullscreen mode

Using a result type (and some convenience conversion functions into ok and into error), we can write a safe http get function as follows:

def safe_get [url: string] { 
  try {
    let response = http get --full $url
    $response | into ok
  } catch {
    {url: $url} | into error
  }
}
Enter fullscreen mode Exit fullscreen mode

We could use it in our code like this:

nu> match (safe_get "https://www.google.com") {                                    
    {ok: $response} => { print $"request succeeded: ($response.status)" },
    {error: $_} => { print "request failed" }
}
request succeeded: 200
Enter fullscreen mode Exit fullscreen mode

And for the failure case:

match (safe_get "www.google.com") {                                              
    {ok: $response} => { print $"request succeeded: ($response.status)" },
    {error: $_} => { print "request failed" }
}
request failed
Enter fullscreen mode Exit fullscreen mode

Now the calling code can react to failure by disambiguating the return values and processing the attached data.

Addendum

Here are the helper functions into ok and into error for completeness sake.

export def "into ok" [value?: any] {
    let v = if $value == null { $in } else { $value }

    {ok: $v}
}

export def "into error" [cause?: any] {
    let c = if $cause == null { $in } else { $cause }

    {error: $c}
}
Enter fullscreen mode Exit fullscreen mode
. . . . .
Terabox Video Player