Web 上 Clojure 简介

Lisp 是那种让人要么爱要么恨的语言。算我一个 Lisp 爱好者。我在 MIT 本科学习期间被“洗脑”,认为 Lisp 是唯一“真正”的编程语言,其他任何语言都只是苍白的模仿。诚然,我在日常工作中使用了 Python 和 Ruby,但我常常希望有机会定期使用 Lisp。

在过去的几年里,一个实现这一目标的机遇之窗已经打开。Clojure,一种在 Java 虚拟机 (JVM) 上运行的现代 Lisp 变体,正在席卷编程界。它是一个真正的 Lisp,这意味着它拥有你想要和期望的所有优点:函数式编程范式、复杂数据结构的轻松使用,甚至像宏这样高级的功能。与其他 Lisp 不同,并且在很大程度上促成了它的成功,Clojure 构建在 JVM 之上,这意味着它可以与 Java 对象互操作,并且可以在许多现有环境中工作。

在本文中,我想分享一些我开始尝试使用 Clojure 进行 Web 开发的经验。虽然我预见在我的大部分专业工作中不会使用 Clojure,但我确实相信,不断尝试新的语言、框架和范式是有用且重要的。Clojure 以适当的比例结合了 Lisp 和 JVM,使其在某种程度上成为主流,这使得它不仅仅是一种很酷但实际上没有人用于任何实际用途的语言,而是更加有趣。

Clojure 基础知识

正如我上面提到的,Clojure 是基于 JVM 的 Lisp 版本。这意味着如果你要运行 Clojure 程序,你也需要一份 Java 副本。幸运的是,考虑到 Java 的普及程度,这在今天已经不是什么大问题了。Clojure 本身以 Java 归档 (JAR) 文件的形式出现,然后你可以执行它。

但是,考虑到你可能想要使用的 Clojure 包和库的数量,你最好使用 Leiningen,这是一个用于安装 Clojure 和 Clojure 相关包的包管理器。(这个名字来自故事《莱宁根对抗蚂蚁》,表明 Clojure 社区不想使用已建立的依赖管理系统 Ant。)你肯定会想要安装 Leiningen。如果你的 Linux 发行版尚未包含现代副本,你可以从 https://raw.github.com/technomancy/leiningen/stable/bin/lein 下载 shell 脚本。

执行此 shell 脚本,将其放在你的 PATH 中。下载 Leiningen jar 文件后,它将在你的 ~/.lein 目录(也称为 LEIN_HOME)中下载并安装 Leiningen。这就是你开始创建 Clojure Web 应用程序所需的一切。

安装 Leiningen 后,你可以创建一个 Web 应用程序。但是为了做到这一点,你需要决定使用哪个框架。通常,你使用 lein new 创建一个新的 Clojure 项目,或者命名你要处理的项目 (lein new myproject),或者命名你要复制的模板,然后命名项目 (lein new mytemplate myproject)。你可以通过执行 lein help new 或查看 https://clojars.org 站点(Clojure jar 文件和库的存储库)来获取现有模板的列表。

你还可以打开 REPL(读取-求值-打印循环)以便直接与 Clojure 通信。我不会在这里深入探讨所有细节,但 Clojure 支持你期望的所有基本数据类型,其中一些映射到 Java 类。Clojure 支持整数和字符串、列表和向量、映射(即字典或哈希)和集合。与所有 Lisp 一样,Clojure 通过将其放在括号内并将函数名称放在首位来指示你要评估(即运行)代码。因此,你可以说


(println "Hello")
(println (str "Hello," " " "Reuven"))
(println (str (+ 3 5)))

你还可以在 Clojure 中分配变量。关于 Clojure,需要了解的重要一点是所有数据都是不可变的。对于 Python 程序员来说,这在某种程度上是熟悉的领域,他们习惯于在语言中拥有一些不可变的数据类型(例如,字符串和元组)。在 Clojure 中,所有数据都是不可变的,这意味着为了“更改”字符串、列表、向量或任何其他数据类型,你实际上必须将同一个变量重新分配给新的数据片段。例如


user=> (def person "Reuven")
#'user/person

user=> (def person (str person " Lerner"))
#'user/person

user=> person
"Reuven Lerner"

虽然所有数据都是不可变的似乎很奇怪,但这往往会减少或消除大量并发问题。鉴于 Clojure 中转换现有数据的功能数量以及使用“def”定义事物的能力,使用起来也出奇地自然。

你还可以创建映射,这是 Clojure 对哈希或字典的实现


user=> (def m {:a 1 :b 2})
#'user/m

user=> (get m :a)
1

user=> (get m :x)
nil

你可以获取与键“x”关联的值,或者如果你不想返回 nil,则可以获取默认值


