CrackedRuby CrackedRuby

Monads and Functors Basics

Overview

Monads and functors represent abstractions from category theory adapted for functional programming. These patterns encapsulate values within computational contexts and define operations for transforming and combining those contexts. A functor maps between categories while preserving structure, implementing a transform operation that applies functions to wrapped values. A monad extends functor capabilities by adding operations that flatten nested contexts and sequence computations that produce wrapped values.

The practical significance emerges when managing effects like optional values, error handling, asynchronous operations, or state manipulation. Rather than unwrapping values, checking conditions, and manually rewrapping results at each step, these abstractions handle the plumbing automatically. Code becomes declarative, describing transformations rather than control flow mechanics.

Ruby's dynamic nature and flexible syntax enable monad and functor implementations, though the language lacks native support found in strictly functional languages. The community has developed several libraries implementing these patterns, while Ruby's built-in Enumerable module demonstrates functor-like behavior through methods like map and flat_map.

# Basic functor behavior - transforming wrapped values
[1, 2, 3].map { |x| x * 2 }
# => [6, 4, 6]

# Monad-like behavior - flattening nested structures
[[1, 2], [3, 4]].flat_map { |arr| arr.map { |x| x * 2 } }
# => [2, 4, 6, 8]

The terminology derives from abstract algebra: functor from function-like mappings between categories, monad from monoid structures in category theory. Understanding these patterns requires distinguishing the wrapped value from its container, recognizing that operations work on the computational context rather than directly on contained values.

Key Principles

A functor consists of a container type and a mapping function. The container wraps values of some type, while the mapping function transforms wrapped values by applying a provided function without unwrapping. The transformation preserves structure - mapping over an empty container yields an empty container, mapping over a container with three elements yields a container with three transformed elements.

Functors satisfy two laws. The identity law states that mapping with the identity function returns the original container unchanged. The composition law requires that mapping with composed functions produces the same result as mapping with each function sequentially. These laws ensure predictable behavior and enable equational reasoning about code.

# Identity law: fmap(identity) = identity
class Box
  attr_reader :value
  
  def initialize(value)
    @value = value
  end
  
  def fmap(&block)
    Box.new(block.call(@value))
  end
end

identity = ->(x) { x }
box = Box.new(42)
box.fmap(&identity).value == box.value
# => true

# Composition law: fmap(f . g) = fmap(f) . fmap(g)
double = ->(x) { x * 2 }
add_one = ->(x) { x + 1 }

box.fmap { |x| add_one.call(double.call(x)) }.value ==
  box.fmap(&double).fmap(&add_one).value
# => true

Monads extend functors with two additional operations: unit (also called return or pure) and bind (also called flatMap or chain). Unit wraps a bare value into the monadic context. Bind applies a function that returns a monad to a monadic value, automatically flattening the result to avoid nested monads.

The monad laws constrain these operations. Left identity requires that binding over a unit-wrapped value applies the function directly. Right identity states that binding with unit returns the original monad unchanged. Associativity ensures that the order of binding operations doesn't matter - binding functions f and g sequentially produces the same result as binding a function that binds f then g.

# Monad operations: unit and bind
class Maybe
  def self.unit(value)
    value.nil? ? Nothing.new : Just.new(value)
  end
  
  def bind(&block)
    raise NotImplementedError
  end
end

class Just < Maybe
  attr_reader :value
  
  def initialize(value)
    @value = value
  end
  
  def bind(&block)
    block.call(@value)
  end
end

class Nothing < Maybe
  def bind(&block)
    self
  end
end

# Left identity: unit(a).bind(f) = f(a)
f = ->(x) { Maybe.unit(x * 2) }
Maybe.unit(21).bind(&f).value == f.call(21).value
# => true

# Right identity: m.bind(unit) = m
m = Maybe.unit(42)
m.bind { |x| Maybe.unit(x) }.value == m.value
# => true

The distinction between map (functor operation) and bind (monad operation) centers on return types. Map takes a function from a bare type to another bare type and lifts it to work on wrapped values. Bind takes a function from a bare type to a wrapped type, preventing nested wrappers by flattening automatically.

Applicative functors occupy the middle ground between functors and monads. They extend functors with operations for applying wrapped functions to wrapped values, enabling multi-argument functions to work on wrapped arguments. Every monad qualifies as an applicative functor, which qualifies as a functor, forming a hierarchy of abstractions with increasing power and constraints.

