dry-monads
repository·main·Indexed 21 days ago
https://github.com/dry-rb/dry-monadsA Ruby library providing functional programming abstractions (monads) to manage control flow, errors, and side effects. It includes implementations such as Result (Success/Failure), Maybe (Some/None), Try, List, Validated, Task, and Lazy, along with Do-notation for simplifying monadic workflows via yield.
What's inside dry-monads
- dry-monads is a Ruby library that provides functional programming abstractions, specifically monads, to help manage control flow and side effects in a predictable way. It is part of the dry-rb ecosystem.
What is the Maybe monad?
mainThe
Maybemonad represents a value that may or may not exist (i.e., it could benil). It is composed of two main types:Maybe::Some: Represents the presence of a value. It wraps a non-nil object.Maybe::None: Represents the absence of a value (equivalent tonil).
You can use
Maybe.coerce(value)to wrap a value: if the value isnil, it returnsNone; otherwise, it returnsSome(value).# If value is present maybe = Dry::Monads::Maybe.coerce(10) # => Some(10) # If value is nil maybe = Dry::Monads::Maybe.coerce(nil) # => NoneWhat is the Validated monad and how does it differ from Result?
mainThe
Validatedmonad represents the outcome of a validation process. It is similar toResult, but with a key difference in how it handles errors during composition:- Error Accumulation: Unlike
Result, which short-circuits on the first error,Validatedimplements#applyin a way that concatenates errors. - Requirement: For error concatenation to work, the error type must implement the
+method (it must be a semigroup), such as anArrayorList. - No Monadic Binding:
Validateddoes not implementbind(orflat_map) because doing so would violate monad laws in the context of error accumulation. It is used for parallel validation rather than sequential dependency.
When using
List<Validated>#traverse, the errors are automatically wrapped in a list, so you don't have to manually manage the collection of errors.# Example: Error accumulation with List # If you have multiple invalid results, Validated concatenates the errors. List::Validated[Valid('London'), Invalid(:name_missing), Invalid(:email_missing)] # => Invalid(List[:name_missing, :email_missing]) # Example: Successful results List::Validated[Valid('London'), Valid('John')] # => Valid(List['London', 'John'])- Error Accumulation: Unlike
What is the Task monad?
mainTheTaskmonad represents an asynchronous computation. It is implemented as a thin wrapper aroundConcurrent::Promisefrom theconcurrent-rubylibrary. It allows you to model and compose operations that will be executed asynchronously, providing a monadic interface for handling success and failure in an async context.How Right-biased monads work in dry-monads
mainRight-biased monads (like
Result/Either,Maybe, andTry) are designed to chain operations that succeed. In these monads, operations likebindandfmapare executed on the internal value if it represents a 'success' (the Right side), but are bypassed if the monad represents a 'failure' (the Left side).- Right side: Contains the successful value. Methods like
bind,fmap, andandoperate on this value. - Left side: Contains the error or empty state. Methods like
bindandfmapare short-circuited and simply return the Left monad itself, allowing errors to propagate through a chain without manual checks.
- Right side: Contains the successful value. Methods like
Use the Result monad for success/failure handling
mainThe
Resultmonad represents an operation that either succeeded or failed. It consists of two primary subclasses:Success(containing the successful value) andFailure(containing the error/failure value).You can use the
Resultclass directly or includeDry::Monads[:result]in your class to gain access toSuccess()andFailure()constructor methods.include Dry::Monads[:result] def perform_operation(id) if id > 0 Success(id) else Failure(:invalid_id) end end result = perform_operation(10) result.success? # => true result.failure? # => falseUse Dry::Monads::Do::All for automatic Do-notation
mainIncluding
Dry::Monads::Do::Allin a class automatically wraps all defined methods with an unwrapping block. This allows you to useyieldinside your methods to unwrap monadic values (likeSuccessorFailure). If a method returns aFailure, the execution of the calling method is automatically halted.Key behaviors:
- Automatic Wrapping: Every method defined in the class is wrapped, so you don't need to list them explicitly as you would with
Do.for(...). - Precedence: If you explicitly pass a block to a method call (e.g.,
method { ... }), that block takes precedence over the automatic unwrapping logic. - Visibility Preservation: The wrapper respects the original visibility (public, protected, or private) of the methods.
require 'dry/monads/do/all' require 'dry/monads/result' class CreateUser include Dry::Monads::Do::All include Dry::Monads::Result::Mixin def call(params) # If `validate` returns Failure, the execution will be halted values = yield validate(params) user = create_user(values) # An explicitly passed block takes precedence over the unwrapping block safely_subscribe(values[:email]) { puts "Already subscribed" } Success(user) end def validate(params) if params.key?(:email) Success(email: params[:email]) else Failure(:no_email) end end def create_user(user) # This method is wrapped, but we don't use the yielded block here UserRepo.new.add(user) end def safely_subscribe(email) repo = SubscriptionRepo.new if repo.subscribed?(email) # This calls the block explicitly passed from `call` via `yield` yield else repo.subscribe(email) end end end- Automatic Wrapping: Every method defined in the class is wrapped, so you don't need to list them explicitly as you would with
Use the Try monad for exception handling
mainThe
Dry::Monads::Trymonad represents a value that can either be a success (Try::Value) or a failure caused by an exception (Try::Error). It is used to wrap code that might raise exceptions, allowing you to handle errors functionally rather than using imperativebegin/rescueblocks.To use the convenient
Try[...] { ... }syntax, includeDry::Monads::Try::Mixinin your class.class Foo include Dry::Monads::Try::Mixin def safe_db_call # Wraps the block and rescues only ZeroDivisionError Try[ZeroDivisionError] { 1 / 0 } end end foo = Foo.new result = foo.safe_db_call # => Try::Error(ZeroDivisionError: divided by 0)Install and require dry-monads
mainTo use the
dry-monadslibrary in your Ruby project, ensure the gem is installed and then require the main entrypoint. Requiringdry/monadsloads the core monad implementations (such asResult,Maybe, andEither) and makes them available for use in your application.require "dry/monads"Pretty-print monad values
mainThe
dry-monadslibrary includes an extension that enables thepretty_printmethod on various monad types. This allows you to use standard RubyPP(Pretty Print) tools to get a human-readable representation of the monad's state and its contained value or error.Supported monads include:
Maybe(Some,None)Result(Success,Failure)Try(Value,Error)List(List)Validated(Valid,Invalid)Task(Task)Lazy(Lazy)
Use Do-notation to simplify monadic workflows
mainThe
Dry::Monads::Do.formethod allows you to useyieldwithin a method to unwrap monadic values (likeSuccessorFailure) sequentially. If a method returns a failure, the execution of the workflow halts immediately and returns that failure. This avoids deeply nestedif/elseorcasestatements when chaining multiple monadic operations.To use it, call
include Dry::Monads::Do.for(:method_name, ...)in your class, specifying the methods you want to wrap with Do-notation logic.class CreateUser include Dry::Monads::Result::Mixin include Dry::Monads::Try::Mixin # Specify which methods should support 'yield' to unwrap monads include Dry::Monads::Do.for(:call) def call(params) # If parse_json returns a Failure, the method halts here and returns the Failure json = yield parse_json(params) # If validate returns a Failure, the method halts here hash = yield validate(json) # You can also use yield inside blocks like transactions user_repo.transaction do user = yield create_user(hash[:user]) yield create_profile(user, hash[:profile]) end Success(user) end private def parse_json(params) Try(JSON::ParserError) { JSON.parse(params) }.to_result end def validate(json) UserSchema.(json).to_monad end def create_user(user_data) Try(Sequel::Error) { user_repo.create(user_data) }.to_result end def create_profile(user, profile_data) Try(Sequel::Error) { user_repo.create_profile(user, profile_data) }.to_result end endConfigure Maybe warning for implicit nil coercion
mainWhen
Some#fmapreturnsnil, it triggers a warning because this behavior is deprecated and will be unsupported indry-monads2.0. You can disable these warnings by settingwarn_on_implicit_coerciontofalseon theMaybeclass.Dry::Monads::Maybe.warn_on_implicit_nil_coercion false