Sunday, April 20, 2008

Learning to LISP

This is my third attempt at learning LISP. My first attempt was at college as part of my Artificial Intelligence elective course. While I gained enough competence in the language to pass the test, I did not particularly like it, and since I never had to use it after that, I soon forgot about it. My second attempt was a few years ago, fueled by comments on the blogosphere touting its advantages as a great tool for functional programming. Even though I did not have a good use case for it, I felt it was worth learning, if only to open my mind to think in terms of functional programming rather than in pure OO terms. I also came across the free e-book on Practical Common Lisp by Peter Siebel on the Internet, and I liked the book so much I bought myself a paper copy. I did attempt to make my way through the book, trying out the examples on a trial copy of Allegro-CL, but other things came up, so I gave up.

This time around, I did have something of a use-case for learning LISP. My plan was to have LISP scripts associated with workflow actions (see my last post), which would be sent over to the JMS server to be executed. In retrospect, though, I think I could probably have used Jython or Javascript to do the same thing more elegantly with less code. However, its here as a placeholder, in case I want to come back to it later.

I must admit that while I am in total awe of people who actually manage to write applications in LISP, I still don't like the LISP syntax that well. Or more likely I just don't get it, and the dislike could just be a function of unfamiliarity. However, what I do get is:

  • In LISP, code == data, much more so than in other languages.
  • LISP is probably the most parser-friendly language on the planet.
  • It is possible to extend LISP with custom functions and operations using defmacro, in ways not possible in C/C++ or Java.

Anyway, I started to look for a LISP interpreter that I could embed into my Java code and send it LISP scriptlets to execute. I chose JLisp initially, but soon realized I could not code LISP to save my life, so I had to stop and learn some LISP before I started up again.

To start off learning some basic LISP so you can start working with it, I strongly recommend the CUSP, a LISP plugin for Eclipse from Bitfauna. It comes integrated with SBCL, but I was not able to make SBCL work, so I downloaded clisp and used CUSP as a simple LISP editor, testing out my code on a terminal with clisp. I worked through about the first 10 chapters of Peter Siebel's book to get a basic understanding of LISP.

I also looked at Jatha while I was looking at JLisp, and while Jatha is probably the more mature of the two, JLisp seemed to be more suitable for what I was trying to do. For the example, I created a simple LISP "library" function closexec that will take a LISP program of this form:

1
2
(import com.mycompany.myapp.lisp.ext.closexec)
(closexec (setf x 1) (setf y 2) com.mycompany.myapp.lisp.Foo)

And then instantiate the Java class, set the variables, and call execute on it. Something like this:

1
2
3
4
5
Foo = new Foo();
foo.setX(1);
foo.setY(2);
foo.execute();
return foo.getResult();

To do this, I had to create a Library implementation called closexec (note the lowercase class name, this is a JLisp convention for Java based library extensions). Here is the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// closexec.java
package com.mycompany.myapp.lisp.ext;

import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;

import net.thinksquared.jlisp.Library;
import net.thinksquared.jlisp.Lisp;
import net.thinksquared.jlisp.Sexp;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.MethodUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class closexec implements Library {

  private final Log log = LogFactory.getLog(getClass());
  
  private Sexp CLOSEXEC;
  
  /**
   * @see net.thinksquared.jlisp.Library#apply(net.thinksquared.jlisp.Sexp, net.thinksquared.jlisp.Sexp)
   */
  public Sexp apply(Sexp function, Sexp args) {
    int result = 0;
    if (function.equals(CLOSEXEC)) {
      String className = null;
      Map<String,String> properties = new LinkedHashMap<String,String>(); 
      for (;;) {
        Sexp head = args.head;
        if (head.equals(args)) {
          // nothing more to extract from list
          break;
        }
        if (! head.at) {
          // not an atom, stack the setters
          Sexp name = head.head.tail.head;
          Sexp value = head.head.tail.tail.head;
          String nameVal = name.pname;
          String valueVal = (value.nmb ? value.pname : value.nval.toString());
          properties.put(nameVal, valueVal);
        } else {
          className = head.toS();
        }
        args = args.tail;
      }
      // now instantiate and run the class
      try {
        Object obj = Class.forName(className).newInstance();
        for (String var : properties.keySet()) {
          BeanUtils.setProperty(obj, var, (String) properties.get(var)); 
        }
        MethodUtils.invokeExactMethod(obj, "execute", null);
        result = Integer.valueOf(BeanUtils.getProperty(obj, "result"));
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
    Lisp lisp = new Lisp();
    return lisp.atom(new BigDecimal(result));
  }

  /**
   * @see net.thinksquared.jlisp.Library#register(java.util.Map, net.thinksquared.jlisp.Lisp)
   */
  @SuppressWarnings("unchecked")
  public void register(Map registry, Lisp lisp) {
    CLOSEXEC = lisp.atom("closexec");
    registry.put(CLOSEXEC, this);
  }
}

And here is the JUnit test to run this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// LispRunnerTest.java
package com.mycompany.myapp.lisp;

import net.thinksquared.jlisp.Lisp;
import net.thinksquared.jlisp.LispThread;
import net.thinksquared.jlisp.Sexp;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;

public class LispRunnerTest {

  private final Log log = LogFactory.getLog(getClass());
  
  private final String prog = 
    "(import com.mycompany.myapp.lisp.ext.closexec) " +
    "(closexec (setf x 1) (setf y 2) com.mycompany.myapp.lisp.Foo)";
  
  @Test
  public void testRunning() throws Exception {
    Lisp lisp = new Lisp();
    Sexp progSexp = new Sexp(prog);
    if (progSexp.bad()) {
      log.error("Lisp code is not well-formed, please check");
    }
    Sexp[] sexps = lisp.execute(prog);
    // first sexp is the import
    log.debug("result=" + sexps[1].toS());
  }
}

And here is the class that is going to be manipulated. Its deliberately kept very simple, since this is just a test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Foo.java
package com.mycompany.myapp.lisp;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Foo {

  private final Log log = LogFactory.getLog(getClass());
  
  private int x;
  private int y;

  private int result;
  
  public void setX(String x) {
    this.x = Integer.valueOf(x);
  }
  
  public void setY(String y) {
    this.y = Integer.valueOf(y);
  }
  
  public int getResult() {
    return result;
  }

  public void execute() {
    log.debug("Executing:" + getClass().getName());
    result = x + y;
  }
}

And the output of running the test looks like this:

1
2
Executing:com.healthline.gyncms.lisp.Foo
result=3

Anyway, as mentioned before, I don't plan on using this. All I need to do is pass in plain text scripts to a remote script runner, which I can do with less effort and with greater elegance with scripting languages that have a tighter integration with Java. With these languages, it is possible to instantiate, set and execute the object directly instead of passing it through the lisp interpreter and executing it with my own code. However, it's another way of doing it, even though it may not be the most optimal.

2 comments (moderated to prevent spam):

Unknown said...

Salmon,

Could you tell more details of the platform you tried to run cusp+sbcl and some details of what prevented sbcl from running?

Thank you

Sujit Pal said...

Hi Sergey, I suspect that sbcl failed to install because I am running on a 64-bit platform (AMD Turion 64x2). I used the cusp update site from within Eclipse, and everything seems to install fine, except the REPL console is not enabled.