Ruby Implementation

Ruby lacks first-class support for algebraic data types and type classes, requiring explicit implementation of monad and functor patterns. The dynamic type system permits flexible implementations but sacrifices compile-time guarantees provided by statically typed functional languages.

The Maybe monad handles optional values without explicit nil checks. The Just variant wraps present values, while Nothing represents absence. Operations on Nothing short-circuit without executing the provided function, propagating absence through a computation chain.

class Maybe
  def self.unit(value)
    value.nil? ? Nothing.new : Just.new(value)
  end
  
  def self.from_nullable(value)
    unit(value)
  end
  
  def fmap(&block)
    bind { |value| Maybe.unit(block.call(value)) }
  end
end

class Just < Maybe
  attr_reader :value
  
  def initialize(value)
    @value = value
  end
  
  def bind(&block)
    block.call(@value)
  end
  
  def value_or(_default)
    @value
  end
  
  def to_s
    "Just(#{@value})"
  end
end

class Nothing < Maybe
  def bind(&block)
    self
  end
  
  def value_or(default)
    default
  end
  
  def to_s
    "Nothing"
  end
end

# Chaining operations without nil checks
def find_user(id)
  # Simulated database lookup
  id == 1 ? Maybe.unit({id: 1, name: "Alice", address_id: 10}) : Maybe.unit(nil)
end

def find_address(address_id)
  address_id == 10 ? Maybe.unit({id: 10, street: "Main St"}) : Maybe.unit(nil)
end

result = find_user(1)
  .bind { |user| Maybe.unit(user[:address_id]) }
  .bind { |addr_id| find_address(addr_id) }
  .bind { |addr| Maybe.unit(addr[:street]) }

puts result.value_or("Unknown address")
# => Main St

The Either monad extends Maybe by capturing failure reasons. The Right variant represents success with a value, while Left represents failure with an error. By convention, computations proceed through Right values and short-circuit on Left, accumulating the first error encountered.

class Either
  def self.right(value)
    Right.new(value)
  end
  
  def self.left(error)
    Left.new(error)
  end
  
  def fmap(&block)
    bind { |value| Either.right(block.call(value)) }
  end
end

class Right < Either
  attr_reader :value
  
  def initialize(value)
    @value = value
  end
  
  def bind(&block)
    block.call(@value)
  end
  
  def value_or(_default)
    @value
  end
  
  def either(left_fn, right_fn)
    right_fn.call(@value)
  end
end

class Left < Either
  attr_reader :error
  
  def initialize(error)
    @error = error
  end
  
  def bind(&block)
    self
  end
  
  def value_or(default)
    default
  end
  
  def either(left_fn, right_fn)
    left_fn.call(@error)
  end
end

# Error handling without exceptions
def parse_integer(str)
  Integer(str)
  Either.right(Integer(str))
rescue ArgumentError
  Either.left("Invalid integer: #{str}")
end

def divide(numerator, denominator)
  denominator.zero? ? 
    Either.left("Division by zero") :
    Either.right(numerator / denominator)
end

result = parse_integer("10")
  .bind { |n| parse_integer("2").fmap { |d| [n, d] } }
  .bind { |n, d| divide(n, d) }

result.either(
  ->(error) { puts "Error: #{error}" },
  ->(value) { puts "Result: #{value}" }
)
# => Result: 5

The List monad treats arrays as contexts for non-deterministic computation. Binding a function over a list applies that function to each element and concatenates the results, naturally expressing computations that branch into multiple possibilities.

class List
  attr_reader :items
  
  def self.unit(value)
    List.new([value])
  end
  
  def initialize(items)
    @items = items
  end
  
  def bind(&block)
    result = @items.flat_map { |item| block.call(item).items }
    List.new(result)
  end
  
  def fmap(&block)
    List.new(@items.map(&block))
  end
end

# Non-deterministic computation
def pairs(n)
  List.new((1..n).to_a).bind { |x|
    List.new((1..n).to_a).bind { |y|
      List.unit([x, y])
    }
  }
end

# Pythagorean triples
def pythagorean_triples(n)
  List.new((1..n).to_a).bind { |x|
    List.new((x..n).to_a).bind { |y|
      List.new((y..n).to_a).bind { |z|
        x * x + y * y == z * z ?
          List.unit([x, y, z]) :
          List.new([])
      }
    }
  }
