Skip to content

Applicative Validation in Ruby

ms-ati edited this page May 10, 2012 · 26 revisions

Applicative Validation in Ruby

What to Stop Doing

Don't return nil when validation fails, and try to handle it later:

def validate(txt)
  if txt == "bar" then txt else nil end
end

# Oops!
validate("foo") + validate("bar") + validate("baz") 
# NoMethodError: undefined method `+' for nil:NilClass

Don't raise an exception, short-circuiting other validations:

def validate(txt)
  if txt == "bar" then txt else raise "Invalid: #{txt}" end
end

# Only gives FIRST error:
validate("foo") + validate("bar") + validate("baz")
# RuntimeError: Invalid: foo

Don't effect external state to record errors:

def errors
  @errors ||= []
end

def validate(txt)
  if txt == "bar" then txt else errors << "Invalid: #{txt}"; "" end
end

# More complicated to test, parallelize, or just reason about
validate("foo") + validate("bar") + validate("baz")
# => "bar"
errors
# => ["Invalid: foo", "Invalid: baz"]
validate("foo") + validate("bar") + validate("baz")
# => "bar"
errors
# => ["Invalid: foo", "Invalid: baz", "Invalid: foo", "Invalid: baz"]

What to Start Doing

Do return a Disjoint Union type such as Either (docs).

The result will be either a success Right containing the validated data, or a failure Left value containing information about the failure.

def validate(txt)
  if txt == "bar" then Right(txt) else Left("Invalid: #{txt}") end
end

validate("bar")
# => Right("bar")
validate("foo")
# => Left("Invalid: foo")
validate("foo").lift_to_a + validate("bar").lift_to_a + validate("baz").lift_to_a
# => Left(["Invalid: foo", "Invalid: baz"])

Explanation

Using applicative validation patterns, you can combine the results of your validations easily and with clarity. In the case of failure you can process all generated errors, and easily combine and transform the result in the case of success.

Examples:

Resources

TODO: Add links to more information here.