有些时候我们希望在REPL中自动给表达式添加括号, 特别是对一些命令行式的程序, 通常只有一行, 毕竟任何任务最终都可以抽象为一个函数极其参数, 这个时候括号就有些多余
比如我们下面一个宏, 执行的时候会调出浏览器转到Google并查询字符串"how clojure repl works"
user=>(search how clojure repl works)
我们希望可以不需要加括号, 就如同在命令行里面一样.
user=>search how clojure repl works
看了一下Clojure的语法和REPL实现, 发现并没有内置的机制可以实现这一点, 唯一的办法就是修改Clojure的源码. 还好Clojure整个项目并不大, 可以随时从源码构建.
下载Clojure 1.6.0的压缩包, 其中已经包含了全部源码和Maven构建配置文件.
经过研究发现这一点可以实现, 方法是在读取到类似这样的表达式的时候, 具体就是只有一行, 以字母开头, 我们会在发送给LispReader之前提前把表达式读取出来, 并加上括号.
Clojure用来读取表达式的工具是PushbackReader, 这是一个对System.in的一个包装. 由于我们需要pushback整个字符串, 而默认的pushback只支持单个字符的pushback, 因此先要修改这个参数
找到LineNumberingPushbackReader并添加一个构造函数
public LineNumberingPushbackReader(Reader r, int size, int pushbackSize){ super(new LineNumberReader(r), pushbackSize); }
然后找到RT.java, 修改*in*的构造
final static public Var IN = Var.intern(CLOJURE_NS, Symbol.intern("*in*"), new LineNumberingPushbackReader(new InputStreamReader(System.in), 1000, 1000)).setDynamic();
最后转到main.clj, 修改如下的代码
(defn is-char-letter [ch] (if (or (and (>= ch (int \a)) (<= ch (int \z))) (and (>= ch (int \A)) (<= ch (int \Z))) ) true nil ) ) ;; a problem, if input (println "hello, repl will ask more ;; but for our case the input will be (println "hello) (defn auto-paren [s] (let [first-ch (.read s)] (.unread s first-ch) ;; push it back no matter what ;(println "first char is " (char first-ch)) (if (= first-ch (int \() ) nil ;; do nothing in this case (if (is-char-letter first-ch) ;; else if it start with a-z A-Z ;; 确定buffer里面只有一行, 然后把整行读出来, 加上括号, 再pushback (let [command-line-no-paren (clojure.string/reverse (loop [ch (.read s) acc ""] ;(println "a char from System.in" (if (= ch -1) "-1" (char ch)) " the number is" ch) (if (or (= ch (int \newline))(= ch -1)) acc (recur (.read s) (str (char ch) acc)) ) ) ) ;_ (println "command-line-no-paren is " command-line-no-paren ) ] (.unread s (char-array (str "(" command-line-no-paren ")" \newline))) ) ) ) ) ) (defn skip-whitespace "Skips whitespace characters on stream s. Returns :line-start, :stream-end, or :body to indicate the relative location of the next character on s. Interprets comma as whitespace and semicolon as comment to end of line. Does not interpret #! as comment to end of line because only one character of lookahead is available. The stream must either be an instance of LineNumberingPushbackReader or duplicate its behavior of both supporting .unread and collapsing all of CR, LF, and CRLF to a single \\newline." [s] ;(println "before the loop block on read") ;; we need to modify the content of the stream (loop [c (.read s)] ;(println "skip whitespace, loop a char: " c) (cond (= c (int \newline)) :line-start (= c -1) :stream-end (= c (int \;)) (do (.readLine s) :line-start) (or (Character/isWhitespace (char c)) (= c (int \,))) (recur (.read s)) :else (do (.unread s c) (auto-paren s) :body))))
跳过注释和空白之后, 如果第一个字符是字母而不是左括号, 我们会读取整个表达式并对其加上括号, 然后pushback到输入流中, 这样reader读到的就是正确的Clojure表达式.
不要考虑修改Reader的逻辑, 那样会过于复杂.
修改后的结果
user=> println "hello" hello nil
不过目前有一个缺陷是如果表达式是变量而不是函数和命令的话, 这个变量会被当作函数, 因此要查看变量需要使用println