Monday, March 12, 2012

A quick look at clojure-py

I played around with clojure-py this weekend. I jotted down some initial thoughts on the advantages of Clojure in pure Python in an earlier post.

Before I start, I want to point out that this is a very young project. My goal was only to get a feel for how things will work when there is a stable, well-tested release. I was impressed with what I saw and will happily await future releases. One to two years is a reasonable time frame for a production quality release on a project of this size. Users (myself included) have no reason to expect any specific functionality to work. We should expect bugs. I want to clarify these points because sometimes readers see something negative like "x doesn't work" and think I've written "Those incompetent developers haven't even implemented x. What a joke. Sigh."

I followed the instructions on the project wiki to install using easy_install. Consistent with the name, the install was easy. I started the REPL at the (Linux) command line using "clojurepy". I was greeted by

clojure-py 0.1.0
user=>

The starting point with any language has to be "Hello, World!":

(println "Hello, World!")

Okay, that's great, but what about something real? There are supposed to be 350 Clojure functions implemented, but I'm not sure which ones they are (reduce and ref are not).

Functions

Define a function:

(defn f [x y z] (+ x y z))

Call it:

(f 3 4.2 7.6)
(f 3, 4.2, 7.6)

clojure-py lets you use commas between arguments. The prefix notation is kind of ugly. Incanter offers infix notation, complete with operator precedence, so you could instead define f using

(defn f [x y z] ($= x + y + z))

Unfortunately ref is not yet implemented, so the Incanter infix library cannot be used. (I'm sure it could be made to work, but at this early stage it's wiser to let the language catch up.)

Multiple Arity

Let's try out multiple arity functions:

(defn sum
  ([x y]
    (+ x y))
  ([x y z]
    (+ x y z)))

(sum 1 2)
(sum 1 2 3)

That works. Now I can avoid prefix operators. This is not a functional solution, and it's pretty lengthy by Lisp standards, so let me use less code to sum an arbitrary number of arguments:

(defn sum [& stuff] (apply + stuff))

The [& stuff] indicates that the function takes a variable number of arguments, which will all be put into "stuff". (apply + stuff) applies the function "+" to all the elements of "stuff". Test it out:
(sum 1 2 3)
(sum 1 2 47.56 13 12 11 10 9)
(sum)

It works. clojure-py is clearly beyond the initial prototype stage; one could write complicated programs at this point.

Accessing Python Libraries

Enough with the pure Clojure stuff. It's nice to see that clojure-py is at such an advanced state but I can do all that and more on the JVM. The cool thing about clojure-py is that you can import existing Python libraries. That may not be helpful in a lot of areas (Java probably has more good quality libraries than any other language) but for numerical computing it is huge.

That includes scipy, Rpy2 to call R, and f2py to call Fortran, among many others. Presumably that also means you could call Pycuda from clojure-py, adding GPU computing to the things you can do from Clojure. I can only imagine the possibilities for using Clojure for GPU metaprogramming. cuda programming is not fun.

Python Standard Library

First I pulled some examples from the Python standard library tutorial. I created a namespace and added everything from the math library:

(ns tryclojure (:require math))

(math/cos 1.5)
(math/cos (/ math/pi 4))
(math/log 1024 2)

(random/choice ["apple" "pear" "banana"])

Oops, that last call throws an error. Let's add random to our namespace, but we want only two functions:


(ns tryclojure
  (:require [random :only [choice random]]))

(random/choice ["apple" "pear" "banana"])

(random/sample (py/range 10) 10)

(random/random)

The second example shows how to access functions like range that don't have to be imported into Python. You do not have to explicitly import them into clojure-py either.

Numpy


Now give numpy a try. I'd rather write np than numpy, so I can use the :as keyword.

(ns tryclojure
  (:require [numpy :as np]))

(np/arange 10)

(* 3 (np/arange 10))

(def x (np/array [1 2 3]))
(def y (np/array [4 5 6]))
(* x y)

(def a (np/array [[1 2] [3 4]]))
(def b (transpose a))
(dot a b)

I'm using arrays rather than matrices, so matrix multiplication is done with dot. (* a b) is element-by-element multiplication. The numpy syntax requires only two arguments to dot, so if you want to compute a*b*b*a, you have to do (dot (dot (dot a b) b) a). You should avoid nesting like that unless you have a very good reason. Nesting like that is only a little easier to follow than GOTO statements in idiomatic FORTRAN 66.

Threading Macro

Let's see if the threading macro is available. Don't worry if you don't know what it is, but it does simplify things. An example is (-> (dot 3.0) (/ 2)), which returns 1.5. It evaluates the first argument and uses it as the first argument of the next call.

(-> (dot a b) (dot b) (dot a))

I could also just use temporary variables in a let statement, which is verbose, but fine if you're a Clojure newbie or plan to share code with a Clojure newbie:

(let [temp-1 (dot a b)
      temp-2 (dot temp-1 b)]
  (dot temp-2 a))

Conclusions

If you want to add Clojure syntax and basic functionality to Python, it looks like you've already got most of what you need. I hope clojure-py eventually implements all of JVM Clojure and then provides a way to pass messages from one implementation to the other. Great work by the developers.

No comments: