Vous êtes sur la page 1sur 11

Using a Ruby Class To Write Functional Code

Pat Shaughnessy

The time sheets I used at my first programming job in the Summer of 1986 looked just like this.

Recently Ive been spending some of my free time studying Clojure and Haskell. Ive been learning ho a program built ith a series of small! pure functions can be very robust and maintainable. Ho ever! I dont ant to give up on Ruby. I ant to keep the e"pressiveness! beauty and readability of Ruby! hile riting simple functions ith no side effects. #ut ho can this be possible$ %nlike functional languages! Ruby encourages you to hide state inside of objects! and to rite functions &methods' that have side effects! modifying an instance variable for e"ample. Isnt using an object oriented language like Ruby! Python! or (ava a decision to abandon the benefits of functional programming$ )o. In fact! a couple of eeks ago Rubys object model helped me refactor one confusing function into a series of small simple ones. *oday Ill sho you hat happened! ho using a Ruby class helped me rite more functional code.

Parsing Timesheet Data


,ets suppose you are a Scrum-aster. and ant to make sure your team of developers! including me! is putting in enough hours on your project &instead of taking long lunches or riting blog posts'. /or e"ample! suppose I report my hours like this0

1ou could parse my timesheet data using this simple Ruby program0

*his is simple enough to understand and orks fine. parse+ is small function2 if you remove the calls to puts it only contains 3 lines of code! t o simple calls to split. Ho could this be any simpler$

A First Pass at a Functional Solution


)e"t you decide to look for a more functional solution by asking Ruby for hat you ant! instead of telling it hat to do. 1ou try to break the problem up into small functions that return hat you need. #ut hat functions should you rite$ 4hat values should they return$ In this simple e"ample! the ans er is obvious0 you can rite a function to parse each value in the timesheet data.

1ou have divided the problem up into small pieces. 6ach function ill return a predictable value based on some input and doesnt have any side effects. *hese ill be pure functions0 *hey ill al ays return the same result given the same arguments. 1ou kno that if you pass a line from my timesheet! last7name ill al ays return 8Shaughnessy.9 1ouve turned the problem around2 youve phrased the problem as a series of :uestions rather than as a list of instructions. Refactoring parse+ above! you implement the functions! at least in a some hat verbose and ugly fashion0

Testing Pure Functions


;s a Certified Scrum-aster.! you believe in *<< and other e"treme programming practices. =riginally! hile riting parse+ above! it didnt even occur to you to rite tests &and if it had! it ould have been very difficult'. Ho ever! no after breaking the problem up into a series of functions! it seems natural to rite tests for them. )e"t! you e"press your e"pectations for these functions using -initest specs! for e"ample0

#ecause the functions are small! the tests are small. #ecause the tests are small! you actually take the time to rite them. #ecause the functions are decoupled from each other! its easy for you to decide hich tests to rite. *o your surprise! you actually find a bug> 3

6arlier in parse+! the e"tra space as lost in the puts output and you didnt notice it. Separating this into a small function and carefully testing it revealed a minor problem. 1ou adjust t o of the functions to remove the e"tra space0

Pushing Ruby Out Of Its Comfort Zone


1oure happy ith your ne tests. Ruby allo ed you describe the behavior of the functions in a very natural! readable ay. Ruby at its best. ;s an added bonus! the tests no also pass> Ho ever! your functions arent so pretty. *here is a lot of obvious duplication0 *he office! employee7id and last7name functions all call line.split&?!?'. *o fi" this! you decide to e"tract line.split&?!?' into a separate function! removing the duplication0

*his doesnt look any better2 in fact! theres a deeper problem here. *o see hat I mean lets refactor parse+ from earlier to use our ne functions0

*his is clean and easy to follo ! but no you have a performance bug0 6ach time around the loop! your code passes the same line to employee7id! office and last7name. )o Ruby ill call the values function over and over again. *his is unnecessary and needless2 in fact! our original parse+ code didnt have this problem. #y introducing functions e have slo ed do n our code. Ho ever! because these are simple! pure functions! you kno they ill al ays return the same value given the same input argument! the same line of te"t in this e"ample. *his means theoretically you can avoid calling split over and over again by caching the results. ;t first! you try to cache the return value of split by using a hash table like this0