user=> (get m :x "None")
"None"

记住,你无法更改你的映射,因为 Clojure 中的数据是不可变的。但是,你可以将其添加到另一个映射中,其值将覆盖你的值


user=> (assoc m :a 100)
{:a 100, :b 2}

在深入探讨之前,我应该指出的最后一件事是,你可以(当然)在 Clojure 中创建函数。你可以使用 fn 创建匿名函数


(fn [first second] (+ first second))

上面定义了一个新函数,它接受两个参数并将它们相加。但是,通常最好将这些函数放入命名函数中,你可以使用 def 来完成


user=> (def add (fn [first second] (+ first second)))
#'user/add

user=> (add 5 3)
8

因为这很常见,所以你也可以使用 defn 宏,它将 deffn 组合在一起


user=> (add 5 3)
8

user=> (defn  add [first second] (+ first second))
#'user/add

user=> (add 5 3)
8
Web 开发

现在你已经了解了 Clojure 的基础知识,让我们考虑一下 Web 开发中涉及的内容。你需要有一个 HTTP 服务器来接受请求,以及一个程序(通常称为“路由器”)来决定为每个请求的 URL 执行哪些函数。然后,你想将数据返回给用户,通常以 HTML 文件的形式。

现在,各种 Web 应用程序框架以不同的方式处理这个问题。在最原始的框架中(例如,CGI 程序),“应用程序”实际上仅在单个程序上调用。像 Ruby on Rails 这样的大型框架处理所有这些部分,甚至允许你交换部分——并且这些部分的每个部分都是使用不同类的实例来完成的,这些实例处理适当的信息。

现在,虽然 Clojure 构建在 JVM 之上,并使用一些原始 Java 类作为其基本数据类型,但它不是一种面向对象的语言。Clojure 是一种函数式语言,这意味着所有上述步骤都将使用函数来处理——无论是你定义的函数还是 Web 框架为你定义的函数。

就本文而言,我将使用 Compojure,这是一个用于 Clojure 的简单 Web 框架。要创建一个基本的 Compojure 项目,你可以使用 Leiningen


lein new compojure cjtest

这可能会从 clojars(Clojure 库存储库)下载许多库,然后下载新的 Clojure 项目(“cjtest”)。完成后,你可以通过进入 cjtest 目录并执行以下命令来运行它


lein ring server

这将下载并安装存在的任何依赖项,然后启动一个简单的 HTTP 服务器,默认情况下在端口 3000 上。然后你可以访问 https://127.0.0.1:3000 上的这个简单应用程序,在那里你将收到来自 Compojure 的“Hello, world”的问候。

现在,这是如何工作的?Compojure 如何知道该怎么做?

答案在 Clojure 项目文件 (project.clj) 中。默认的通用 Compojure 项目是使用(当然!)Lisp 代码定义的。实际上,在 Lisp 世界中,使用 Lisp 代码作为数据是非常标准的。Leiningen 为我创建的默认项目如下所示


(defproject cjtest "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [compojure "1.1.5"]]
  :plugins [[lein-ring "0.8.5"]]
  :ring {:handler cjtest.handler/app}
  :profiles
  {:dev {:dependencies [[ring-mock "0.1.5"]]}})

这使用了 Clojure 的 defproject 宏,为其命名为“cjtest”并赋予版本号。然后有几个关键字,一种 Clojure 数据类型,用作(除其他外)映射中的键。因此,上面的示例向 Clojure 显示程序具有默认描述,并且它依赖于 Clojure 和 Compojure(并指定了每个的版本)。

第二个配置文件也用 Lisp 代码编写,位于 src/cjtest 目录中,名为 handler.clj。默认设置如下所示


(ns cjtest.handler
  (:use compojure.core)
  (:require [compojure.handler :as handler]
            [compojure.route :as route]))

(defroutes app-routes
  (GET "/" [] "Hello World")
  (route/resources "/")
  (route/not-found "Not Found"))

(def app
  (handler/site app-routes))

换句话说,如果有人请求“/”,它将返回“Hello World”。然后,为你可能想要提供的任何静态资源添加路由,这些资源应该位于 Clojure 项目的 resources 目录中。然后,为处理 404 错误添加未找到路由。

你可以使用以下命令运行这个惊人的新 Web 应用程序


lein ring server

如果你想知道“ring”在那里做什么,只需说 Ring 是 Clojure 库,它处理几乎所有与 Web 相关的活动。然而,它是一个低级库,它不充当实际的 Web 应用程序框架。它使用标准的 Jetty 系统用于 Java Web 应用程序,尽管你无需担心现阶段,因为 Leiningen 提供了自动化;当你运行 Ring 时,任何缺少的库或依赖项都将被下载。

