Monadt – Algebraic Data Types and Monads in Ruby, Part 1: ADTs

Functional programming is elegant and expressive. I’ve written before about my love of partial application, and how the funkify gem can be used to bring the power of partial application to your Ruby code. But partial application is just one of the powerful idioms from functional languages that I’d like to borrow in object-oriented languages. I’m also pretty into algebraic data types and monads.

So, continuing my pattern of adding functional concepts to object-oriented languages whether they like it or not, I recently created the monadt gem which adds support for using algebraic data types and monads to Ruby. This library gives you these powerful features while attempting to be as readable and Ruby-like as possible.

In this post, I’m going to explain how the monadt library provides algebraic data types. I will cover its use of monads in a future post.

Algebraic Data Types

An algebraic data type (or discriminated union), is like a C union which also maintains which value of the union should be used. Alternatively, you can think of ADTs like enums with data values attached to the different enum values. So for example, if I wanted to create an ADT that captures the state of a car’s gear and velocity, it might look like this:


type GearSpeed =
  | Park
  | LowGear of int * double
  | Drive of double

where Park has no additional data (we are in the parked gear and not moving), LowGear tracks both the forced low gear and our current velocity, and Drive indicates that we are in the regular drive gear at a certain speed. Some possible values:


Park              // parked
Drive 45.0        // drive, 45 mph
LowGear (1, 10.0) // first gear, 10 mph
Drive 55.2        // drive, 55.2 mph
LowGear (2, 20.0) // second gear, 20 mph

What’s so powerful about ADTs is that you can use them to expressively define all possible states. I cannot create a GearSpeed without specifying one of the three cases, and I cannot specify one of the cases without providing its required values. Additionally, languages with ADTs define powerful pattern matching semantics to quickly identify which case you are in and extract the values.


match aGearSpeed with
| Park -> "stopped"
| LowGear (gear, speed) -> sprintf "%d gear, %f mph" gear speed
| Drive speed -> sprintf "drive, %f mph" speed

ADTs also allow you to specify default cases, and they check at compile time to make sure you have covered all the cases when you branch.


let isStopped aGearSpeed =
  match aGearSpeed with
  | Park -> true
  | _ -> false

// will not compile, missing Park case
let getSpeed aGearSpeed =
  match aGearSpeed with
  | LowGear (_, speed) -> speed
  | Drive speed -> speed

monadt

So where does Ruby come in? As a dynamically typed language, we can’t formally restrict the types associated with data, and we can’t guarantee all cases have been covered at compile time. However, we can define ADT such that pattern matching will indicate which case we are and easily extract the correct values.

The monadt library (get it?) builds on the code from this blog post by fellow Atom Job Vranish, but it uses functional/functor map style pattern matching, instead of taking a lambda intended to have side effects.

Here’s an example:


class GearSpeed
  Park = data
  LowGear = data :gear, :speed
  Drive = data :speed
end

This code declares an ADT GearSpeed with the same meaning as the original definition in F#. It has three different values: Park, LowGear, and Drive. Park has no associated data, LowGear has two pieces of associated data (“gear” and “speed”), and Drive has one (“speed”). We can’t restrict the types of those values, but we can name them for future refernce.

You can create new instances with the standard Ruby constructor for the value type:


GearSpeed::LowGear.new 3, 21.0  # gear = 3, speed = 21.0 mph

Given an ADT value, we can do pattern matching using monadt’s match() and with() functions:


match val,
  with(GearSpeed::Park) { "stopped" },
  with(GearSpeed::LowGear) { |gear, speed| "#{gear} gear, #{speed} mph" },
  with(GearSpeed::Drive) { |speed| "drive, #{speed} mph" }

The above code takes an ADT value and pattern matches by type, taking a lambda which will yield the values associated with the particular ADT case. You can also do default matching, using the “Default” type in monadt instead of the _ wildcard (as you would in F# or Haskell):


match val,
  with(GearSpeed::Park) { true },
  with(Default) { false }

Decorating ADTs

But wait, there’s more! To handle other common usages, and to shorten the length of code to create ADT values, monadt includes a method decorate_adt().


decorate_adt GearSpeed

Calling this method on an ADT type adds the following:

  1. constructor functions based on the lowercase ADT value name, attached to the parent type:
    
    GearSpeed.drive 50.0 # same as GearSpeed::Drive.new 50.0
    GearSpeed.park # same as GearSpeed::Park.new
    GearSpeed.low_gear 2, 10.5 # same as GearSpeed::LowGear.new 2, 10.5
    
  2. is_###? functions for each case (same lowercase naming as the constructor):
    
    gearspeed_value.is_park?
    gearspeed_value.is_drive?
    gearspeed_value.is_low_gear?
    
  3. sensible default to_s()
    
    gearspeed_value.to_s # "Drive(36.0)", "Park", or "LowGear(2, 12.5)"
    

Moving on to Monads

That covers the basics of using ADTs with monadt. In my next post, I’ll talk about how monadt uses Ruby Enumerators to simulate the syntactic sugar associated with monads in Haskell or F#, particularly focusing on Maybe and Either monads—two types that can be implemented as ADTs.

Conversation
  • Tom Adams says:

    Very cool, and succinct. Have you seen this? https://github.com/nkpart/adt

  • Comments are closed.