Clojure
Clojure
因为工作需要(写 jepsen test),不得不学习这门语言。这是一门 lisp 系纯函数式的语言。我还没忘记第一次读 SICP 时 lisp 给我带来的伤害,看起来 Clojure 也是括号满地,但是事到如今也只能硬着头皮上了。
学习
- learnxinyminutes:学语言首选,并且它下面也推荐了几个其他网址,方便快速入门,我觉得挺好的。
- official quickref:肯定少不了来这里查。可以学到许多 builtin functions。
- Solving Problems the Clojure Way - Rafal Dittwald:函数式思想入门,茅塞顿开
- Clojure 风格指南
- Writing Macros - Clojure for the Brave and True
开发环境
vscode 安装 Calva Spritz 即可。
Ctrl + Shift + P
,create a mini clojure project。然后点击下方的 REPL(一个可以实时查看执行结果的东西),打开(Jack-in)即可。一般选择 Babashka,因为其他两个都打不开。
用法只需要掌握:在当前代码行上,按 Alt + Enter
执行单句。如果有调用前面的函数,先按 Ctrl + Shift + c, Enter
引入依赖,再执行单句。(什么邪恶快捷键)
更多可以看The Top 10 Calva Commands。
语言基础
零散知识点。
- function 隐式命名规则:
- 后面加 v 指返回 vector(
[]
),例如filterv
,mapv
。
- 后面加 v 指返回 vector(
- list 和 vec 的区别:list 的尾部是前面,vec 尾部是后面。许多函数例如
conj
,peek
,pop
都遵循这个规律。正常人都会用 vector 吧。(peek '(1 2)) ; => 1 (peek [1 2]) ; => 2
- 但是有例外,
last
不遵循此规律。。
(last '(1 2)) ; => 2
- 但是有例外,
->
和->>
的区别:添加的位置不同。->
加到调用链每一环函数参数首位,->>
加到调用链每一环参数末尾。(-> 1 (func x y)) ; (func 1 x y) (->> 1 (func x y)) ; (func x y 1)
- apply 可以实现函数参数解包,例如这两个是等价的。
(defn wrap [x y z] (myfunc x y z)) (defn wrap [& args] (apply myfunc args))
- if 内可以判断很多类型,常见的
[]
()
都能判 false - peek 是 last 的高速代替
- identity 代表返回自身的函数,用于 filter 等。但是不允许用
#(%)
。
和其他语言对比:
- loop - recur 就是 for - continue
- defprotocol 就是定义 interface,defrecord 就是 class
- mapcat 就是 flatmap
- distinct 就是 unique 列表去重
- 没有 zip function,要自己写。
(defn zip "[1 2 3] [4 5 6] => [[1 4] [2 5] [3 6]" [m] (apply map vector m))
- 没有 filter-map function:
(defn filter-map [f coll] (filter identity (map f coll)))
项目管理
- filename 除去
.clj
后缀,不能包含.
。因为 ns 里的.
代表多一层 dir/
。 - 导入另一个文件的函数:想要运行这个需要用
(ns xxx.yyy) (require '[xxx.zzz :as myalias]) ; 也可以这样写 (ns xxx.yyy (:require [xxx.zzz :as myalias])) (myalias/myfunc 123)
clj
命令行,在 REPL 内运行只会报错。只有下面的:refer
形式可以在 REPL 用,不会炸。 - 引用标准库:
use :only
已经 deprecate 了,用 require。(ns chapter2.2-37U (:require [clojure.test :refer [deftest is]]) (:require [chapter2.2-36 :as acn]))
包管理
一般使用 deps.edn
作为包管理。project.clj
已经过时了。
{:deps
{cheshire/cheshire {:mvn/version "5.11.0"}
org.clojure/data.json {:mvn/version "2.5.0"}}}
使用 clj xxx.clj
运行程序时会自动下载依赖。依赖通过 maven 管理,下载位置在 ~/.m2
。
序列化
如果只是需要 RPC,不涉及其他语言,可以用 nippy,这是一个快速的 binary 序列化库。
如果需要 json 等标准格式,可以用官方的 clojure.data.json 或者 cheshire。
不过要注意,无论是 clojure.data.json 还是 cheshire,对 key type 的处理都是一坨大便。
(ns test
(:require [clojure.data.json :as json]))
(def my-data {:type :invoke, :f :txn, :value [[:w 2 1]], :time 3291485317, :process 0, :index 0})
(println my-data)
(def json-string (json/write-str my-data))
(println json-string)
(def deser-data (json/read-str json-string :key-fn keyword))
(println deser-data)
输出:
{:type :invoke, :f :txn, :value [[:w 2 1]], :time 3291485317, :process 0, :index 0}
{"type":"invoke","f":"txn","value":[["w",2,1]],"time":3291485317,"process":0,"index":0}
{:type invoke, :f txn, :value [[w 2 1]], :time 3291485317, :process 0, :index 0}
冒号被丢失了!
因此要解决这个问题,需要自行进行一些处理。
(ns test
(:require [cheshire.core :as json])
(:require [clojure.walk :as walk]))
(defn custom-serialize [data]
(json/generate-string
(walk/postwalk
(fn [x]
(if (keyword? x)
(str x)
x))
data)))
(defn custom-deserialize [json-str]
(let [data (json/parse-string json-str)]
(walk/postwalk
(fn [x]
(if (string? x)
(if (.startsWith x ":")
(keyword (subs x 1))
x)
x))
data)))
(def my-data {:type :invoke, :f :txn, :value [[:w 2 1]], :time 3291485317, :process 0, :index 0})
(println my-data)
(def json-string (custom-serialize my-data))
(println json-string)
(def deser-data (custom-deserialize json-string))
(println deser-data)
输出:
{:type :invoke, :f :txn, :value [[:w 2 1]], :time 3291485317, :process 0, :index 0}
{":type":":invoke",":f":":txn",":value":[[":w",2,1]],":time":3291485317,":process":0,":index":0}
{:type :invoke, :f :txn, :value [[:w 2 1]], :time 3291485317, :process 0, :index 0}
打包
将 clojure 打包为 jar 文件,方便调用。 假设我有一个 src/mytest.clj
:
(ns mytest
(:gen-class))
(defn -main
[& args]
(println (str "Hello, World!" args)))
老式:lein uberjar
官网有 tools.build Guide,但是讲得实在是抽象。因此我先尝试老式打包。
- project.clj
(defproject myproject "0.1.0" :description "A simple Hello World project" :dependencies [[org.clojure/clojure "1.11.4"]] ; 是必要的,否则找不到 main :main mytest :aot [mytest])
然后运行 lein uberjar
打包,java -cp target/myproject-0.1.0-standalone.jar clojure.main -m mytest
验证。也可以直接 java -jar target/myproject-0.1.0-standalone.jar
。
tools.build Guide,多看看,实际上没有太抽象,照着抄就完了。
deps.edn
{:paths ["src"] ;; project paths :deps {} ;; project deps :aliases {;; Run with clj -T:build function-in-build :build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.5" :git/sha "2a21b7a"}} :ns-default build}}}
build.clj
(ns build (:require [clojure.tools.build.api :as b])) (def lib 'my/lib1) (def version (format "1.2.%s" (b/git-count-revs nil))) (def class-dir "target/classes") (def jar-file (format "target/%s-%s.jar" (name lib) version)) (def uber-file (format "target/%s-%s-standalone.jar" (name lib) version)) ;; delay to defer side effects (artifact downloads) (def basis (delay (b/create-basis {:project "deps.edn"}))) (defn clean [_] (b/delete {:path "target"})) (defn jar [_] (b/write-pom {:class-dir class-dir :lib lib :version version :basis @basis :src-dirs ["src"]}) (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) (b/jar {:class-dir class-dir :jar-file jar-file})) (defn uber [_] (clean nil) (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) (b/compile-clj {:basis @basis :ns-compile '[mytest] :class-dir class-dir}) (b/uber {:class-dir class-dir :uber-file uber-file :basis @basis :main 'mytest}))
运行 clj -T:build clean && clj -T:build uber
即可。(clj -T:build jar
是来凑数的不用管)