2.1 Maybe
(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 how 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.
> (define (safe-first lst) (if (empty? lst) nothing (just (first lst))))
Now, consider using that operation on a list of characters.
> (safe-first '(#\a #\b #\c)) (just #\a)
> (safe-first '()) #<nothing>
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))) (just 97)
> (map char->integer (safe-first '())) #<nothing>
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:
> (define (safe-/ a b) (if (zero? b) nothing (just (/ a b))))
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:
> (map (λ (x) (safe-/ 2 x)) (safe-first '(10 20 30))) (just (just 1/5))
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.
The chain function works just like map, but it joins the two wrappers together into a single wrapper after the operation is finished.
> (chain (λ (x) (safe-/ 2 x)) (safe-first '(10 20 30))) (just 1/5)
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)) (just 4/3)
> (divide-first-two '(5 0)) #<nothing>
> (divide-first-two '(5)) #<nothing>
> (divide-first-two '()) #<nothing>
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)) (just 20/11)
> (divide-first-two '(3 0)) #<nothing>
Using the monadic interface, we can sequence arbitrary computations that can fail without writing a single line of explicit error handling code.
> (just 'hello) (just 'hello)
> nothing #<nothing>
Optional values are monads that short-circuit on nothing.
> (map add1 (just 1)) (just 2)
> (map add1 nothing) #<nothing>
> ((pure +) (just 1) (just 2)) (just 3)
> (do [n <- (just 1)] (pure (add1 n))) (just 2)
The nothing binding also serves as a match expander that only recognizes the nothing value, but it must be surrounded with parentheses to be compatible with the syntax of match.
> (define/match (value-or-false mval) [((just val)) val] [((nothing)) #f]) > (value-or-false (just 'something)) 'something
> (value-or-false nothing) #f
> (maybe 0 add1 nothing) 0
> (maybe 0 add1 (just 1)) 2
> (maybe 0 add1 (just 2)) 3
> (from-just #f nothing) #f
> (from-just #f (just "hello")) "hello"
procedure
(from-just! just-value) → any/c
just-value : just?
> (from-just! (just "hello")) "hello"
> (from-just! nothing) from-just!: contract violation
expected: just?
given: #<nothing>
in: the 1st argument of
(-> just? any/c)
contract from:
<pkgs>/functional-lib/data/maybe.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/functional-lib/data/maybe.rkt:15:11
procedure
(filter-just maybes-lst) → list?
maybes-lst : (listof maybe?)
> (filter-just (list (just 1) nothing (just 3))) '(1 3)
> (map-maybe (λ (x) (if (positive? x) (just (sqrt x)) nothing)) (list -2 3 0 9)) '(1.7320508075688772 3)
procedure
(false->maybe v) → any/c
v : any/c
> (false->maybe #f) #<nothing>
> (false->maybe "hello") (just "hello")
syntax
(with-maybe-handler exn-pred? body ...)
exn-pred? : (any/c . -> . any/c)
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")) #<nothing>
> (with-maybe-handler exn:fail:contract? (bytes->string/utf-8 #"hello")) (just "hello")
procedure
(exn->maybe exn-pred? proc arg ...) → maybe?
exn-pred? : (any/c . -> . any/c) proc : procedure? arg : any/c
This can be especially useful when paired with curry, which can be used to produce a wrapped version of a procedure that throws exceptions that instead reports failures in terms of optional values.
> (define try-bytes->string/utf-8 (curry exn->maybe exn:fail:contract? bytes->string/utf-8)) > (try-bytes->string/utf-8 #"\303") #<nothing>
> (try-bytes->string/utf-8 #"hello") (just "hello")