One day, after reading a few pages of the pickaxe book and doing some hunting on the internet for Ruby evangelists, I came up with the following piece of Ruby code as the implementation of the Strategy Design Pattern :
class Filter
def filter(values)
new_list = []
values.each { |v| filter_strategy(v, new_list) }
new_list
end
end
class EvenFilter < Filter
def even?(i)
i%2 == 0
end
def filter_strategy(value, list)
if even?(value)
list << value
end
end
end
of = EvenFilter.new
array = [1,2,3,4,5]
puts of.filter(array)
On further introspection, more reading of the pickaxe book and more rummaging through the musings of Java bashers in LtU, the light of lambda dawned on me. Looked like I was going through the enlightenment of the functional programming paradigms, the enhanced expressivity and abstraction that higher order procedures add to the programs. I could appreciate the value of lexical closures, bottom-up programming and functional abstractions. The new class for Strategy implementation is adapted from Nathan Murray's excellent presentation on Higher Order Procedures in Ruby :
class FilterNew
def filter(strategy)
lambda do |list|
new_list = []
list.each do |element|
new_list << element if strategy.call(element)
end
new_list
end
end
end
of = FilterNew.new
filter_odds = of.filter( lambda{|i| i % 2 != 0} )
array = [1,2,3,4,5]
puts filter_odds.call(array)
The Disappearing Strategy Classes
In the new implementation, where is the strategy class that is supposed to be hooked polymorphically in the context and provide the flexible OO implementation ?
It has disappeared into the powerful abstraction of the language. The method filter() in the second example does not return the newly created list, unlike the first one - it returns a procedure, which can act on other sets of data. The second example is an implementation at a much higher level of abstraction, which adds to the expressivity of the intended functionality.
In fact with functional programming paradigms, many of the design patterns which GOF have carefully listed in the celebrated book on Design Patterns, simply go away in a language that allows user to program at a higher level of abstraction. Have a look at this excellent presentation by Norvig.
As Paul rightly mentions in his post, the paradigms of functional programming hides a lot of accidental complexity mainly because of the following traits, which the language offers :
- Higher level of abstraction, which leads to lesser LOC, and hence reduced number of bugs
- Side-effect free pure functional code, which liberates the programmer from managing state and sequence of execution
- Improved concurrency and scalability because of the stateless and side-effect-free programming model
Ruby or Lisp ?
People look upon Lisp as the language of the Gods, someone has mentioned Ruby as an acceptable Lisp, many others consider Ruby as lying midway between Java and Lisp. Ruby is an object-oriented language with functional programming capabilities, while Lisp came into being in 1958 with the landmark 'eval' function of John McCarthy. As Paul Graham says :
With macros, closures, and run-time typing, Lisp transcends object-oriented programming.
Lisp and Smalltalk have been the main inspirations to Matz behind designing the Ruby language. May be Ruby is more pragmatic than Lisp, but the roots of Ruby are definitely engrained within the concepts of pure macros, lexical closures and extensibility mechanisms that Lisp provides. Lisp is the true embodiment of "code-as-data" paradigm. Lispers claim that Lisp (or any of its dialects) is definitely more expressive than Ruby, Lisp macros can extend the language more seamlessly than Ruby blocks. I am not qualified enough to comment on this. But my only observation is that behind the nice Lispy DSL that Rails provide, its implementation looks really clumsy and possibly would have been much more cleaner in Lisp.
Not only Ruby, functional programming constructs are beginning to make their appearence in modern day OO languages as well. C# and Visual Basic already offer lambdas and comprehensions, Java will have closures in the next release - the Lisp style is promising to come back.
Still I do not think Lisp is going to make mainstream, yet I need to learn Lisp to be a better fit in today's world of changing programming paradigms.
13 comments:
At the risk of sounding monotonous (or possibly monotonic), perhaps you should consider learning Haskell rather than Lisp. Haskell is a pure functional language, so you will not be distracted by non-pure constructs and worries about whether they are appropriate in a given context. Haskell also has strong static typing. Whether you like strong type systems or not, you will understand the issues much better after learning Haskell.
BTW, thanks for the compliment.
Paul.
I don't intend to disagree with Paul Johnson, but there is still a relative shortage of good high-level Haskell books compared to the classics using Lisp and Scheme. So, perhaps you might still look at Lisp, at least enough to try your hands on SICP and other key works. They work for Haskellers too.
SICP stands for Structure and Interpretation of Computer Programs, and you can find the text on the web. Whether it's appropriate for you, you only can decide. But you can find it recommended highly in several places, I'm sure.
what's the deal with your filtering? if you think in terms of first-class functions, you can just pass the boolean function into some global filtering function, like so
def filter(lst)
lst.inject([]) {|accum,elem|
yield(elem) ? [elem]+accum : accum }
end
so you'd have filter (1..5) {|i| i%2==0}.
incidentally haskell has filter built-in, so you can just do
filter (\i->i%2==0) [1..5]
this has the friendly name 'find_all' in the enumerable class.
what's the deal with your filtering? if you think in terms of first-class functions, you can just pass the boolean function into some global filtering function, like so
def filter(lst)
lst.inject([]) {|accum,elem|
yield(elem) ? [elem]+accum : accum }
end
so you'd have filter (1..5) {|i| i%2==0}.
incidentally haskell has filter built-in, so you can just do
filter (\i->i%2==0) [1..5]
ruby has this too; it's 'find_all' in the enumerable class.
Enumerable ( the parent of both Hash & Array ) has a number of iteration methods built in.
So ...
You could do exactly what you are looking at with either :
[1,2,3,4,5].collect { |x| x % 2 == 0 }
and if you don't like the name you can always use:
module Enumerable
# alias the built in collect method
# to the name filter.
alias_method :filter, :collect
end
[1,2,3,4,5].filter { |x| x % 2 == 0 }
... just passing along the information.
jd.
Haskell has even:
>> filter even [1..5]
=> [2,4]
Also, % is not modulo in Haskell, but `mod` is:
>> filter (\i -> i `mod` 2 == 0) [1..5]
=> [2,4]
The Haskell equivalent of your strategy code is:
>> filterOdds = filter odd
>> filterOdds [1..5]
=> [2,4]
The normal way to do this in Ruby is:
>> (1..5).reject{|i| i % 2 != 0}
=> [2,4]
or with a def:
>> def filter_odds(arr)
>> arr.reject{|i| i % 2 != 0}
>> end
You can call it like this:
>> filter_odds([1,2,3,4,5])
or like this:
>> f = method(:filter_odds)
>> f.call([1,2,3,4,5])
Learn to program.
In Common Lisp you'd write in a functional style:
CL-USER 1 > (remove-if-not 'evenp (list 1 2 3 4 5))
(2 4)
For many things the object-oriented programming style (as you shown in Ruby) adds just too much unnecessary complication.
But when you need it (say, for larger architectures), Common Lisp has the Common Lisp Object System built in.
Common Lisp provides the dynamic features comparable to Ruby, but typical Common Lisp implementations will be able to generate much faster code - using an incremental or a batch compiler.
I have been making my living doing Rails full-time for more than a year now and have pushed Ruby around enough to find enough edges not just in Rails, but in the Ruby language itself, where pretty much only the power of macros would solve the problem the way I wanted to. So now I'm working my way through Paul Graham's book ANSI Common Lisp and am getting excited by the possibilities.
nice post
Thanks.Lisp programs can manipulate source code as a data structure giving rise to the macro systems.
Even shorter version of the strategy filter in ruby
class FilterNew
def filter(strategy)
lambda do |list|
list.select { |element| element if strategy.call(element) }
end
end
end
For almost every position I have held, the product implementation language had already been chosen. I had to learn what was there and become proficient in its use. Before I left private sector eight years ago, those languages included Perl and C++, because that is what Legato used. I also maintained a CoStandby For Windows configuration utility that was written in Java.
Leaving the private sector eight years ago meant becoming very proficient in Informix 4GL, whether I liked it or not. Please trust me when I say you do not want to be a hero by re-writing a municipal tax collection system, because you are used to programming in C/C++. You go with what you have and start designing your way into more modern languages. I did just that.
Before picking a "pure" functional programming language like Haskell or a not so pure, the question for me has had to be what will be useful in the market. I chose Clojure for that reason.
Whether it is the right or wrong kind of Lisp did not matter to me. Because it is a JVM language, I can easier introduce some carefully developed pieces into our production environment without totally wigging everyone out.
Thanks for this post. It was interesting as were the other comments.
may I guess that you eat your corn in spirals not rows? This appears to be a trait of analysis thinking rather than algebraic. Also may explain my interest in Ruby and Lisp. Cheers
Post a Comment