*his looks straightfor ard0 *he keys in split7lines are the lines and the values are the corresponding split lines. 1ou use Rubys elegant AAB operator either to return a cached value from the hash or actually call split! updating the hash. *he only problem ith this is that it doesnt ork. *he code inside of the values function cant access the split7lines hash! located outside the method. ;nd if you move split7lines inside of values! it ould become a local variable and not retain values across method calls. *o ork around this problem you could pass the cache as an additional argument to values! but this ould make your program even more verbose than it is no . =r you could create the values method using define7method! instead of def! like this0

*his confusing Ruby synta" allo s the code inside of the ne values method to access the surrounding scope! including the hash table. Ho ever! taking a step back! something about your program no feels rong.

Instead of making your code simpler and easier to understand! functional programming has started to make your Ruby code more confusing and harder to read. 1ouve introduced a ne data structure to cache results! and resorted to confusing metaprogramming to make it ork. ;nd your functions are still :uite repetitive. 4hats gone rong$ Possibly Ruby isnt the right language to use ith functional programming.

Introducing a Ruby Class


)e"t! you decide to forget all about functional programming and to try again by using a Ruby class. 1ou rite a ,ine class! representing a single line of te"t from the timesheet te"t file0

;nd you decide to move your functions into the ne ,ine class0

)o you have a lot less noise. *he biggest improvement is that no theres no need to pass the line of te"t around as a parameter to each function. Instead! you hide it a ay in an instance variable! making the code much easier to read. ;lso! your functions have become methods. )o you kno all the functions related to parsing lines are in the ,ine class. 1ou kno here to find them! and more or less hat they are for. Ruby has helped you organiEe your code using a class! hich is really just a collection of functions. Continuing to simplify! you refactor the value method at the bottom to remove the confusing define7method synta"0

)o each instance of the ,ine class! each line of te"t you program uses! ill have its o n copy of Gvalues. #y using a Ruby class! you dont need to resort to a hash table to map bet een lines &keys' and split lines &values'. Instead you employ a very common Ruby idiom! combining an instance variable Gvalues! ith the AAB operator. Instance variables are the perfect place to cache information such as method return values.

Brea ing All the Rules


)o your code is much easier to read. %sing an object oriented instead of a functional design turned out to be a good idea.

4ith your object oriented solution! you have broken some of the most important rules of functional programming0 /irst! you created hidden state! the Gline instance variable! rapping it up and hiding it inside the ,ine class. *he Gvalues instance variable holds even more state information. ;nd second! the initialiEe and values methods have side effects0 *hey change the value of Gline and Gvalues. /inally! all the other methods of ,ine are no longer pure functions> *hey return values that depend on state located outside of each function0 the Gline variable. In fact! they can return different values even though they take no arguments at all. #ut I believe these are technicalities. 1ou havent lost the benefits of functional programming ith this refactoring. 4hile the methods of ,ine depend on e"ternal state &Gline and Gvalues'! that state isnt located very far a ay. Its still easy to predict! understand and test hat these small functions do. ;lso! hile Gline is technically a mutable string that you change in your program! in practice it isnt. 1ou set it once using initialiEe and then never change it again. 4hile you may update Gvalues each time values is called! its just a performance optimiEation. It doesnt change the overall behavior of values. 1ouve broken the rules and re ritten your pure! functional program is a more idiomatic! Ruby manner. Ho ever! you havent lost the spirit of functional programming. 1our code is just as easy to understand! maintain and test.

Creating an Ob!ect Pi"eline


4rapping up! you refactor your original program to use your ne ,ine class like this0

=f course! theres not much difference here. 1ou simply added a line of code to create ne line objects! and then called its methods instead of your original functions. /inally! you decide to take one step further and refactor again by mapping the array of te"t lines to an array of line objects0

;gain! not much difference in the code. Ho ever! the ay you think about your program has changed dramatically. )o your code implements a pipeline of sorts! passing data through a series of operations or transformations. 1ou start ith an array of te"t lines from a file! convert them into an array of Ruby objects! and finally process each object using your parse functions. *his pattern of passing data through a series of operations is common in languages such as Haskell and Clojure. 4hats interesting here is ho Ruby objects are the perfect target for these operations. 1ouve used a Ruby class to implement a functional programming pattern.

Update: =ren <obEinski suggested adding a to7s method to ,ine! hich ould allo us to push the object pipeline idea even further. *hanks =ren> See <ave *homass article *elling! ;sking! and the Po er of (argon for more background on 8*ell! <ont ;sk.9

+J

Pat Shaughnessy

++

Vous aimerez peut-être aussi