You got Haskell in my Ruby! Cleaner Ruby validations using the Either monad and Kleisli gem

11 minute read

You got Haskell in my Ruby!

Alternate title: “You could have invented Either!”

Update: It came to my attention from some Reddit comments that the simple example I use in this article is probably not the best for showing off a good use case of the kleisli gem as the “naive” code can be simplified. Therefore, I will be updating this post in the future with a more thorough example. In the meantime, I’d encourage you to read the article “Cleaner, safer Ruby API clients with Kleisli” by the kleisli gem author which has a more comprehensive use case!

I’m still a rank beginner at Haskell, but I guess it’s already leaving some tracks in my brain as I find myself wanting algebraic data types and pattern matching when I’m writing Ruby.

Recently this became even more apparent when I had to perform a laundry list of validations against an image as part of a feature where users can upload custom avatars of themselves. Before I save the user-uploaded image to the database and do post-processing on it, I have to ensure that it meets some requirements:

  • verify the file size is < 1MB
  • verify the image is a valid image format
  • verify the image is of type PNG, JPEG, or GIF
  • verify the image dimensions are within 5,000x5,000 pixels

Also, I need a JSON-friendly version of the validation return value as the website is uploading the image via Ajax.

Version 1: naive validation object

Let’s write an object to do the validation as naively as possible. To fit into my Rails project neatly, I’m going to pass it the user-uploaded file directly (which is an ActionDispatch::Http::UploadedFile).

Here’s a naive version (note I’m using mini_magick but eliding the require as this is a Rails project):

class AvatarValidator
  MAX_SIZE = 1.megabyte
  ALLOWED_FORMATS = %w(PNG JPEG GIF)
  MAX_WIDTH, MAX_HEIGHT = [5000, 5000]

  def initialize(uploaded_file)
    @uploaded_file = uploaded_file
  end

  def validate
    @valid ||= begin
      if @uploaded_file.size > MAX_SIZE
        "Picture size must be no larger than #{MAX_SIZE} bytes"
      else
        image = MiniMagick::Image.open(@uploaded_file.tempfile.path)
        if image.valid?
          if ALLOWED_FORMATS.include?(image.type)
            if image.width > MAX_WIDTH or image.height > MAX_HEIGHT
              "Picture dimensions must be within #{MAX_WIDTH}x#{MAX_HEIGHT}"
            else
              true
            end
          else
            "Picture must be an image of type #{ALLOWED_FORMATS.join(' or ')}"
          end
        else
          "Picture must be a valid image format"
        end
      end
    rescue StandardError
      "Sorry, an internal error occurred. Try again or contact us."
    end
  end

  def validate_as_json
    if validate == true
      {success: true}
    else
      {success: false, message: validate}
    end
  end
end

The AvatarValidator is initialized with the uploaded file. The validate method will either return true if the file is valid according to our specifications or a String indicating the error message if not. The validate_as_json wraps the validate method, returning a Hash that I can use in a controller for an Ajax-friendly .json format.

There are a couple things I don’t like with this approach. The first being the return values: two primitive types are being returned with no context about what they represent. If the values stay pretty close to where they are generated, this is usually okay. However, when projects grow large, these kinds of values have a habit of ending up far away from where they are generated, like some kind of cancerous metastasis, causing confusion as to where they originate from when they cause bugs.

You especially see the problem happen with nils, because they can inhabit any type in Ruby and are used pretty often to represent invalid or unexpected values. So they can happily get passed between long chains of methods until one of the methods eventually tries to do something with it and blows up, far from where the nil actually originated from, leaving you to have to dig through a stacktrace and figure out where it came from.

So the first thing I’m inclined to do is to create a type to add some context to what these return values represent. I’m thinking some kind of Success class to represent a successful validation, and an Error class to represent a failed validation. Additionally, the Error class should also store some kind of message so that I know what went wrong during the validation.

Let’s give it a try:

Version 2: make return values first-class objects, wrapping a context