end

puts pythagorean_triples(15).items.inspect
# => [[3, 4, 5], [5, 12, 13], [6, 8, 10], [9, 12, 15]]

Ruby's Enumerable module demonstrates functor properties through map and monad properties through flat_map, though it doesn't enforce functor laws or provide the full monad interface. Custom implementations gain explicit control over behavior and can implement additional operations.

# Using Ruby's built-in monad-like operations
result = [1, 2, 3]
  .flat_map { |x| [x, x * 10] }
  .map { |x| x + 1 }
  .select { |x| x > 5 }

puts result.inspect
# => [6, 11, 21, 31]

The dry-monads gem provides production-ready monad implementations with additional features like result type conversion, do-notation through blocks, and integration with dry-validation. This library offers the most mature Ruby monad implementation for production applications.

require 'dry/monads'

class UserService
  include Dry::Monads[:result, :maybe]
  
  def find_user(id)
    user = database_lookup(id)
    user ? Success(user) : Failure(:user_not_found)
  end
  
  def validate_age(user)
    user[:age] >= 18 ?
      Success(user) :
      Failure(:user_too_young)
  end
  
  def process(id)
    find_user(id)
      .bind { |user| validate_age(user) }
      .fmap { |user| user[:name] }
  end
end

Common Patterns

The railway-oriented programming pattern uses Either monads to model computation tracks. Successful operations stay on the "success track" (Right), while failures switch to the "error track" (Left). Once on the error track, subsequent operations bypass until reaching explicit error handling.

class Result
  def self.success(value)
    Success.new(value)
  end
  
  def self.failure(error)
    Failure.new(error)
  end
  
  def self.from_exception
    begin
      Result.success(yield)
    rescue StandardError => e
      Result.failure(e.message)
    end
  end
end

class Success < Result
  attr_reader :value
  
  def initialize(value)
    @value = value
  end
  
  def bind(&block)
    block.call(@value)
  end
  
  def success?
    true
  end
end

class Failure < Result
  attr_reader :error
  
  def initialize(error)
    @error = error
  end
  
  def bind(&block)
    self
  end
  
  def success?
    false
  end
end

# Processing pipeline with error handling
def validate_email(email)
  email.include?('@') ?
    Result.success(email) :
    Result.failure("Invalid email format")
end

def check_domain(email)
  domain = email.split('@').last
  %w[gmail.com yahoo.com].include?(domain) ?
    Result.success(email) :
    Result.failure("Domain not allowed")
end

def create_user(email)
  Result.from_exception do
    # Simulated user creation
    {email: email, id: rand(1000)}
  end
end

result = validate_email("user@gmail.com")
  .bind { |e| check_domain(e) }
  .bind { |e| create_user(e) }

if result.success?
  puts "User created: #{result.value}"
else
  puts "Failed: #{result.error}"
end

The Maybe monad chain pattern sequences operations where each step might fail. Rather than checking for nil after each operation, the chain automatically short-circuits on Nothing and continues with Just values. This eliminates nested conditionals and makes the happy path explicit.

class User
  attr_reader :id, :profile
  
  def initialize(id, profile)
    @id = id
    @profile = profile
  end
end

class Profile
  attr_reader :settings
  
  def initialize(settings)
    @settings = settings
  end
end

def find_user_setting(user_id, setting_key)
  Maybe.from_nullable(fetch_user(user_id))
    .bind { |user| Maybe.from_nullable(user.profile) }
    .bind { |profile| Maybe.from_nullable(profile.settings) }
    .bind { |settings| Maybe.from_nullable(settings[setting_key]) }
    .value_or("default_value")
end

def fetch_user(id)
  return nil if id > 100
  User.new(id, Profile.new({theme: "dark", language: "en"}))
end

puts find_user_setting(50, :theme)
# => dark
puts find_user_setting(150, :theme)
# => default_value

The list monad pattern generates combinations or searches solution spaces. Each bind operation branches computation into multiple paths, with flat_map flattening the nested results into a single sequence. This pattern naturally expresses problems involving permutations, combinations, or constraint satisfaction.

# Generating combinations
def chess_positions
  files = ('a'..'h').to_a
  ranks = (1..8).to_a
  
  List.new(files).bind { |file|
    List.new(ranks).bind { |rank|
      List.unit("#{file}#{rank}")
    }
  }
