dry-monads

repository·main·Indexed 21 days ago

https://github.com/dry-rb/dry-monads

A 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.

Tokens
10.9K
Snippets
44
Records
51
Agent score
74%

What's inside dry-monads

  1. Overview of dry-monads

    main
    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.
  2. What is the Maybe monad?

    main

    The Maybe monad represents a value that may or may not exist (i.e., it could be nil). It is composed of two main types:

    1. Maybe::Some: Represents the presence of a value. It wraps a non-nil object.
    2. Maybe::None: Represents the absence of a value (equivalent to nil).

    You can use Maybe.coerce(value) to wrap a value: if the value is nil, it returns None; otherwise, it returns Some(value).

    # If value is present
    maybe = Dry::Monads::Maybe.coerce(10) # => Some(10)
    
    # If value is nil
    maybe = Dry::Monads::Maybe.coerce(nil) # => None
  3. What is the Validated monad and how does it differ from Result?

    main

    The Validated monad represents the outcome of a validation process. It is similar to Result, but with a key difference in how it handles errors during composition:

    • Error Accumulation: Unlike Result, which short-circuits on the first error, Validated implements #apply in 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 an Array or List.
    • No Monadic Binding: Validated does not implement bind (or flat_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'])
  4. What is the Task monad?

    main
    The Task monad represents an asynchronous computation. It is implemented as a thin wrapper around Concurrent::Promise from the concurrent-ruby library. 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.
  5. How Right-biased monads work in dry-monads

    main

    Right-biased monads (like Result/Either, Maybe, and Try) are designed to chain operations that succeed. In these monads, operations like bind and fmap are 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, and and operate on this value.
    • Left side: Contains the error or empty state. Methods like bind and fmap are short-circuited and simply return the Left monad itself, allowing errors to propagate through a chain without manual checks.
  6. Use the Result monad for success/failure handling

    main

    The Result monad represents an operation that either succeeded or failed. It consists of two primary subclasses: Success (containing the successful value) and Failure (containing the error/failure value).

    You can use the Result class directly or include Dry::Monads[:result] in your class to gain access to Success() and Failure() 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? # => false
  7. Use Dry::Monads::Do::All for automatic Do-notation

    main

    Including Dry::Monads::Do::All in a class automatically wraps all defined methods with an unwrapping block. This allows you to use yield inside your methods to unwrap monadic values (like Success or Failure). If a method returns a Failure, 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
  8. Use the Try monad for exception handling

    main

    The Dry::Monads::Try monad 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 imperative begin/rescue blocks.

    To use the convenient Try[...] { ... } syntax, include Dry::Monads::Try::Mixin in 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)
  9. Install and require dry-monads

    main

    To use the dry-monads library in your Ruby project, ensure the gem is installed and then require the main entrypoint. Requiring dry/monads loads the core monad implementations (such as Result, Maybe, and Either) and makes them available for use in your application.

    require "dry/monads"
  10. Pretty-print monad values

    main

    The dry-monads library includes an extension that enables the pretty_print method on various monad types. This allows you to use standard Ruby PP (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)
  11. Use Do-notation to simplify monadic workflows

    main

    The Dry::Monads::Do.for method allows you to use yield within a method to unwrap monadic values (like Success or Failure) sequentially. If a method returns a failure, the execution of the workflow halts immediately and returns that failure. This avoids deeply nested if/else or case statements 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
    end
  12. Configure Maybe warning for implicit nil coercion

    main

    When Some#fmap returns nil, it triggers a warning because this behavior is deprecated and will be unsupported in dry-monads 2.0. You can disable these warnings by setting warn_on_implicit_coercion to false on the Maybe class.

    Dry::Monads::Maybe.warn_on_implicit_nil_coercion false