Validated

Suppose you've got a form to validate. If you are using Result combined with Do your code might look like this:

require 'dry/monads'

class CreateAccount
  include Dry::Monads[:result, :do]

  def call(form)
    name = yield validate_name(form)
    email = yield validate_email(form)
    password = yield validate_password(form)

    user = repo.create_user(
      name: name,
      email: email,
      password: password
    )

    Success(user)
  end

  def validate_name(form)
    # Success(name) or Failure(:invalid_name)
  end

  def validate_email(form)
    # Success(email) or Failure(:invalid_email)
  end

  def validate_password(form)
    # Success(password) or Failure(:invalid_password)
  end
end

If any of the validation steps fails the user will see an error. The problem is if name is not valid the user won't see errors about invalid email and password, if any. Validated circumvents this particular problem.

Validated is actually not a monad but an applicative functor. This means you can't call bind on it. Instead, it can accumulate values in combination with List:

require 'dry/monads'

class CreateAccount
  include Dry::Monads[:list, :result, :validated, :do]

  def call(form)
    name, email, password = yield List::Validated[
      validate_name(form),
      validate_email(form),
      validate_password(form)
    ].traverse.to_result

    user = repo.create_user(
      name: name,
      email: email,
      password: password
    )

    Success(user)
  end

  def validate_name(form)
    # Valid(name) or Invalid(:invalid_name)
  end

  def validate_email(form)
    # Valid(email) or Invalid(:invalid_email)
  end

  def validate_password(form)
    # Valid(password) or Invalid(:invalid_password)
  end
end

Here all validations will be processed at once, if any of them fails the result will be converted to a Failure wrapping the List of errors:

create_account.(form)
# => Failure(List[:invalid_name, :invalid_email])

octocatEdit on GitHub