end

# Finding solutions with constraints
def valid_queens(n)
  def safe?(positions, row, col)
    positions.each_with_index.all? { |c, r|
      c != col && (row - r).abs != (col - c).abs
    }
  end
  
  def place_queens(positions, row, n)
    return List.unit(positions) if row == n
    
    List.new((0...n).to_a).bind { |col|
      safe?(positions, row, col) ?
        place_queens(positions + [col], row + 1, n) :
        List.new([])
    }
  end
  
  place_queens([], 0, n)
end

solutions = valid_queens(4)
puts "Found #{solutions.items.length} solutions"
# => Found 2 solutions

The kleisli composition pattern combines monadic functions by composing their effects. Given functions a → M b and b → M c, kleisli composition produces a → M c, sequencing the monadic effects without manual chaining. This enables building complex operations from simple monadic functions.

module Kleisli
  def self.compose(f, g)
    ->(x) { f.call(x).bind(&g) }
  end
end

# Individual monadic operations
parse_int = ->(s) {
  Integer(s)
  Maybe.unit(Integer(s))
rescue ArgumentError
  Maybe.unit(nil)
}

double = ->(n) { Maybe.unit(n * 2) }
increment = ->(n) { Maybe.unit(n + 1) }

# Composed operation
process = Kleisli.compose(
  parse_int,
  Kleisli.compose(double, increment)
)

puts process.call("5").value_or(0)
# => 11
puts process.call("invalid").value_or(0)
# => 0

Practical Examples

A configuration loader demonstrates Maybe monad usage for handling missing or invalid configuration values. Each step in loading configuration might fail - file might not exist, JSON might be invalid, required keys might be missing. The Maybe monad chain makes the sequence explicit without cluttering code with nil checks.

require 'json'

class ConfigLoader
  def self.load(path, *keys)
    read_file(path)
      .bind { |content| parse_json(content) }
      .bind { |config| extract_keys(config, keys) }
  end
  
  def self.read_file(path)
    Maybe.unit(File.read(path))
  rescue Errno::ENOENT, IOError
    Maybe.unit(nil)
  end
  
  def self.parse_json(content)
    Maybe.unit(JSON.parse(content))
  rescue JSON::ParserError
    Maybe.unit(nil)
  end
  
  def self.extract_keys(config, keys)
    result = keys.reduce(config) { |obj, key|
      return nil unless obj.is_a?(Hash) && obj.key?(key.to_s)
      obj[key.to_s]
    }
    Maybe.unit(result)
  end
end

# Usage
database_url = ConfigLoader
  .load("config/database.yml", "production", "url")
  .value_or("sqlite://memory")

api_key = ConfigLoader
  .load("config/secrets.yml", "api", "key")
  .value_or(ENV["API_KEY"])

A validation pipeline shows Either monad application for accumulating validation errors while processing input. Unlike Maybe which loses failure information, Either captures why validation failed and can chain multiple validators that each might produce different errors.

class Validator
  def self.validate_presence(value, field)
    value.nil? || value.empty? ?
      Either.left("#{field} is required") :
      Either.right(value)
  end
  
  def self.validate_length(value, field, min:, max:)
    length = value.length
    if length < min
      Either.left("#{field} must be at least #{min} characters")
    elsif length > max
      Either.left("#{field} must be at most #{max} characters")
    else
      Either.right(value)
    end
  end
  
  def self.validate_format(value, field, pattern:)
    pattern.match?(value) ?
      Either.right(value) :
      Either.left("#{field} has invalid format")
  end
end

def validate_username(input)
  Validator.validate_presence(input, "Username")
    .bind { |v| Validator.validate_length(v, "Username", min: 3, max: 20) }
    .bind { |v| Validator.validate_format(v, "Username", pattern: /\A[a-zA-Z0-9_]+\z/) }
end

def validate_email(input)
  Validator.validate_presence(input, "Email")
    .bind { |v| Validator.validate_format(v, "Email", pattern: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i) }
end

# Process registration
username_result = validate_username("ab")
email_result = validate_email("invalid")

username_result.either(
  ->(err) { puts "Username error: #{err}" },
  ->(val) { puts "Valid username: #{val}" }
)
# => Username error: Username must be at least 3 characters