class AvatarValidator
  MAX_SIZE = 1.megabyte
  ALLOWED_FORMATS = %w(PNG JPEG GIF)
  MAX_WIDTH, MAX_HEIGHT = [5000, 5000]

  class Success; end
  class Error
    attr_reader :message
    def initialize(message)
      @message = message
    end
  end

  def initialize(uploaded_file)
    @uploaded_file = uploaded_file
  end

  def validate
    @valid ||= begin
      if @uploaded_file.size > MAX_SIZE
        Error.new("Picture size must be no larger than #{MAX_SIZE} bytes")
      else
        image = MiniMagick::Image.open(@uploaded_file.tempfile.path)
        if image.valid?
          if ALLOWED_FORMATS.include?(image.type)
            if image.width > MAX_WIDTH or image.height > MAX_HEIGHT
              Error.new("Picture dimensions must be within #{MAX_WIDTH}x#{MAX_HEIGHT}")
            else
              Success.new
            end
          else
            Error.new("Picture must be an image of type #{ALLOWED_FORMATS.join(' or ')}")
          end
        else
          Error.new("Picture must be a valid image format")
        end
      end
    rescue StandardError
      Error.new("Sorry, an internal error occurred. Try again or contact us.")
    end
  end

  def validate_as_json
    case validate
    when AvatarValidator::Success
      {success: true}
    when AvatarValidator::Error
      {success: false, message: validate.message}
    end
  end
end

Now our return values can either be an AvatarValidator::Success instead of a bare true, or an AvatarValidator::Error with a message which contains the error instead of just a bare String. The code is a little longer than before, but I feel like it’s safer now.

The other thing that is bothering me about this approach is the use of nested conditionals for each step of the validation. This approach is pretty much at its limit, as the successful validation is currently nested within conditional branches four layers deep! If I needed to add more validations, like ensuring the the image is of square proportions or doesn’t have any animation frames, I’d have to add even more nested layers.

One solution would be to convert the nested conditionals into a bunch of individual conditionals with early returns, and Success being at the very bottom, like so:

def validate
  @valid ||= begin
    if @uploaded_file.size > MAX_SIZE
      return Error.new("Picture size must be no larger than #{MAX_SIZE} bytes")
    end
    unless ALLOWED_FORMATS.include?(image.type)
      return Error.new("Picture must be an image of type #{ALLOWED_FORMATS.join(' or ')}")
    end
    # ...
    Success.new
  end
end

However that does not look like very idiomatic Ruby, and there is danger in forgetting to use return in each conditional, or forgetting to put the Success as the last value in the method (which would return one of those pesky nils if we ended on a conditional branch not-taken).

Luckily, there is a concept we can borrow from functional programming that will both clean up this conditional branching mess as well as generalize the concept that we invented about propagating the success/error context of our validation: it’s called the Either monad.

The general concept of the Either monad is that you wrap success values in a Right, and failures in a Left. The mnemonic to remember which is which is to remember that “right” = “correct.” If you’re aware of the connotation of “left” being “sinister” or “evil”, you could also remember that. (Fun fact: I was naturally left-handed as a small child until my grandfather would slap my hand every time I would try to use it dominantly. Thanks for beating the evil out of me Grandpa! :))

But in short, we will be replacing our use of AvatarValidator::Success with Right and AvatarValidator::Error with Left.1

I’m going to use the Kleisli gem to write this version of the validator. This gem helps clean up our conditional validation mess by introducing a >-> operator, which lets us do a sort of pattern-matching on these two possible Either values. If given a Right value, >-> will unwrap the value inside the Right and evaluate a block with it. If given a Left, it will simply ignore/skip the block. In this way, >-> lets us build a sort of short-circuiting pipeline for doing validation, which will either immediately return a Left with an error message if any step in the pipeline returns a Left, or a Right only if every step in the pipeline returns a Right.

Let’s try it out!

Version 3: Either monad using the Kleisli gem

require 'kleisli'

class AvatarValidator
  MAX_SIZE = 1.megabyte
  ALLOWED_FORMATS = %w(PNG JPEG GIF)
  MAX_WIDTH, MAX_HEIGHT = [5000, 5000]

  def initialize(uploaded_file)
    @uploaded_file = uploaded_file
  end

  def validate
    @valid ||= begin
      Right(@uploaded_file) >-> value {
        if value.size > MAX_SIZE
          Left("Picture size must be no larger than #{MAX_SIZE} bytes")
        else
          Right(value)
        end
      } >-> value {
        image = MiniMagick::Image.open(value.tempfile.path)
        if image.valid?
          Right(image)
        else
          Left("Picture must be a valid image format")
        end
      } >-> value {
        if ALLOWED_FORMATS.include?(value.type)
          Right(value)
        else
          Left("Picture must be an image of type #{ALLOWED_FORMATS.join(' or ')}")
        end
      } >-> value {
        if value.width > MAX_WIDTH or value.height > MAX_HEIGHT
          Left("Picture dimensions must be within #{MAX_WIDTH}x#{MAX_HEIGHT}")
        else
          Right(value)
        end
      }
    rescue StandardError
      Left("Sorry, an internal error occurred. Try again or contact us.")
    end
  end

  def validate_as_json
    case validate
    when Kleisli::Either::Right
      {success: true}
    when Kleisli::Either::Left
      {success: false, message: validate.value}
    end
  end
