25

我正在涉足 clojure,并且在尝试确定与这个常见的 python 习语等效的 clojure(和/或 Lisp)时遇到了一些麻烦。

习惯用法是在python模块的底部通常有一些测试代码,然后是运行代码的语句,例如:

# mymodule.py
class MyClass(object):
    """Main logic / code for the library lives here"""
    pass

def _runTests():
    # Code which tests various aspects of MyClass...
    mc = MyClass() # etc...
    assert 2 + 2 == 4

if __name__ == '__main__': _runTests()

这对于简单的临时测试很有用。人们通常会通过编写来使用这个模块from mymodule import MyClass,在这种情况下_runTests()永远不会被调用,但是在最后的代码片段中,人们也可以通过python mymodule.py直接从命令行键入来运行它。

Clojure(和/或常见的 lisp)中是否有等效的成语?我不是在追求一个成熟的单元测试库(嗯,我是,但不是在这个问题中),我只想在一个模块中包含一些代码,这些代码只会在某些情况下运行,所以我可以一种运行我一直在研究的代码的快速方法,但仍然允许我的文件像普通模块/命名空间一样被导入。

4

8 回答 8

28

从命令行一遍又一遍地运行 Clojure 脚本并不习惯。REPL 是一个更好的命令行。Clojure 是一个 Lisp,启动 Clojure 并让同一个实例永远运行并与之交互而不是重新启动它是很常见的。您可以一次更改一个正在运行的实例中的函数,运行它们并根据需要戳它们。摆脱繁琐而缓慢的传统编辑/编译/调试周期是 Lisps 的一大特色。

您可以轻松地编写函数来执行诸如运行单元测试之类的操作,并且只要您想运行它们就可以从 REPL 中调用这些函数,否则就忽略它们。在 Clojure 中很常见使用clojure.contrib.test-is,将测试函数添加到命名空间,然后使用clojure.contrib.test-is/run-tests来运行它们。

另一个不从命令行运行 Clojure 的好理由是 JVM 的启动时间可能会令人望而却步。

如果你真的想从命令行运行 Clojure 脚本,有很多方法可以做到。有关讨论,请参阅Clojure 邮件列表。

一种方法是测试是否存在命令行参数。鉴于foo.clj在当前目录中:

(ns foo)

(defn hello [x] (println "Hello," x))

(if *command-line-args*
  (hello "command line")
  (hello "REPL"))

根据您启动 Clojure 的方式,您将获得不同的行为。