email_result.either(
  ->(err) { puts "Email error: #{err}" },
  ->(val) { puts "Valid email: #{val}" }
)
# => Email error: Email has invalid format

A data transformation pipeline uses functor mapping to transform nested data structures while preserving structure. Each transformation step maps over the structure without manually unwrapping and rewrapping values, keeping transformation logic separate from structure traversal.

class Tree
  attr_reader :value, :left, :right
  
  def initialize(value, left: nil, right: nil)
    @value = value
    @left = left
    @right = right
  end
  
  def fmap(&block)
    Tree.new(
      block.call(@value),
      left: @left&.fmap(&block),
      right: @right&.fmap(&block)
    )
  end
  
  def to_a
    result = []
    result.concat(@left.to_a) if @left
    result << @value
    result.concat(@right.to_a) if @right
    result
  end
end

# Build tree
tree = Tree.new(
  5,
  left: Tree.new(3, left: Tree.new(1), right: Tree.new(4)),
  right: Tree.new(8, left: Tree.new(6), right: Tree.new(9))
)

# Transform all values
doubled = tree.fmap { |x| x * 2 }
incremented = tree.fmap { |x| x + 1 }
stringified = tree.fmap { |x| "Value: #{x}" }

puts doubled.to_a.inspect
# => [2, 6, 8, 10, 12, 16, 18]
puts incremented.to_a.inspect
# => [2, 4, 5, 6, 7, 9, 10]

An async computation example shows how monads model computations with different effects. While Ruby lacks first-class async monads like Haskell's IO monad, the pattern applies to modeling computations that produce values in the future, enabling composition of async operations.

class Async
  def initialize(&block)
    @block = block
  end
  
  def bind(&fn)
    Async.new do
      result = @block.call
      fn.call(result).run
    end
  end
  
  def fmap(&fn)
    bind { |x| Async.unit(fn.call(x)) }
  end
  
  def self.unit(value)
    Async.new { value }
  end
  
  def run
    @block.call
  end
end

# Simulated async operations
def fetch_user(id)
  Async.new do
    sleep(0.1) # Simulate network delay
    {id: id, name: "User#{id}", posts: [1, 2, 3]}
  end
end

def fetch_post(post_id)
  Async.new do
    sleep(0.1)
    {id: post_id, title: "Post #{post_id}", author_id: 1}
  end
end

def enrich_user(user_id)
  fetch_user(user_id)
    .bind { |user|
      fetch_posts = user[:posts].map { |pid| fetch_post(pid) }
      Async.unit(user.merge(post_details: fetch_posts.map(&:run)))
    }
end

result = enrich_user(1).run
puts "User: #{result[:name]}, Posts: #{result[:post_details].length}"

Design Considerations

Monads suit problems where computations produce values within a context that needs consistent handling across multiple steps. Optional values, error conditions, asynchronous operations, state threading, and non-deterministic computation all involve contexts that monads model effectively. The bind operation handles context propagation automatically, letting code focus on transformation logic.

The decision to use monads depends on computation structure. Linear pipelines where each step depends on the previous step's success benefit from monadic composition. Code with multiple independent computations combined at the end fits applicative functors better. Simple data transformations without context need only functor map operations.

Ruby's dynamic typing reduces monad benefits compared to statically typed languages. Type systems in languages like Haskell prevent mixing wrapped and unwrapped values at compile time, catching errors early. Ruby permits calling any method on any object, so wrapping values in monads trades compile-time safety for runtime checking and explicit interfaces.

The verbosity of manual monad implementation in Ruby discourages adoption. Languages with do-notation or comprehension syntax make monadic code read almost like imperative code. Ruby lacks these features, requiring explicit bind calls that can obscure logic. For simple Maybe chains, direct nil checking might be clearer.

# Monadic style - explicit but verbose
def find_nested_value(hash, *keys)
  keys.reduce(Maybe.unit(hash)) { |maybe_obj, key|
    maybe_obj.bind { |obj|
      Maybe.from_nullable(obj.is_a?(Hash) ? obj[key] : nil)
    }
  }.value_or(nil)
end

# Imperative style - direct but requires nil checks
def find_nested_value_imperative(hash, *keys)
  keys.reduce(hash) { |obj, key|
    return nil unless obj.is_a?(Hash)
    obj[key]
  }
end