end

I hope you find this as readable as I do! I find it really easy to see at a glance what the different steps in the pipeline are doing, and the chaining using the >-> operator helps to prevent fat-finger mistakes.

I have to give props to the Kleisli gem for striking a nice balance between providing useful functional programming tools while still keeping the syntax Rubyish. Also, after looking through similar gems, I find it comforting that it “aims to be idiomatic Ruby to use in Enter-Prise production apps, not a proof of concept.” Some other similar gems seem to either have unwieldy syntax or are more toy academic exercises (which is fine, but I don’t want to use them for Serious Business™).

Bonus version: type-annotating methods with contracts.ruby

To add even more safety to this object, there is an interesting gem out there called contracts.ruby which basically lets you add runtime type checking to the boundaries of your methods. This can’t give us the purely functional static type checking that languages like Haskell have, but it is a nice tool for catching erroneous inputs and outputs early on, preventing bad values from propagating between method calls. It’s really great for catching dumb mistakes like returning a nil from a method that shouldn’t be, and the DSL is pretty expressive (as you’ll see, I can even describe the internal structure of a Hash that should be returned).

Here’s a version with contracts.ruby type-checking added to the methods:

require 'kleisli'
require 'contracts'

class AvatarValidator
  include Contracts
  MAX_SIZE = 1.megabyte
  ALLOWED_FORMATS = %w(PNG JPEG GIF)
  MAX_WIDTH, MAX_HEIGHT = [5000, 5000]

  Contract ActionDispatch::Http::UploadedFile => Any
  def initialize(uploaded_file)
    @uploaded_file = uploaded_file
  end

  Contract None => Kleisli::Either
  def validate
    @valid ||= begin
      Right(@uploaded_file) >-> value {
        if value.size > MAX_SIZE
          Left("Picture size must be no larger than #{MAX_SIZE} bytes")
        else
          Right(value)
        end
      } >-> value {
        image = MiniMagick::Image.open(value.tempfile.path)
        if image.valid?
          Right(image)
        else
          Left("Picture must be a valid image format")
        end
      } >-> value {
        if ALLOWED_FORMATS.include?(value.type)
          Right(value)
        else
          Left("Picture must be an image of type #{ALLOWED_FORMATS.join(' or ')}")
        end
      } >-> value {
        if value.width > MAX_WIDTH or value.height > MAX_HEIGHT
          Left("Picture dimensions must be within #{MAX_WIDTH}x#{MAX_HEIGHT}")
        else
          Right(value)
        end
      }
    rescue StandardError
      Left("Sorry, an internal error occurred. Try again or contact us.")
    end
  end

  Contract None => Or[{ :success => Bool}, { :success => Bool, :message => String }]
  def validate_as_json
    case validate
    when Kleisli::Either::Right
      {success: true}
    when Kleisli::Either::Left
      {success: false, message: validate.value}
    end
  end
end

Now if we pass an unexpected value to any of these methods (or try to return an unexpected value from them), we will get an immediate runtime type error:

>> AvatarValidator.new('/tmp/path/to/some/file')
ContractError: Contract violation for argument 1 of 1:
        Expected: ActionDispatch::Http::UploadedFile,
        Actual: "/tmp/path/to/some/file"
        Value guarded in: AvatarValidator::initialize
        With Contract: ActionDispatch::Http::UploadedFile => Any
        At: /home/abe/code/gun_crawler_web/app/models/avatar_validator.rb:12

Which is a big improvement over the bizarre errors you’d get when you try to use a value of the wrong type as if it were a different type.

If you enjoyed this, check out Haskell!

This whole example really uses only the most basic concepts in Haskell. If any of this has piqued your interest, I highly recommend learning some Haskell! The best resource for that in my opinion is Chris Allen (@bitemyapp)’s guide (it’s the one I find the best as a beginner myself, anyway).

Haskallywags logo

And if you’re in the Madison, WI area, come check out the Haskell meetup hosted by Bendyworks! We’ve just started working through the exercises referenced in Chris Allen’s guide, so it’s a great time to come if you’re a beginner like me.

References

Footnotes

1 Note that unlike our AvatarValidator::Success, a Right actually wraps a value. This is required so that we can build the pipeline that unwraps the value and passes it to the next block.

Technically what we had actually built was sort of an upside-down Maybe monad around error values, with a None (AvatarValidator::Success) indicating success (no error), and a Just m (AvatarValidator::Error message) indicating the error + error message.

Updated: