pzol/monadic
{ "createdAt": "2012-04-29T04:29:28Z", "defaultBranch": "master", "description": "helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in Scala to my ruby projects", "fullName": "pzol/monadic", "homepage": "", "language": "Ruby", "name": "monadic", "pushedAt": "2016-01-28T11:23:01Z", "stargazersCount": 227, "topics": [], "updatedAt": "2025-10-24T10:16:33Z", "url": "https://github.com/pzol/monadic"}Monadic
Section titled “Monadic”I am currently working on a successor, currently under the working name Deterministic. Check it out and let me know your thoughts and wishes.
helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in Scala and Haskell to my ruby projects.
My motivation to create this gem was that I often work with nested Hashes and need to reach deeply inside of them so my code is sprinkled with things like some_hash.fetch(:one, {}).fetch(:two, {}).fetch(:three, “unknown”).
We have the following monadics (monads, functors, applicatives and variations):
- Maybe - use if you have one exception
- Either - use if you have many exceptions, and one call depends on the previous
- Validation - use if you have many independent calls (usually to validate an object)
What’s the point of using monads in ruby? To me it started with having a safe way to deal with nil objects and other exceptions. Thus you contain the erroneous behaviour within a monad - an indivisible, impenetrable unit. Functional programming considers throwing exceptions to be a side-effect, instead we propagate exceptions, i.e. return them as a result of a function call.
A monad is most effectively described as a computation that eventually returns a value. — Wolfgang De Meuter
Most people probably will be interested in the Maybe monad, as it solves the problem with nil invocations, similar to andand and others.
Maybe is an optional type, which helps to handle error conditions gracefully. The one thing to remember about option is: ‘What goes into the Maybe, stays in the Maybe’.
Maybe(User.find(123)).name._ # ._ is a shortcut for .fetch
# confidently diving into nested hashesMaybe({})[:a][:b][:c] == NothingMaybe({})[:a][:b][:c].fetch('unknown') == "unknown"Maybe(a: 1)[:a]._ == 1Basic usage examples:
# handling nil (None serves as NullObject)Maybe(nil).a.b.c == Nothing
# NothingMaybe(nil)._ == Nothing"#{Maybe(nil)}" == "Nothing"Maybe(nil)._("unknown") == "unknown"Maybe(nil).empty? == trueMaybe(nil).truly? == false
# Just stays Just, unless you unbox itMaybe('FOO').downcase == Just('foo')Maybe('FOO').downcase.fetch == "foo" # unboxing the valueMaybe('FOO').downcase._ == "foo"Maybe('foo').empty? == false # always non-emptyMaybe('foo').truly? == true # depends on the boxed valueMaybe(false).empty? == falseMaybe(false).truly? == falseMap, select:
Maybe(123).map { |value| User.find(value) } == Just(someUser) # if user foundMaybe(0).map { |value| User.find(value) } == Nothing # if user not foundMaybe([1,2]).map { |value| value.to_s } == Just(["1, 2"]) # for all Enumerables
Maybe('foo').select { |value| value.start_with?('f') } == Just('foo')Maybe('bar').select { |value| value.start_with?('f') } == NothingFor Enumerable use #flat_map:
Maybe([1, 2]).flat_map {|v| v + 1 } == Just([2, 3])Treat it like an array:
Maybe(123).to_a == [123] Maybe([123, 456]).to_a == [123, 456] Maybe(nil).to_a == []#to_s
Maybe(nil).to_s == 'Nothing'Maybe(1).to_s == '1'#or
Maybe(nil).or(1) == Just(1)Maybe(1).or(2) == Just(1)Maybe(nil).or(1) == Just(1)Maybe(nil).or(nil) == NothingFalsey values (kind-of) examples:
user = Maybe(User.find(123))user.name._
user.subscribed? # always trueuser.subscribed?.truly? # true if subscribed is trueuser.subscribed?.fetch(false) # same as aboveuser.subscribed?.or(false) # same as aboveRemember! a Maybe is never false (in Ruby terms), if you want to know if it is false, call #empty? of #truly?
#truly? will return true or false, always.
Maybe supports #proxy to avoid naming clashes between the underlying value and Maybe itself.
Maybe({a: 1}).proxy.fetch(:a) == Maybe(1)# this is in effect syntactic sugar forMaybe({a: 1}).map {|e| e.fetch(:a) }Slug example
# instead ofdef slug(title) if title title.strip.downcase.tr_s('^[a-z0-9]', '-') endend
# or
def slug(title) title && title.strip.downcase.tr_s('^[a-z0-9]', '-')end
# do it with a defaultdef slug(title) Maybe(title).strip.downcase.tr_s('^[a-z0-9]', '-')._('unknown-title')endObject#_?
Section titled “Object#_?”Works similar to the Elvis operator _? - ruby does not allow ?: as operator and use it like the excellent andand
require 'monadic/core_ext/object' # this will import _? into the global Objectnil._? == Nothing"foo"._? == 'foo'{}._?.a.b == Nothing{}._?[:foo] == NothingIn fact this is a shortcut notation for Maybe(obj)
Either
Section titled “Either”Its main purpose here to handle errors gracefully, by chaining multiple calls in a functional way and stop evaluating them as soon as the first fails.
Assume you need several calls to construct some object in order to be useful, after each you need to check for success. Also you want to catch exceptions and not let them bubble upwards.
What is specific to this implementation is that exceptions are caught within the execution blocks. This way I have all error conditions wrapped in one place.
Success represents a successfull execution of an operation (Right in Scala, Haskell).
Failure represents a failure to execute an operation (Left in Scala, Haskell).
The Either() wrapper works like a coercon. It will treat all falsey values nil, false or empty? as a Failure and all others as Success. If that does not suit you, use Success or Failure only. However as ruby cannot enforce the value returned from within a bind, it will auto-magically coerce the return value into an Either.
result = parse_and_validate_params(params). # must return a Success or Failure inside bind ->(user_id) { User.find(user_id) }. # if #find returns null it will become a Failure bind ->(user) { authorized?(user); user }. # if authorized? raises an Exception, it will be a Failure bind ->(user) { UserDecorator(user) }
if result.success? @user = result.fetch # result.fetch or result._ contains the inner value render 'page'else @error = result.fetch render 'error_page'endYou can use alternate syntaxes to achieve the same goal:
# block and Haskell like >= operatorEither(operation). >= { successful_method }. >= { failful_operation }
# start with a Success, for instance a parameterSuccess('pzol'). bind ->(previous) { good }. bind -> { bad }
Either.chain do bind -> { good } # >= is not supported for Either.chain, only bind bind -> { better } # better returns Success(some_int) bind ->(previous_result) { previous_result + 1 }end
either = Either(something)either += truth? Success('truth, only the truth') : Failure('lies, damn lies')Exceptions are wrapped into a Failure:
Either(true). bind -> { fail 'get me out of here' } # return a Failure(RuntimeError)Another example:
Success(params). bind ->(params) { Either(params.fetch(:path)) } # fails if params does not contain :path bind ->(path) { load_stuff(params) } #Either#or allows to provide alternate values in case of Failure:
Either(false == true).or('false was not true') == Failure(false was not true)Success('truth needs no sugar coating').or('all lies') == Success('truth needs no sugar coating')Either#or supports also a block
Failure(1).or {|other| 1 + 2 } == Failure(3)Storing intermediate results in instance variables is possible, although it is not very elegant:
result = Either.chain do bind { @map = { one: 1, two: 2 } } bind { @map.fetch(:one) } bind { |p| Success(p + 100) }end
result == Success(101)Try helper which works similar to Either, but takes a block. Think of it as a secure if-then-else.
Try { Date.parse('2012-02-30') } == FailureTry { Date.parse('2012-02-28') } == Success
date_s = '2012-02-30'Try { Date.parse(date_s) }.or {|e| "#{e.message} #{date_s}" } == Failure("invalid date 2012-02-30")
# with a predicateTry(true) == Success(true)Try(false) { "string" } == Failure("string")Try(false) { "success"}.or("fail") == Failure("fail")
VALID_TITLES = %w[MR MRS]title = 'MS'Try(VALID_TITLES.inlude?(title)) { title }.or { "title must be on of '#{VALID_TITLES.join(', ')}'' but was '#{title}'"} == "title must be on of 'MR, MRS' but was 'MS'"Validation
Section titled “Validation”The Validation applicative functor, takes a list of checks within a block. Each check must return either Success of Failure.
If Successful, it will return Success, if not a Failure monad, containing a list of failures.
Within the Failure() provide the reason why the check failed.
Example:
def validate(person) check_age = ->(age_expr) { age = age_expr.to_i case when age <= 0; Failure('Age must be > 0') when age >= 130; Failure('Age must be < 130') else Success(age) end }
check_sobriety = ->(sobriety) { case sobriety when :sober, :tipsy; Success(sobriety) when :drunk ; Failure('No drunks allowed') else Failure("Sobriety state '#{sobriety}' is not allowed") end }
Validation() do check { check_age.(person.age) } check { check_sobriety.(person.sobriety) } endendThe above example, returns either Success([32, :sober]) or Failure(['Age must be > 0', 'No drunks allowed']) with a list of what went wrong during the validation.
See also examples/validation.rb and examples/validation_module
Validation#fill method for validating filling Structs:
ExampleStruct = Struct.new(:a, :b)module ExampleValidator extend self def a(params); Try { params[0] }.or 'a cannot be empty'; end def b(params); Try { params[1] }.or 'b cannot be empty'; endend
result = Validation.fill(ExampleStruct, [1, 2], ExampleValidator) == Sucessexample = result.fetchexample.a == 1example.b == 2All Monads include this module. Standalone it is an Identity monad. Not useful on its own. It’s methods are usable on all its descendants.
#map is used to map the inner value
# minimum implementation of a monadclass Identity include Monadic::Monad def self.unit(value) new(value) endend
Identity.unit('FOO').map(&:capitalize).map {|v| "Hello #{v}"} == Identity(Hello Foo)Identity.unit([1,2]).map {|v| v + 1} == Identity([2, 3])#bind allows (priviledged) access to the boxed value. This is the traditional no-magic #bind as found in Haskell,
You` are responsible for re-wrapping the value into a Monad again.
# due to the way it works, it will simply return the value, don't rely on this though, different Monads may# implement bind differently (e.g. Maybe involves some _magic_)Identity.unit('foo').bind(&:capitalize) == Foo
# proper useIdentity.unit('foo').bind {|v| Identity.unit(v.capitalize) } == Identity(Foo)#fetch extracts the inner value of the Monad, some Monads will override this standard behaviour, e.g. the Maybe Monad
Identity.unit('foo').fetch == "foo"References
Section titled “References”- [Wikipedia Monad]!(See also http://en.wikipedia.org/wiki/Monad)
- Learn You a Haskell - for a few monads more
- Monad equivalent in Ruby
- Option Type
- NullObject and Falsiness by @avdi
- andand
- ick
- Monads in Ruby
- The Maybe Monad in Ruby
- Monads in Ruby with nice syntax
- Maybe in Ruby
- Monads on the Cheap
- Rumonade - another excellent (more scala-like) monad implementation
- Monads for functional programming
- Monads as a theoretical foundation for AOP
- What is an applicative functor?
Installation
Section titled “Installation”Add this line to your application’s Gemfile:
gem 'monadic'And then execute:
$ bundleOr install it yourself as:
$ gem install monadicCompatibility
Section titled “Compatibility”Monadic is tested under ruby MRI 1.9.2, 1.9.3 jruby 1.9 mode, rbx 1.9 mode are currently not passing the tests on travis
Contributing
Section titled “Contributing”- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Added some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request

