If Ruby Had...

A small wishlist

I went through LYAH, and it gave me a great appreciation for the programming language and functional programming style in general. Two things I really like are partial function application and pattern matching. If Ruby had these features, how could they be used in a Rails app?

Beware!

I’ve been learning Rails for about a month now. I’m sure my ‘before’ code could be improved markedly.

Pattern matching:

My Rails app has a lot of related data objects, and I find that I’m adding new relations fairly regularly. Artists have many Releases through Contributions, Releases have many ReleaseDates, Users follow many Artists, etc. As such, I’ve got methods like will eventually look like this:

class User
  def follow_artist(artist)
    case artist.class
      when String
        artist = Artist.find_by(name: release)
      when Artist
        # everything OK
      else
        raise ArgumentError.new "Unsupported type"
    end
    
    self.follows.create(artist: artist)
  end
end

class Artist
  def add_release(release)
    case release.class
      when String
        release = Release.find_by(name: release)
      when Release
        # everything OK
      else
        raise ArgumentError.new "Unsupported type"
    end
    
    self.contributions.create(release: release)
  end
end

Pattern matching would mostly be nice as a way to make the code more concise. If Ruby had it, it might look like:

class User
  def follow_artist(artist)
    pattern_match artist.class,
       String   => ->{ artist = Artist.find_by(name: artist) },
       Artist   => ->{ artist },
       otherwise:  ->{ raise ArgumentError.new "Unsupported type" }

    self.follows.create(artist: artist)
  end
end

The pattern_match function would take an expression and a hash of results paired with lambdas and execute the expression corresponding with the result. The return value of the pattern_match function is the return of the lambda that gets executed.

The above code, in my opinion, looks quite a bit cleaner, and allows a bit more modularity than the former. It can be refactored like so:

class User
  def follow_artist(artist)
    self.follows.create( 
      pattern_match(artist.class,
        String  => ->{ { artist: Artist.find_by(name: artist) } },
        Artist  => ->{ { artist: artist } },
        otherwise: ->{ raise ArgumentError }
      )
    )
  end
end

Partial function application:

Mathematicians have determined that any function with multiple arguments can be expressed as a series of functions that take a single argument, return a function that takes a single argument, etc. until all arguments have been used and then returns the final result. In Haskell, this means that you can define a function: func x y z = x + y + z that takes three arguments and sums them. You can further define a function func' = func 1. func' is a function that takes two arguments, sums them, and adds 1. The func function has been partially applied. func'' = func' 2 partially applies func' with the argument 2, which means that func'' is now a function that takes a single argument and adds 3 to it. The following code snippet illustrates what is happening:

That’s all fine, but it seems really abstract and kind of weird and confusing. Why would you want to do that?

Going back to my add_X method above, even with the pattern_match function defined, there is a lot of code repetition between models. They’re all essentially doing the same thing: Receiving an object, pattern matching the object, and responding to the type of object. The specifics are different, but could it be abstracted out? With partial function application, it would be fairly easy. The method body would look something like:

class ActiveRecord
  def add_relation(base, relations, matching_function)
    base.relations.create(matching_function) 
    # where matching_function calls pattern_match(expression, lambdas)
  end
end

In languages with partially applied functions, the parameters that aren’t likely to change much are assigned first, and the parameters that change frequently are listed later. Each class would want to partially apply the method starting with the base class, then specify the relations, and then specify the matching function.

class Release
  # Creates the add_relation function for a Release object. Partially applied!
  def add_release_relation
    super.add_relation(self)
  end

  # Further supplies the contribution relation to the function, which now
  # expects the pattern matching function before it returns a result.
  def add_contribution_relation
    self.add_release_relation(self.contributions)
  end

  # Finally, a concrete function! This supplies the matching function to the 
  # above relation. Of course, we have to define the artist lambdas now.
  def add_artist(artist)
    self.add_contribution_relation(pattern_match artist, artist_matching)
  end

  # The return value of the lambdas should be a parameters hash for creating
  # the relation.
  def artist_matching
    { Artist  => ->{ { artist: artist } }
    , String  => ->{ { artist: Artist.find_by(name: artist) } }
    , otherwise: ->{ raise ArgumentError.new }
    }
  end

  ## And again for release_dates!
  def add_release_date_relation
    self.add_release_relation(self.release_dates)
  end

  def add_release_date(release_date)
    self.add_release_date_relation(pattern_match release_date, release_date_matching)
  end

  def release_date_matching
    { ReleaseDate => ->{ { release_date: release_date } }
    , Date => -> { { release_date: ReleaseDate.new(date: release_date) } }
    , otherwise: ->{ raise ArgumentError.new }
    }
  end
end

With this sort of setup, every class in my application is reusing the same basic code for the creation of releases. All they’re doing is customizing the methods to be more and more specific, until it eventually does what’s wanted. Since everything is so broken up, a rather thorough testing of the base methods will practically ensure that the methods that build upon it have little to go wrong. Naturally, this sort of thing is much more powerful in a language with a powerful type system and restricted side-effects, but it’s not strictly theoretical.

(Yes, you could just explicitly pass all parameters to that initial method, but that’s not as fun!)