当然,在运行上述命令后,如果你访问 /,你会从服务器收到“Hello world”消息,因为你已将 GET / 映射到一个字符串。如果你愿意,你也可以将其映射到一个函数,方法是在路由名称后添加方括号,然后添加函数体


(defroutes app-routes
  (GET "/" [] "Hello World")
  (GET "/fancy/:name" [name]
       (str "Hello, " name))
  (route/resources "/")
  (route/not-found "Not Found"))

你甚至可以添加 HTML 标签进行一些格式化


(defroutes app-routes
  (GET "/" [] "Hello World")
  (GET "/fancy/:name" [name]
       (str "<p><b>Hello</b>, " name "<p>"))
  (route/resources "/")
  (route/not-found "Not Found"))

请注意,在 Lisp 中,由于前缀语法,你无需使用 + 或任何其他运算符从三个(或任意数量)部分创建字符串。你只需添加额外的参数,用空格分隔,它们就会添加到 str 返回的新字符串中。

如果你不想内联你的函数,你总是可以在其他地方定义它,然后将其传递给 defroutes


(defn say-hello
  [req]
  (str "<p><b>Hello</b>, " 
  ↪(get (get req :route-params) :name) "<p>"))

(defroutes app-routes
  (GET "/" [] "Hello World")
  (GET "/fancy/:name" [name] say-hello)
  (route/resources "/")
  (route/not-found "Not Found"))

请注意,此函数 say-hello 接受一个参数(称为 req),这是一个映射的映射,其中包含与你收到的 HTTP 请求相关的所有数据。如果你想获取在路由中定义的 name 参数,你必须从传递给你的函数的 req 映射内的 route-params 映射中获取它。你可能需要探索 req 对象以了解 Compojure 正在传递什么以及如何使用此类数据。

更花哨

现在,Compojure 将自己宣传为 Web 应用程序的路由机制。这就提出了 Compojure 带来了哪些类型的模板的问题。答案是没有。没错,Compojure 本身希望你返回包含 HTML 的字符串,但如何制作这些字符串取决于你。

因此,你可以创建字符串,如上所示,以便返回一个值。或者,你可以使用模板引擎,在 Clojure 世界中,这仅仅意味着包含另一个依赖项并使用该依赖项定义的函数


(defproject cjtest "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [compojure "1.1.5"]
                 [hiccup "1.0.3"]]
  :plugins [[lein-ring "0.8.5"]]
  :ring {:handler cjtest.handler/app}
  :profiles
  {:dev {:dependencies [[ring-mock "0.1.5"]]}})

现在,在 handler.clj 文件中,你需要进行另一个更改,以便你可以访问“hiccup”HTML 生成系统


(ns cjtest.handler
  (:use compojure.core hiccup.core)
  (:require [compojure.handler :as handler]
            [compojure.route :as route]))

完成这些设置后,你现在可以使用函数和 Clojure 的向量语法创建 HTML


(defn say-hello
  [req]
  (html [:p [:b "Hello, "  
  ↪(get (get req :route-params) :name) ]]))

现在,如果你看到括号并说“哎呀,这就是我不喜欢 Lisp 的地方”,我会理解的。但是,如果你将此视为 Lisp 代码,并意识到如果你的模板是有效的 Lisp,则它们会自动成为有效的 HTML,那么你正在按照 Clojure 想要鼓励的方式思考——即尽可能多地使用 Lisp 数据结构,并将它们提供给知道如何使用它们的函数。

结论

正如你所看到的,Clojure 提供了 Lisp 结构的简洁性和语法,同时保持在 Java 和 JVM 的世界中。我的下一篇文章将更深入地探讨 Compojure,调查表单、数据库连接以及现代 Web 应用程序想要使用的其他问题。

资源

Clojure 语言的主页是 https://clojure.net.cn,其中包含大量文档。

你可以在 Leiningen 的主页上阅读更多关于它的信息:http://leiningen.org。同样,Compojure 的文档在其主页 http://compojure.org 和 Hiccup 的文档在 https://github.com/weavejester/hiccup

关于 Clojure 的两本好书是 Stuart Halloway 和 Aaron Bedra 撰写的 Programming Clojure(由 Pragmatic Programmers 出版)以及 Chas Emerick、Brian Carper 和 Christophe Grand 撰写的 Clojure Programming(由 O'Reilly 出版)。在过去一两年里,我都读过这两本书,并且因为不同的原因而喜欢它们,没有明显的偏好。

Reuven M. Lerner 是一位资深的 Web 开发人员,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp),并在 http://lerner.co.il/newsletter 发布免费的程序员每周新闻通讯。Reuven 的 Twitter 账号是 @reuvenmlerner,与妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论