$ java -cp ~/path/to/clojure.jar:. clojure.main foo.clj --
Hello, command line
$ java -cp ~/path/to/clojure.jar:. clojure.main
Clojure 1.1.0-alpha-SNAPSHOT
user=> (use 'foo)
Hello, REPL
nil
user=>

src/clj/clojure/main.clj如果您想了解它是如何工作的,请参阅 Clojure 源代码。

另一种方法是将代码编译成.class文件并从 Java 命令行调用它们。给定一个源文件foo.clj

(ns foo
  (:gen-class))

(defn hello [x] (println "Hello," x))

(defn -main [] (hello "command line"))

建立一个目录来存放编译后的.class文件;这默认为./classes. 你必须自己创建这个文件夹,Clojure 不会创建它。还要确保您设置$CLASSPATH为包含./classes源代码的目录;我假设foo.clj在当前目录中。所以从命令行:

$ mkdir classes
$ java -cp ~/path/to/clojure.jar:./classes:. clojure.main
Clojure 1.1.0-alpha-SNAPSHOT
user=> (compile 'foo)
foo

classes目录中,您现在将拥有一堆.class文件。要从命令行调用您的代码(-main默认运行该函数):

$ java -cp ~/path/to/clojure.jar:./classes foo
Hello, command line.

clojure.org上有很多关于编译 Clojure 代码的信息。

于 2009-06-10T08:18:39.363 回答
1

我对 Clojure 很陌生,但我认为关于 Clojure 组的讨论可能是一种解决方案和/或解决方法,特别是 Stuart Sierra 于 4 月 17 日晚上 10:40 发表的帖子。

于 2009-06-10T02:28:39.617 回答
1

在 Common Lisp 中,您可以使用带有features的条件阅读。

#+testing (run-test 'is-answer-equal-42)

如果绑定到cl:*features*的功能列表将包含符号 :testing ,则只会在加载期间读取并执行上述内容。

例如

(let ((*features* (cons :testing *features*)))
   (load "/foo/bar/my-answerlib.lisp"))

将暂时添加 :testing 到功能列表中。

您可以定义自己的功能并控制 Common Lisp 系统读取和跳过哪些表达式。

此外,您还可以执行以下操作:

#-testing (print '|we are in production mode|)
于 2009-06-10T04:33:24.633 回答
1

http://rosettacode.org/wiki/Scripted_Main#Clojure上还有一个不同可能性的列表。(如果你找到一个新的 - 请添加它。;-))

于 2011-05-19T16:17:58.323 回答
0

你可能想看看 clojure-contrib 的test-is库。这不是同一个习惯用法,但它应该支持非常相似的工作流程。

于 2009-06-10T04:17:37.360 回答
0

Common Lisp 和 Clojure(以及其他 lisp)提供了与 REPL 交互的环境,你不需要像 « if __name__ == '__main__'» 这样的技巧。python 有类似 REPL 的环境:命令行中的 python、ipython、Emacs 的 python 模式等。

您应该只创建库,向其中添加一个测试套件(Common Lisp 有很多测试框架;我更喜欢5am框架,这里有一个框架调查。然后你加载库,在 REPL 中你可以对库做任何事情:运行测试、调用函数、实验等。

当您发现一个失败的测试时,您对其进行修复,重新编译更改的代码,然后继续试验,运行测试而无需重新启动整个应用程序。这节省了很多时间,因为正在运行的应用程序可能已经积累了很多状态(它可能已经创建了 gui 窗口,连接到数据库,到达了一些不容易重现的关键时刻),并且您不必重新启动它每次更改后。

这是 Common Lisp 的示例(来自我的 cl-sqlite 库):

编码:

(def-suite sqlite-suite)

(defun run-all-tests ()
  (run! 'sqlite-suite));'

(in-suite sqlite-suite)

(test test-connect
  (with-open-database (db ":memory:")))

(test test-disconnect-with-statements
  (finishes
    (with-open-database (db ":memory:")
      (prepare-statement db "create table users (id integer primary key, user_name text not null, age integer null)"))))
...

和互动环节:

CL-USER> (sqlite-tests:run-all-tests)
.......
 Did 7 checks.
    Pass: 7 (100%)
    Skip: 0 ( 0%)
    Fail: 0 ( 0%)

NIL
CL-USER> (defvar *db* (sqlite:connect ":memory:"))
*DB*
CL-USER> (sqlite:execute-non-query *db* "create table t1 (field text not null)")
; No value
CL-USER> (sqlite:execute-non-query *db* "insert into t1 (field) values (?)" "hello")
; No value
CL-USER> (sqlite:execute-to-list *db* "select * from t1")
(("hello"))
CL-USER> 

现在假设我在 sqlite:execute-to-list 中发现了错误。我去这个函数的代码,修复错误并重新编译这个函数。然后我调用固定函数并确保它有效。内存数据库并没有消失,它与重新编译前的状态相同。

于 2009-06-10T06:10:12.303 回答
0

Boot是一种构建工具(leiningen 的替代品),它支持脚本。所以你可以有一个以它开头的启动脚本,#!/usr/bin/env boot它可以有一个-main方法。

您还可以创建从命令行调用的任务,这些任务将调用代码的不同功能。您可以有一个打包任务,可以为这些功能之一创建一个 uberjar 作为入口点。

于 2016-02-10T15:19:51.427 回答
-3

如果您正在谈论拥有“入口点”,您当然可以这样做:

(ns foo)

(defn foo [n]
  (inc n))

(defn main []
  (println "working")
  (println "Foo has ran:" (foo 1)))

(main)

现在会发生的是,只要这段代码是 (load-file "foo.clj")'d 或 (use 'foo) 或 (require 'foo),那么 (main) 将被调用,这通常不会完成。

更常见的是,可以在 REPL 中加载代码文件,然后用户调用主函数。

于 2009-06-18T04:11:40.377 回答