|(require data/maybe)||package: functional-lib|
The maybe pattern implements optional values, values that represent computations that can fail. Idiomatic Scheme uses #f to represent a “lack of a value”, similar to now null is used in other programming languages, but this exhibits a few problems:
Sometimes #f can be a valid value, at which point it is ambiguous whether or not a result is nonexistent or if it is simply the value #f.
Composing operations that can fail can be tedious and can result in deeply nested conditionals, as each step of the computation must check if the value is #f and short-circuit if necessary.
Maybe reifies the concept of a lack of a value as nothing and the presence of a value as just. It then provides a series of combinators to help work with operations that can fail without excessive error-checking.
Optional values are functors, applicative functors, and monads. This provides a reasonable framework for managing failable computations in a consistent and extensible way. For example, consider an operation that can fail.
Now, consider using that operation on a list of characters.
> (safe-first '(#\a #\b #\c))
> (safe-first '())
It is possible that you might want to, rather than retrieve the first character, get the first character’s unicode code point. If safe-first returned #f rather than nothing upon failure, you would need to branch to check if the value was found before attempting to convert the character to an integer.
(let ([c (safe-first list-of-chars)]) (if c (char->integer c) #f))
It would be possible to use and to make things a little shorter, but the explicit error-checking would still be necessary. However, since optional values are just functors, it is possible to just use map.
> (map char->integer (safe-first '(#\a #\b #\c)))
> (map char->integer (safe-first '()))
Consider another example: safely dividing a number without having division-by-zero errors. We can implement a safe-/ function like we did with safe-first:
Now, obviously we could use it just like we used safe-first, but what if we want to use them both together? That is, we want to call safe-/ on the result of safe-first. We could try using map again, which seems like it should work:
Oops, now we have a just wrapped inside another just. This is because map replaces whatever is inside the functor, not the functor itself, and we returned (just 1/5) from our mapping function. Instead, we want the inner just to be subsumed by the outer one. For that, we can use chain.
We can use multiple calls to chain to sequence many failable operations at once. For example, we could write a function that divides the first two numbers of a list that won’t ever throw exceptions:
> (define (divide-first-two lst) (chain (λ (a) (chain (λ (xs) (chain (λ (b) (safe-/ a b)) (safe-first xs))) (safe-rest lst))) (safe-first lst))) > (divide-first-two '(4 3 2 1))
> (divide-first-two '(5 0))
> (divide-first-two '(5))
> (divide-first-two '())
It works! That is, itself, kinda cool. Unfortunately, following all the nested calls to chain will very likely make your head spin. That’s where do comes in. The same exact function can be rewritten using do in a much clearer way:
> (define (divide-first-two lst) (do [a <- (safe-first lst)] [xs <- (safe-rest lst)] [b <- (safe-first xs)] (safe-/ a b))) > (divide-first-two '(20 11))
> (divide-first-two '(3 0))
Using the monadic interface, we can sequence arbitrary computations that can fail without writing a single line of explicit error handling code.
> (map add1 (just 1))
> (map add1 nothing)
> ((pure +) (just 1) (just 2))
> (do [n <- (just 1)] (pure (add1 n)))
> (define/match (value-or-false mval) [((just val)) val] [((nothing)) #f]) > (value-or-false (just 'something))
> (value-or-false nothing)
> (from-just! (just "hello"))
> (from-just! nothing)
from-just!: contract violation
in: the 1st argument of
(-> just? any/c)
(assuming the contract is correct)
> (filter-just (list (just 1) nothing (just 3)))
> (map-maybe (λ (x) (if (positive? x) (just (sqrt x)) nothing)) (list -2 3 0 9))
> (false->maybe #f)
> (false->maybe "hello")
This is useful for interacting with Racket APIs that throw exceptions upon failure and adapting them to produce optional values instead.
> (with-maybe-handler exn:fail:contract? (bytes->string/utf-8 #"\303"))
> (with-maybe-handler exn:fail:contract? (bytes->string/utf-8 #"hello"))
exn-pred? : (any/c . -> . any/c) proc : procedure? arg : any/c
> (define try-bytes->string/utf-8 (curry exn->maybe exn:fail:contract? bytes->string/utf-8)) > (try-bytes->string/utf-8 #"\303")
> (try-bytes->string/utf-8 #"hello")