Clojure中相当于Python惯用语句 "if __name__ == '__main__'" 的是什么?

25

我正在涉猎Clojure,并且在尝试确定Clojure(和/或Lisp)中这个常见的Python习惯用法的等效方式时遇到一些小问题。

这种习惯用法是在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 myodule.py 运行它。

在Clojure(和/或common lisp)中是否有等效的习惯用语?我不需要一个完整的单元测试库(好吧,我需要,但不是在这个问题中),我只想在一些特定情况下运行模块中的代码,这样我就可以快速运行我正在开发的代码,但仍然允许我的文件像正常模块/命名空间一样被导入。

8个回答

28

在命令行中反复运行Clojure脚本并不符合惯用语法,REPL是更好的命令行。作为一种Lisp语言,通常会启动Clojure并保持同一实例永久运行,并与其交互而不是重新启动它。您可以逐个更改运行实例中的函数,根据需要运行它们并对它们进行调试。摆脱繁琐缓慢的传统编辑/编译/调试周期是Lisp的一个伟大特性。

您可以轻松编写函数来执行例如运行单元测试等操作,只需在需要运行它们时从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=>

如果您想了解这是如何工作的,请查看Clojure源代码中的src/clj/clojure/main.clj

另一种方法是将您的代码编译成.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代码的大量信息。


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|)

我认为特性不适合这个。 特性显示可用的功能,而不是某个环境状态或请求运行代码。 - dmitry_vk
为什么不呢?特性可用于各种用途:描述运行该程序的硬件,一些核心库的可用性,软件的某些模式,Lisp实现的版本,语言的版本,是否处于:production-mode或:development-mode等。 - Rainer Joswig

1

1

我对Clojure非常不熟悉,但我认为Clojure群组上的这个讨论可能是一个解决方案或者变通方法,特别是Stuart Sierra在4月17日晚上10点40分发布的帖子。


0

Common Lisp和Clojure(以及其他Lisp)提供了带有REPL的交互式环境,您不需要像“if __name__ == '__main__'”那样的技巧。对于Python,也有类似REPL的环境:命令行中的Python、IPython、Emacs的Python模式等。

您只需创建库,为其添加测试套件(Common Lisp有许多测试框架;我更喜欢5am框架,这里有可用框架的调查here)。然后,您加载库,在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中发现了一个bug。我进入这个函数的代码,修复了这个bug并重新编译了这个函数。然后我调用修复后的函数并确保它能正常工作。内存数据库没有消失,它的状态与重新编译之前相同。

3
name=='main'习语实际上与REPL没有任何关系 - 它是一种区分“作为模块导入”和“作为脚本运行”的方法。其中的代码通常不是您在REPL中尝试的琢磨和试验代码,而是您想要重复执行完全相同的代码。测试代码是一个例子,但通常最常见的是具有可重用性作为模块的脚本。 - Brian
是的,一般来说,它们是不同的东西。但在这个问题的背景下,检查 name 是用于运行(和重新运行)测试的,而在Lisp中,REPL是惯用的用例。 - dmitry_vk
用户要求使用name==main习语,而不是repl或测试套件。 - mcandre

0

Boot 是一种构建工具(与 Leiningen 相比的另一种选择),它支持脚本。因此,您可以编写一个以 #!/usr/bin/env boot 开头的 Boot 脚本,并且该脚本可以有一个 -main 方法。

您还可以创建从命令行调用不同代码函数的任务。并且您可以有一个打包任务,可以为其中一个这些函数作为入口点创建 uberjar。


0

你可能想要查看Clojure-contrib中的test-is库。虽然不是相同的习惯用语,但它应该支持非常相似的工作流程。


-3

如果您要谈论“入口点”,那么您肯定可以这样做:

(ns foo)

(defn foo [n]
  (inc n))

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

(main)

现在发生的是,每当这段代码被 (load-file "foo.clj")、(use 'foo) 或 (require 'foo) 调用时,就会调用 (main) 函数,这通常不会这样做。
更常见的情况是,在 REPL 中加载代码文件,然后用户将调用 main 函数。

能否以这样的方式完成,即只有在直接运行foo.clj时才触发(main),而不是在另一个脚本加载它时触发? - mcandre
我不这么认为,因为在这两种情况下,您都将评估(然后编译)所有表达式。始终存在AOT编译,它允许定义入口点:http://clojure.org/compilation - Chris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接