Annotating immutable hashes with methods

marick edited this page Jan 15, 2012 · 5 revisions

As I describe in the video, I want to import the FP style of relying more heavily on basic types (like Hash) than on custom classes. However, I also want to attach functions (or methods) to such objects so that I can use method chaining. That is, I like to write something like this:

	reservation = ...
	new_reservation = 
	    reservation.
	    without_duplicate_animals.
	    with_classification(DUPLICATE).
	    as_saved_to_disk

... rather than:

  new_reservation = as_saved_to_disk.(
                      with_classification.(without_duplicate_animals.(reservation),
                                           DUPLICATE))

... or even:

  no_dups = without_duplicate_animals.(reservation)
  classified = with_classification.(no_dups, DUPLICATE)
  new_reservation = as_saved_to_disk.(classified)

I've created a quick-and-dirty immutable version of Hash, FunctionalHash, that supports this style.

One dubious decision I've made is that all the keys in a FunctionalHash are also methods. I think method chains like the following are jarring to read:

	    reservation.
	    without_duplicate_animals.
	    with_classification(DUPLICATE).
            as_saved_to_disk[:id]   # I'd rather see as_saved_to_disk.id
# or
            reservation.
	    without_duplicate_animals[:timeslice].
	    shift_timeslice(7.days)[:start_date]

Method chaining can be accomplished in several ways. One is by making the clumps-of-data that the program manipulates subclasses of FunctionalHash:

class Reservation < FunctionalHash
  def without_duplicate_animals #...
  def with_classification(type) #...
end

Stylistically, I don't like that because there's too much buy-in to the "classes represent the world" view of Object-Oriented programming. I prefer to think in terms of bundles of related functions that are attached to hashes when the programmer finds it convenient. That is, I want to emphasize modules over classes. So this looks a little better:

class Reservation < FunctionalHash
  include DatabaseAccess
  include ReservationModificationFunctions
end

reservation = Reservation.new(owner: "fred")

That's pretty wordy and still looks awfully OO-like. So how about this idea? Instead of a class containing methods, we think of maker functions that make FunctionalHashes with appropriate bundles of methods attached. As a convenience, a module method on FunctionalHash can be used to make those makers:

reservation_maker = FunctionalHash.make_maker(DatabaseAccess, ReservationModificationFunctions)
reservation = reservation_maker.(owner: "fred")  # Values for the hash are passed in to the maker
result = 
  reservation.
  do_something_to_reservation.
  do_something_else

I also like the idea of being able to attach methods to hashes after they're created. Suppose I have an empty hash representing some sort of underdescribed "thing":

	thing = FunctionalHash.new

I want to make a "derived" hash with the same data but with new methods attached:

       activated_thing = thing.become(BundleOMethods)

The name become is a bit of an odd choice. How does a thing become a BundleOfMethods? The reason I chose the name is that I like the Ruby convention of naming modules with adjectives like Comparable or Enumerable. In my opinion, the same convention makes sense in functional Ruby. If you think about it, the real-world adjectives we use to modify nouns tell us something about what verbs we use with the objects so described. For example, if an object is Light, we might expect to throw it or carry it. So I wouldn't expect something to become BundleOfMethods. It'd expect it to become Light or ReservationShaped. (I'll say more in the future about my feeling that shape might make a good metaphor to use when grouping methods.)


It's probably important to note that methods attached in these ways are "sticky". When you create one FunctionalHash from another by adding, removing, or changing keys, the new hash has whatever methods the original had. That's required for method chaining. Consider this chain:

	reservation = ...
	new_reservation = 
	    reservation.
	    without_duplicate_animals.
	    with_classification(DUPLICATE).
	    as_saved_to_disk

Each of the methods must create a new FunctionalHash. (They can't change the hash they're called upon since such hashes are immutable). If methods weren't sticky, the result of without_duplicate_animals could not have with_classification sent to it.

This can result in some oddity. For example, some code of mine takes a FunctionalHash of reservation data to create a new entry in the database's reservations table. As it does so, it merges some useful information onto the original data under the :new_id and :omitted_animals keys. (Hey, the info has to be put somewhere and putting it onto a "descendant" reservation hash allows the code to continue to be structured as a chain of message sends.) That useful information is destined for the javascript front end, so it needs to be turned into JSON:

    new_reservation.
      only(:new_id, :omitted_animals).
      to_json

only is a FunctionalHash function that produces a new FunctionalHash with all keys other than those named stripped out. So we have a FunctionalHash that looks something like this:

    {:new_id => 534, :omitted_animals => ["betsy", "jake"] }

... and yet this thing that has nothing whatever to do with reservations still responds to messages like :without_duplicate_animals. That's weird and unsettling... and maybe good if the idea of all of this is to break us out of the trap of thinking of data in here (our code) as some sort of mirror of the essence of an out there. In a pragmatist turn, our response to "It makes no sense to have reservation-ist methods attached to a hash that has had all the reservation-ey nature stripped away from it" could be: "OK, so don't use them."