treeコマンドを作る

Clojureの練習として、treeコマンドを作って・・・みようとしたのですが、
最初いきなりClojureで作ろうとして混乱してしまいました。


そこで、まず自分が使いなれたPythonでtreeを作ってみました。

実行例:
$> python tree.py hello-clojure

P:\\hello-clojure
--build.xml
--COPYING
--html
`--index.html
`--src |--log4j.properties |--logging.properties |--META-INF | `--jdoconfig.xml |--org | `--example | |--HelloAppEngineServlet.java | `--HelloClojureServlet.clj `--WEB-INF |--appengine-web.xml |--lib | `--clojure.jar `--web.xml
Pythonのコード:
#encoding:shift-jis
#tree.py
from __future__ import division, print_function, unicode_literals

import os
import sys
from os.path import *
from operator import methodcaller

def add_prefix_to_strs(prefix, strs):
    return [prefix + s for s in strs]

def tree_lines_(path, is_last=True):
    path = abspath(path)
    
    if is_last:
        branch_str = "`--"
        indent_str = "   "
    else:
        branch_str = "|--"
        indent_str = "|  "
    
    lines = [branch_str + basename(path)]
    if isdir(path):
        entries = os.listdir(path)
        entries.sort(key=methodcaller("lower"))
        for i, entry in enumerate(entries):
            is_last_entry = i == len(entries) - 1
            entry_lines = tree_lines_(join(path, entry), is_last_entry)
            entry_lines = add_prefix_to_strs(indent_str, entry_lines)
            lines.extend(entry_lines)
    return lines

def tree_lines(path):
    path = abspath(path)
    
    lines = []
    lines.append(path)
    lines.append("|")
    entries = os.listdir(path)
    entries.sort(key=methodcaller("lower"))
    for i, entry in enumerate(entries):
        is_last_entry = i == len(entries) - 1
        entry_lines = tree_lines_(join(path, entry), is_last_entry)
        lines.extend(entry_lines)
    return lines

def print_lines(lines):
    for line in lines:
        print(line)

def main():
    if 2 <= len(sys.argv):
        if isdir(sys.argv[1]):
            root = sys.argv[1]
        else:
            root = dirname(sys.argv[1])
    else:
        root = os.getcwd()
    
    print()
    print_lines(tree_lines(root))

if "__main__" == __name__:
    main()

tree最低限の機能しか無いのですが、これをClojureに翻訳してみます。

Clojureのコード:
;tree.clj
(ns tree
  (:import java.io.File)
  (:use clojure.contrib.str-utils))

(defn add-prefix-to-strs [prefix strs]
  (for [s strs] (.concat prefix s)))

(defn sub-entries [dir]
  (for [entry (.list dir)] (File. dir entry)))

(defn with-is-last [seq]
  (partition 2
    (concat (interleave (repeat false) (drop-last seq))
            (list true (last seq)))))

(defn tree-lines- [file, last?]
  (let [[prefix prefix-sub] (if last? ["`--" "   "]
                                      ["|--" "|  "])]
    (concat
      [(.concat prefix (.getName file))]
      (when (.isDirectory file)
        (let [entries (with-is-last (sort (sub-entries file)))]
          (for [[last-entry? entry] entries
                line (add-prefix-to-strs prefix-sub (tree-lines- entry last-entry?))]
            line))))))

(defn tree-lines [root-dir]
  (concat
    [(.getAbsolutePath root-dir) "|"]
    (let [entries (with-is-last (sub-entries root-dir))]
      (for [[last-entry? entry] entries
            line (tree-lines- entry last-entry?)]
           line))))

(defn print-lines [lines]
  (doseq [line lines]
    (println line)))

(defn get-dir [file]
  (if (.isDirectory file) 
    file
    (.getParentFile file)))

(let [root (if (nil? *command-line-args*)
             (File. ".")
             (get-dir (File. (first *command-line-args*))))]
  (print \newline)
  (print-lines (tree-lines root)))

気づいた点など

  • パスやディレクトリ構造はjava.io.Fileをimportして使う(Clojure var.1.2 以降は(:use clojure.java.io))
  • Clojureにはシーケンスの長さを取得する関数が無い?ウソです、countで取得できます。
  • ClojureにはPythonのzip関数が無い?
  • Python版28行目のような「リストの最後で」という条件式を、Clojureに直訳することはできない?
  • 取り合えず、Clojureでは「リストの最後で」はwith-is-last関数として独立させる必要がある(Pythonのようにコピペすることは出来なさそう)
  • Python版16行目〜21行目のようなifの中で複数の変数に代入するパターンは、Clojureに翻訳するとletとifがネストする。
  • 全体的にClojureはカッコのネストが深くなりがち
  • ネストが深くなると、コードを理解するのも、カッコの対応を保つのも難しくなるの
  • ネストを避けるには短い関数に分割せざるを得ない

結論

実はClojure(Lisp)はプログラマに短い関数を書くよう強制する言語であった!

GAEjでcompojureを使う場合、cljの先頭で

(ns servlet
  (:gen-class :extends javax.servlet.http.HttpServlet)
  (:use compojure.html))

してはいけません!!

もし、compojure.htmlを使おうとすると・・・

HTTP ERROR 500

Problem accessing /. Reason:

    java.rmi.server.UID is a restricted class. Please see the Google  App Engine developer's guide for more details.

Caused by:

java.lang.NoClassDefFoundError: java.rmi.server.UID is a restricted class. Please see the Google  App Engine developer's guide for more details.
	at com.google.appengine.tools.development.agent.runtime.Runtime.reject(Runtime.java:51)
	at org.apache.commons.fileupload.disk.DiskFileItem.(DiskFileItem.java:103)
	at java.lang.Class.forName0(Native Method)
         (以下略)

compojure.htmlのいづれかのモジュールがjava.rmi.server.UIDを要求していると思われます。具体的にどれなのかは知りません。


必要なものだけuseするようにしましょう。

(ns servlet
  (:gen-class :extends javax.servlet.http.HttpServlet)
  (:use [compojure.http servlet routes]))