Page 16 - HRM-00-v1
P. 16
LIBRARIES
Functional Ruby with `dry-monad`s
Note that this doesn’t require any specific checks for nil values. If nil is encountered Maybe returns None, which all subsequent steps will just pass through without trying to perform any further operations on it.
To extract the actual result we have two choices: the unsafe value! method, which will raise an error for None values, or the preferred value_or alternative, which allows the caller to specify a sensible de- fault value:
maybe_name(current_user).value!
#=> Dry::Monads::UnwrapError: value! was called on None
maybe_name(current_user).value_or(“ANONYMOUS USER”) #=> “ANONYMOUS USER»
Now let’s try this again with an actual user:
user = OpenStruct.new(name: “john monadoe”)
maybe_name(current_user)
#=> Some(“JOHN MONADOE”)
maybe_name(current_user).value!
#=> “JOHN MONADOE”
maybe_name(current_user).value_or(“ANONYMOUS USER”) #=> “JOHN MONADOE»
Success! Admittedly the maybe_name function is quite verbose, espe- cially compared to Ruby’s “lonely operator” (&.) or Rail’s try meth- od, which essentially achieves the same results. However, this was mostly done for demonstration purposes; generally one would use fmap in this case, which, unlike bind, works with blocks that return unwrapped values and automatically rewraps the result:
M.Maybe(nil).fmap(&:name).fmap(&:upcase).value_or(“ANONYMOUS USER”) “ANONYMOUS USER”
M.Maybe(current_user).fmap(&:name).fmap(&:upcase).value_or(“ANONYMOUS USER”) “JOHN MONADOE”
#=> Some(“JOHN MONADOE»)
Other Useful Monads
At this point, you may still wonder if all of this effort is really worth it just to avoid a couple of nil checks. However, there are different “con- texts” that have been modeled as monads, and everything we covered so far (bind, fmap) also applies to them.
The Result monad is similar to Maybe, but instead of None, it allows us to return an error object with additional information. For example, here’s a sqrt function, which provides an exception-safe wrapper around Ruby’s Math.sqrt:
require ‘dry/monads/result’
def sqrt(n)
return M.Failure(“Value needs to be >= 0”) if n < 0 M.Success(Math.sqrt(n))
end
sqrt(9)
#=> Success(3.0)
sqrt(-1)
#=> Failure(“Value needs to be >= 0»)
If the input value n is outside the acceptable range, we return an er- ror message wrapped in Failure; otherwise, the result is wrapped in Success. Of course these values are composable too:
sqrt(9).fmap { |n| n + 1 }.value_or(0) #=> 4.0
sqrt(-1).fmap { |n| n + 1 }.value_or(0) #=> 0
The Result monad is used to great effect in the dry-transaction 4 gem, which provides a business workflow DSL and is also available as an extension to dry-validation 5, a library for defining schemas and their accompanying validation rules.
Another useful monad is Try, which can be used for wrapping code that can potentially raise exceptions:
In case the user enters 0 (or just hits enter),
Try { 1 / 0 }.fmap { |n| n + 1 } Try::Error(ZeroDivisionError: divided by 0)
Dividing one by zero would cause a ZeroDivisionError, but instead an instance of Try::Error is returned. With valid input, everything works as expected, and we’ll receive Try::Value instead:
Try { 1 / 1 }.fmap { |n| n + 1 } #=> Try::Value(2)
Try { 1 / 1 }.bind { |n| n + 1 } #=>2
The possible result of a Try operation can be converted to a Result or Maybe value by using to_result or to_maybe.
Do Notation
Functional languages like Haskell and Scala provide a special syntax for working with monads, called “do notation.” While it’s not possi- ble to mirror this exactly in Ruby, dry-monads provides a reasonable alternative 6.
The following example demonstrates how a function for transferring money could use do notation to sequence steps that can fail:
require ‘dry/monads/result’ require ‘dry/monads/do/all’
def transfer_money(params)
sender = yield fetch_user(params[:sender_id]) receiver = yield fetch_user(params[:receiver_id]) amount = yield verify_amount(params[:amount])
September 2019
| 16