Library selection impacts monad usability. The dry-monads gem provides mature implementations with syntactic sugar through do-notation blocks and pattern matching integration. Custom implementations offer full control but require more code and testing. Ruby's built-in Enumerable provides functor and monad operations for collections without additional dependencies.

Monads compose sequentially but not horizontally. Combining independent monadic computations requires lifting operations to applicative level or manually extracting values. Monad transformers stack different monadic effects but add complexity. For applications needing multiple effect types simultaneously, alternative patterns like effect systems might fit better.

Performance considerations matter for hot paths. Monadic abstraction introduces object allocation and method call overhead. A tight loop processing millions of items might run faster with direct nil checks than Maybe monad chains. Profile before optimizing, as clarity often matters more than marginal performance gains.

Team familiarity influences adoption success. Developers experienced with functional programming adopt monads easily, while those from imperative backgrounds find the abstraction unfamiliar. Introduce monads gradually in code reviews and documentation rather than converting entire codebases. Focus on pain points like error handling or optional value chains where monads provide clear wins.

Reference

Functor Operations

Operation Signature Description Laws
fmap (a -> b) -> F a -> F b Transform wrapped value Identity, Composition
map (a -> b) -> F a -> F b Alias for fmap Identity, Composition

Monad Operations

Operation Signature Description Laws
unit/return/pure a -> M a Wrap value in monad Left identity, Right identity
bind/flatMap M a -> (a -> M b) -> M b Chain monadic operations Left identity, Right identity, Associativity
join M (M a) -> M a Flatten nested monads Corresponds to bind

Common Monad Types

Monad Purpose Success Case Failure Case
Maybe Optional values Just(value) Nothing
Either Error handling Right(value) Left(error)
List Non-determinism Non-empty list Empty list
IO Side effects Deferred computation Not applicable
State State threading Stateful computation Not applicable
Reader Environment access Value with context Not applicable
Writer Logging/output Value with log Not applicable

Monad Laws

Law Formula Description
Left Identity unit(a).bind(f) = f(a) Wrapping then binding equals direct application
Right Identity m.bind(unit) = m Binding with unit returns original monad
Associativity m.bind(f).bind(g) = m.bind(x -> f(x).bind(g)) Binding order doesn't matter

Functor Laws

Law Formula Description
Identity fmap(id) = id Mapping identity returns original
Composition fmap(f . g) = fmap(f) . fmap(g) Composed map equals sequential maps

Ruby Implementation Patterns

Pattern Code Structure Use Case
Maybe Chain Maybe.unit(x).bind{}.bind{}.value_or() Sequential operations with potential failure
Either Pipeline Either.right(x).bind{}.bind{}.either() Error handling with failure reasons
List Comprehension List.new([]).bind{}.bind{}.items Non-deterministic computation
Railway Pattern Success(x).bind{}.bind{}.success? Success/failure track routing
Kleisli Composition Kleisli.compose(f, g) Combining monadic functions

Comparison with Imperative Patterns

Functional Approach Imperative Equivalent Trade-off
Maybe.unit(x).bind(f) x.nil? ? nil : f(x) Abstraction vs directness
Either.right(x).bind(f) begin; f(x); rescue; end Explicit vs exception-based
list.fmap(f) list.map{f} Generic vs built-in
m.bind(f).bind(g) x = f(m); g(x) Composition vs sequential

Common Gotchas

Issue Description Solution
Nested Monads bind returns monad, creating M(M a) Use bind not fmap
Wrong Abstraction Using monad when functor suffices Check if flattening needed
Performance Overhead Object allocation in tight loops Profile and optimize hot paths
Type Confusion Mixing wrapped and unwrapped values Consistent monad usage
Imperative Mixing Breaking chain with non-monadic code Complete transformation in chain

Monad Transformer Stack

Transformer Base Monad Result Type Use Case
MaybeT Any monad M M (Maybe a) Optional values in context
EitherT Any monad M M (Either e a) Errors in context
StateT Any monad M s -> M (a, s) State with other effects
ReaderT Any monad M e -> M a Environment with effects

Ruby Gems

Gem Focus Key Features Maturity
dry-monads General monads Result, Maybe, Try, List, Task Production-ready
deterministic Railway programming Success/Failure, pattern matching Stable
kleisli Either monad Right/Left with sugar Maintained
monadic Pure monads Academic implementation Experimental