Saturday, August 27, 2011

Python and Clojure comparisons

Since I've slowly been trying to move to a functional style of programming (despite most companies in the industry still wanting all kinds of esoteric knowledge about OOP, design patterns and other OO madness like UML), I thought it might be interesting to contrast some python code with clojure code.  The python code I'll show here is definitely not the norm, but still valid.

Since I just covered clojure destructuring, it might be helpful to see the equivalent usage in python.  Take for example a common use in python for passing in arguments to functions like these:

 1 def get_positional_args(*args):
 2   for arg in args:
 3     print arg
 4     
 5 def get_positional_arg1(farg, *args):
 6   print "first argument is", farg
 7   for i, arg in enumerate(args):
 8     print "argument ", i + 2, "=", arg
 9     
10 def get_keyword_args(**kwargs):
11   for name in kwargs:
12     print name, "=", kwargs[name]
13     
14 def get_pos_and_kw_args(farg, sarg, *args, **kwargs):
15   print "First argument is", farg
16   print "second argument is", sarg
17   for i, arg in enumerate(args):
18     print "Argument #", i+2, "=", arg
19   for name in kwargs:
20     print name, "=", kwargs[name]

And these are the equivalent functions in clojure:

 1 (defn get-positional-args  [ & more ]
 2   (println more))
 3 
 4 (defn get-positional-arg1   [ x & more ]
 5   (println x)
 6   (println more))
 7 
 8 (defn get_keyword_args [ m ]
 9   (doseq [ name m ]
10     (println "key =" (name 0) "value =" (name 1))))
11 
12 (defn get-pos-and-kw-args [ f s m & more ]
13   (println "first arg is " f)
14   (println "second arg is " s)
15   (doseq 
16     [ arg (map vector (iterate inc 3) more) ]
17     (println "Argument" (arg 0) " is " (arg 1)))
18   (doseq [ kv m ]
19     (println "key = " (kv 0) " value = " (kv 1))))

Now, I don't know about you, but I think that the argument that 'lisp' has too many parenthesis isn't really all that true for clojure.  Sure, it has way more than python, and python is perhaps the cleanest looking language I've ever seen, but the other lisps/schemes I've seen aren't in the same league as clojure when it comes to reader friendliness.

So what are some other things that python and clojure have in common?  Believe it or not, python has a lazy language feature as well.  Clojure prefers using lazy sequences whenever possible (though it's not a lazy language by default like Haskell).  Still, through the use of Clojure macros or functions like delay and force, clojure can (explicitly) be made very lazy.  Python isn't nearly as lazy, but it does have one nice lazy feature....generators.

Generators and generator expressions are a way to explicitly run through a (possibly infinite) sequence.  The key to generators is the 'yield' statement, which "freezes" the functions until the generator object's next() function is called.  For example, we can create an infinite series of even numbers:

 1 def gen_even():
 2   i = 0
 3   while True:
 4     yield i
 5     i += 2
 6     
 7     
 8 def iterate_range(gen, i):
 9   res = None
10   for x in range(i):
11     res = gen.next()

However, the implementation above would not create a sequence as most people think (though technically, the generator itself has a next() method ).  A generator is really just an object with a next() method, and when you call that next() method, it 'yields' a value.  The next next() call generates another value.  But a generator by itself does not generate a sequence.  To generate a list, let's do this:

12 def create_map(gen, i)
13   return [ gen.next() for x in xrange(i) ]

What would happen if we call this (note, use xrange rather than range, since range has to generate a list consuming more memory)?

g = gen_even()
create_map(g, 10)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

This is very similar to lazy sequences in clojure.  For example, the equivalent in clojure to the above would be this:


(def v (iterate #(+ 2 %) 0))

I could perform the following actions on the lazy sequence v

(take 10 v)
(nth 9 v)


The first line would yield the same as python's create_map() above. The second function would return the 10th item in the sequence. The advantage of both python's generator and clojure lazy sequence is that the entire sequence is NOT stored in memory. Only whatever is required for the calculation is needed.

So why would you use generators or lazy sequences? One possibility is to eliminate recursive calls. Recursion in both python and clojure consumes stack space, and thus might blow out when the recursion depth is very large. Another possibility is to model infinite sequences such as those found in math. For example, that the sum i -> inf where f(x) = 1/2^i is 1.

def half_gen():
  i = 2
  while True:
    yield 1.0/i
    i = i * 2
    
h = half_gen()
sum([ h.next() for x in xrange(100000) ])


No comments:

Post a Comment