Professional Documents
Culture Documents
Jul 17th, 2012 The following is an aimless journey through a degenerate form of Ruby, in an effort to learn a bit more about functional programming, simplicity, and A ! design" #uppose that the only way we ha$e to organi%e code in Ruby is to ma&e lambdas, and the only way we ha$e to structure data are arrays'
1 2 3 4 5 6 7 8 square = ->(x) { x * x } square.(4) # => 16 person = [" a!e""#$a%e& pr'n()person = ->((na$e"*en+er)) { pu(s "#{na$e} 's a #{*en+er}" } pr'n()person.(person)
This is the most bare(bones essence of functional programming' all we ha$e is functions" )et*s write some real(ish code this way and see how far we get before it starts becoming painful" #uppose we want to manipulate a database of people, and someone has pro$ided us a few functions to interact with a data store" +e want to use these to add a ,! and some $alidations" -ere*s how we interact with our data store'
1 2 3 4 'nser()person.(na$e",'r(-+a(e"*en+er) # => re(urns an '+ up+a(e)person.(ne.)na$e"ne.),'r(-+a(e"ne.)*en+er"'+) +e%e(e)person.('+) /e(0-)person.('+) # => re(urns (-e na$e" ,'r(-+a(e" an+ *en+er as an arra1
.irst, we need to be able to add a person to our database, along with some $alidations" +e*ll get this data from user input /we can assume that pu(s and *e(s are built(ins that wor& as e0pected1'
1 2 3 4 5 6 7 8 pu(s "2a$e3" na$e = *e(s pu(s "4'r(-+a(e3" ,'r(-+a(e = *e(s pu(s "5en+er3" *en+er = *e(s
+e need a function to do our $alidations and add a person to the database" +hat might it loo& li&e2 !t should accept the attributes of a person and return either an id /on successfully $alidation and insertion1, or an error message, representing what went wrong" #ince we don*t ha$e e0ceptions or hashes ( just arrays ( we*re going to ha$e to get creati$e"
)et*s create a con$ention in our system that e$ery business logic methods returns an array of si%e 2" The first element is the return $alue on success, and the second element is an error message on failure" The presence or absence of data in one of these slots indicates the result" 3ow that we*$e sorted out what we accept as arguments and what we*re going to return, let*s write our function'
a++)person = ->(na$e",'r(-+a(e"*en+er) { 1 re(urn [n'%""2a$e 's requ're+"& 2 re(urn [n'%""4'r(-+a(e 's requ're+"& 3 88 4 re(urn [n'%""5en+er 's requ're+"& 5 re(urn [n'%""5en+er $us( ,e 8$a%e8 or 8/e$a%e8"& 6 *en+er 9= 8/e$a%e8 7 8 '+ = 'nser()person.(na$e",'r(-+a(e"*en+er) 6 [[na$e",'r(-+a(e"*en+er"'+&"n'%& } '/ 7(r'n*(na$e) == 88 '/ 7(r'n*(,'r(-+a(e) == '/ 7(r'n*(*en+er) == 88 '/ *en+er 9= 8$a%e8 ::
!f you aren*t familiar with 7(r'n*(), it is a function that coalesces nil to the empty string, so we don*t ha$e to chec& for both" +ith this function, what we*d li&e to do is call it in a loop until the user has pro$ided correct input, li&e so'
1 'n!a%'+ = (rue 2 .-'%e 'n!a%'+ 3 pu(s "2a$e3" 4 na$e = *e(s 5 pu(s "4'r(-+a(e3" 6 ,'r(-+a(e = *e(s 7 pu(s "5en+er3" 8 *en+er = *e(s 6 resu%( = a++)person.(na$e",'r(-+a(e"*en+er) 1; '/ resu%([1& == n'% 11 pu(s "7u00ess/u%%1 a++e+ person #{resu%([;&[;&}" 12 'n!a%'+ = /a%se 13 e%se 14 pu(s "<ro,%e$# #{resu%([1&}" 15 en+ 16 en+
4f course, we ne$er said anything about .-'%e loops '1 #uppose we don*t ha$e them"
7 *en+er = *e(s 8 resu%( = a++)person.(na$e",'r(-+a(e"*en+er) 6 '/ resu%([1& == n'% 1; pu(s "7u00ess/u%%1 a++e+ person #{resu%([;&[;&}" 11 resu%([;& 12 e%se 13 pu(s "<ro,%e$# #{resu%([1&}" 14 *e()ne.)person.() 15 en+ 16 } 17 18 person = *e()ne.)person.()
+e can en$ision that our code is going to ha$e a lot of '/ resu%([1& == n'% in it, so let*s wrap it in a function" The great thing about functions is that they allow us to re(use structure, as opposed to logic" The structure here is chec&ing for an error and doing one thing on success and another on error"
1 -an+%e)resu%( = ->(resu%("on)su00ess"on)error) { 2 '/ resu%([1& == n'% 3 on)su00ess.(resu%([;&) 4 e%se 5 on)error.(resu%([1&) 6 en+ 7}
3otice what the use of -an+%e)resu%( allows us to e0plicitly name $ariables, instead of using Array de(referencing" 3ot only can we name error)$essa*e, but, using Ruby*s array( e0traction synta0, we can 5e0plode6 our person array into its attributes $ia the (('+"na$e",'r(-+a(e"*en+er)) synta0"
#o far, so good" This code is probably a bit weird loo&ing, but it*s not terribly $erbose, or comple0"
1 2 3 4 5 6 7 8 6 1;
And, since we use these e0tractions in *e()ne.)person, that has to change, too" ,gh'
1 *e()ne.)person = -> { 2 pu(s "2a$e3" 3 na$e = *e(s.0-o$p 4 pu(s "4'r(-+a(e3" 5 ,'r(-+a(e = *e(s.0-o$p 6 pu(s "5en+er3" 7 *en+er = *e(s.0-o$p 8 pu(s "='(%e3" 6 ('(%e = *e(s.0-o$p 1; 11 resu%( = a++)person.(na$e",'r(-+a(e"*en+er"('(%e) 12 13 -an+%e)resu%(.(resu%(" 14 ->((na$e",'r(-+a(e"*en+er"('(%e"'+)) { 15 pu(s "7u00ess/u%%1 a++e+ person #{'+}" 16 ['+"na$e",'r(-+a(e"*en+er"('(%e"'+& 17 }" 18 ->(error)$essa*e) { 16 pu(s "<ro,%e$# #{error)$essa*e}" 2; *e()ne.)person.() 21 } 22 ) 23 }
This is the $ery definition of high(coupling" *e()ne.)person really shouldn*t care about the particular fields of a person9 it should simply read them in, and then pass them to a++)person" )et*s see if we can ma&e that happen by e0tracting some of this code into new functions"
1 2 3 4 5 6 7 8 6 1; 11 12 13 14 15 16 17 18 16 2; 21 22 23 24 25 26 rea+)person)/ro$)user = -> { pu(s "2a$e3" na$e = *e(s.0-o$p pu(s "4'r(-+a(e3" ,'r(-+a(e = *e(s.0-o$p pu(s "5en+er3" *en+er = *e(s.0-o$p pu(s "='(%e3" ('(%e = *e(s.0-o$p [na$e",'r(-+a(e"*en+er"('(%e& } person)'+ = ->(*)"'+) { '+ } *e()ne.)person = -> { -an+%e)resu%(.(a++)person.(*rea+)person)/ro$)user.()) ->(person) { pu(s "7u00ess/u%%1 a++e+ person #{person)'+.(person)}" person }" ->(error)$essa*e) { pu(s "<ro,%e$# #{error)$essa*e}" *e()ne.)person.() } ) }
+e*$e now abstracted the way in which we store a person into two functions' rea+)person)/ro$)user and person)'+" At this point, *e()ne.)person will not need to change if we add more fields to a person" !f you*re confused about the use of * in this code, here*s a brief e0planation' * allows us to treat an array as a list of arguments and $ice $ersa" !n person)'+, we use the parameter list *)"'+, which tells Ruby to place all arguments to the function, sa$e the last, into the $ariable ) /so(named because we don*t care about its $alue1, and place the last argument in the $ariable '+" This only wor&s in Ruby 1":9 in 1"; only the last argument of a function may use the * synta0" .urther, when we call a++)person, we use the * on the results of rea+)person)/ro$)user" #ince rea+)person)/ro$)user returns an array, we want to treat that array as if it were an argument list, since a++)person accepts e0plicit arguments" The * does that for us" 3ice< =ac& to our code, you*ll note that we still ha$e coupling between rea+)person)/ro$)user and person)'+" They both are intimate with how we store a person in an array" .urther, if we added new features to actually do something with our people database, we can en$ision more methods coupled to this array(based format" +e need some sort of data structure"
>
acts li&e a constructor, but instead of returning an object /which don*t e0ist for us1, we return a function that, when called, can tell us the $alues of the $arious attributes of our person" +e e0plicitly itemi%e the possible attributes, so we ha$e a fairly firm definition of what the type of a person is"
ne.)person
!nteresting" The si%e of these two bits of code is more or less the same, but the class(based $ersion is full of special forms" #pecial .orms are essentially magic pro$ided by the language or runtime" To understand this code, you need to &now'
what 0%ass means that calling ne. on the class*s name calls the 'n'('a%'?e methods what methods are that prepending @ to a $ariable ma&es it pri$ate to the class* instance the difference between a class and an instance what a((r)rea+er does @
)i&e ! said, ! find this interesting" +e ha$e two ways of writing essentially the same code, and one way reAuires you to ha$e a lot more special &nowledge than the other" 4B, now that we ha$e a real data structure, let*s rewor& our code to use it, instead of arrays'
rea+)person)/ro$)user = -> { pu(s "2a$e3" na$e = *e(s.0-o$p pu(s "4'r(-+a(e3" ,'r(-+a(e = *e(s.0-o$p pu(s "5en+er3" *en+er = *e(s.0-o$p pu(s "='(%e3" 1 ('(%e = *e(s.0-o$p 2 3 ne.)person.(na$e",'r(-+a(e"*en+er"('(%e) 4 } 5 6 a++)person = ->(person) { 7 re(urn [n'%""2a$e 's requ're+"& '/ 7(r'n*(person. 8 (#na$e)) == 88 6 re(urn [n'%""4'r(-+a(e 's requ're+"& '/ 7(r'n*(person. 1; (#,'r(-+a(e)) == 88 11 re(urn [n'%""5en+er 's requ're+"& '/ 7(r'n*(person. 12 (#*en+er)) == 88 13 re(urn [n'%""5en+er $us( ,e 8$a%e8 or 8/e$a%e8"& '/ person.(#*en+er) 9= 14 8$a%e8 :: 15 person.(#*en+er) 9= 16 8/e$a%e8 17 18 '+ = 'nser()person.(person.(#na$e)"person.(#,'r(-+a(e)"person. 16 (#*en+er)"person.(#('(%e)) 2; [ne.)person.(person.(#na$e)"person.(#,'r(-+a(e)"person. 21 (#*en+er)"person.(#('(%e)"'+)"n'%& 22 } 23 24 *e()ne.)person = -> { 25 -an+%e)resu%(.(a++)person.(rea+)person)/ro$)user.())" 26 ->(person) { 27 pu(s "7u00ess/u%%1 a++e+ person #{person.(#'+)}" person }" ->(error)$essa*e) { pu(s "<ro,%e$# #{error)$essa*e}" *e()ne.)person.() } ) }
is a bit noisier, due to the synta0 of getting an attribute, but we can now add new fields $ery easily and &eep things structured"
a++)person
+hile we*re at it, let*s add inheritance< #uppose we ha$e an employee that is a person, but with an employee id number'
1 ne.)e$p%o1ee = ->(na$e",'r(-+a(e"*en+er"('(%e"e$p%o1ee)'+)nu$,er"'+) { 2 person = ne.)person.(na$e",'r(-+a(e"*en+er"('(%e"'+) 3 re(urn ->(a((r',u(e) { 4 re(urn e$p%o1ee)'+)nu$,er '/ a((r',u(e == #e$p%o1ee)'+)nu$,er
5 re(urn person.(a((r',u(e) 6 } 7}
+e*$e created classes, objects, and inheritance, all with just functions, and in just a few lines of code" !n a sense, an object in an 44 language is a set of functions that ha$e access to a shared set of data" !t*s not hard to see why adding an object system to a functional language is considered tri$ial by those &noweldgable in functional languages" !t*s certainly a lot easier than adding functions to an object(oriented language< Although the synta0 for accessing attributes is a bit clun&y, !*m not feeling a ton of pain by not ha$ing classes" ?lasses seem almost li&e syntactic sugar at this point, rather than some radical concept" 4ne thing that seems problematic is mutation" )oo& at how $erbose a++)person is" !t calls 'nser()person to put our person into the database, and gets an !C bac&" +e then ha$e to create an entirely new person just to set the !C" !n classic 44, we*d just do person.'+ = '+" !s mutable state what*s nice about this construct2 !*d argue that its compactness is what*s nice, and the fact that this compactness is implemented $ia mutable state is just incidental" ,nless we are in a se$erely memory(star$ed en$ironment, with terrible garbage collection, we aren*t going to be concerned about ma&ing new objects" +e are going to be annoyed by the needless repetition of building new objects from scratch" #ince we already &now how to add functions to our, er, function, let*s add one to bring bac& this compact synta0"
1 ne.)person = ->(na$e",'r(-+a(e"*en+er"('(%e"'+=n'%) { 2 re(urn ->(a((r',u(e"*ar*s) { 3 re(urn '+ '/ a((r',u(e == #'+ 4 re(urn na$e '/ a((r',u(e == #na$e 5 re(urn ,'r(-+a(e '/ a((r',u(e == #,'r(-+a(e 6 re(urn *en+er '/ a((r',u(e == #*en+er 7 re(urn ('(%e '/ a((r',u(e == #('(%e 8 '/ a((r',u(e == #sa%u(a('on 6 '/ 7(r'n*(('(%e) == 88 1; re(urn na$e 11 e%se 12 re(urn ('(%e A " " A na$e 13 en+ 14 en+ 15 16 '/ a((r',u(e == #.'(-)'+ # B=== 17 re(urn ne.)person.(na$e",'r(-+a(e"*en+er"('(%e"ar*s[;&) 18 en+ 16 2; n'% 21 } 22 }
3 4 5 6 7 8 6 1;
(#na$e)) == 88 re(urn [n'%""4'r(-+a(e 's requ're+"& '/ 7(r'n*(person. (#,'r(-+a(e)) == 88 re(urn [n'%""5en+er 's requ're+"& '/ 7(r'n*(person. (#*en+er)) == 88 re(urn [n'%""5en+er $us( ,e 8$a%e8 or 8/e$a%e8"& '/ person.(#*en+er) 9= 8$a%e8 :: person.(#*en+er) 9= 8/e$a%e8 '+ = 'nser()person.(person.(#na$e)"person.(#,'r(-+a(e)"person. (#*en+er)"person.(#('(%e)) [person.(#.'(-)'+"'+)"n'%& # B==== }
!t*s not Auite as clean as person.'+ = '+, but it*s terse enough that it*s still readable, and the code is better for it"
+e can model a map as a list, by treating it as a list with three entires' the &ey, the $alue, and the rest of the map" )et*s a$oid the 544 style6 of ma&ing 5methods6 and just &eep it pureful functional'
1 2 3 4 5 6 7 8 6 e$p(1)$ap = [& a++ = ->($ap"De1"!a%ue) { [De1"!a%ue"$ap& } *e( = ->($ap"De1) { re(urn n'% '/ $ap == n'% re(urn $ap[1& '/ $ap[;& == De1 re(urn *e(.($ap[2&"De1) }
10
+e could certainly replace our ne.)person implementation with a map, but it*s nice to ha$e an e0plicit list of attributes that we support, so we*ll lea$e ne.)person as(is" 4ne last bit of magic" 'n0%u+e is a nice feature of Ruby9 it lets us bring modules into scope to a$oid using the namespace" ?an we do that here2 +e can get close'
1 2 3 4 5 6 7 8 6 1; 11 12 13 14 15 16 17 18 16 2; 'n0%u+e)na$espa0e = ->(na$espa0e"0o+e) { 0o+e.(->(De1) { *e((na$espa0e"De1) }) } a++)person = ->(person) { re(urn [n'%""2a$e 's requ're+"& (#na$e)) == 88 re(urn [n'%""4'r(-+a(e 's requ're+"& (#,'r(-+a(e)) == 88 re(urn [n'%""5en+er 's requ're+"& (#*en+er)) == 88 re(urn [n'%""5en+er $us( ,e 8$a%e8 or 8/e$a%e8"& 8$a%e8 :: 8/e$a%e8 'n0%u+e)na$espa0e(peop%e" ->()) { '+ = )(#'nser().(person.(#na$e)" person.(#,'r(-+a(e)" person.(#*en+er)" person.(#('(%e)) '/ 7(r'n*(person. '/ 7(r'n*(person. '/ 7(r'n*(person. '/ person.(#*en+er) 9= person.(#*en+er) 9=
11
} }
[)(#ne.).(#.'(-)'+"'+)"n'%&
4B, this might be o$er the top, but it*s fairly interesting to thin& of something li&e 'n0%u+e as just a way to 5type less stuff6, and that we can achie$e a similar reduction in 5typing stuff6 by just using functions"
12