November 4, 2018
By: Kevin

Clojure,你成功引起了我的注意

应该是在2012年,我还是个Haskell的粉丝,看过一些lisp的入门(因为使用emacs,得写些elisp配置文件),对lisp有这个语言括号很多而且性能很差的认知。当时在YouTube上看一个clojure的视频

视频里的人说:

如果你已经会编程了,这不是你的优势,而只会是障碍,你需要改掉一些坏毛病

我去...哥也是博通n种语言的人,c、c++、Java、python之类都是老伙计了。

我们会学习Clojure,一门lisp

语气神态都让我想起圣经上说“你不可有别的神”,记得一开始他写了个递归

(defn fact [x]
  (if (zero? x)
    1
    (*' x (fact (dec x)))))

; 注意(clojure.core/*')是一个函数支持自动升级,integer溢出后自动升级为BigInteger

我心里说:”这玩意性能很差呀,而且可会栈溢出”,果然他接着用substitution model展开了这段执行代码

由于Clojure是基于immutable data structure的,可能执行起来是这样的:

(fact 5)
(* 5 (fact 4))
(* 5 (* 4 (fact 3)))
(* 5 (* 4 (* 3 (fact 2))))
(* 5 (* 4 (* 3 (* 2 (fact 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1 (fact 0))))))
(* 5 (* 4 (* 3 (* 2 (* 1 1)))))
(* 5 (* 4 (* 3 (* 2 1))))
(* 5 (* 4 (* 3 2)))
(* 5 (* 4 6))
(* 5 24)
120

这个递归的过程,每一步都依赖下一步的结果,的确性能不高

这个就有点意思了,一般lisp由于递归都性能不高这个事实大家都略过不提,这个问题是可解决的?

clojure 可以用recur实现尾递归

(defn fact [x]
  (loop [accum 1
         n x]
    (if (= n 1)
      accum
      (recur (*' accum n) (dec n)))))

所以它的执行是这样的:

  (fact 5)
  (iter 5 1)
  (iter 4 5)
  (iter 3 20)
  (iter 2 60)
  (iter 1 120)
  (iter 0 120)
  120

请大家注意,在这个过程中,我们并没有一堆等待处理的压栈,每一步计算得到的accm都是下一步的结果,直到第n步,n等于1的时候,我们就得到最终结果

这个其实挺自然的,除了‘没有压栈’这部分,不论是谁设计的这个语言,至少他/她把新能放在心上。

至此,虽然我还有疑问,这个语言成功的引起了我的注意,接下来,他介绍了函数式编程中高阶函数(参数是函数的函数)的概念,这个对我并不陌生,但是他的介绍角度很新颖。

有oo编程背景的人一般会有这样的疑问:你不能调用一个还没写的函数,如果写了,干嘛不去直接调用,把函数作为参数这件事情显得有点多余。

结下来他用微积分举了个例子

对于任意的函数的导数

$$D f(x) = f'(x) = \frac {f(x + d(x)) -f(x)} {dx}$$

我们可以做如下定义:

(def dx .0001)

(defn deriv [f]
  (fn [x]
    (/ (- (f (+ x dx)) (f x))
       dx)))

(defn cube [x]
  (* x x x ))

运行的结果:

user> ((deriv cube ) 10)
300.0030000089282
user> ((deriv cube ) 2)
12.000600010022566
user> ((deriv cube ) 3)
27.00090001006572
user> ((deriv cube ) 4)
48.00120000993502

大学的导数公式还没全忘的话,你会发现这个值很接近 $3x^2$, !我可以对任意的数学公式进行求导了

事后我真的去进行了一番验证,没栈溢出。

  • 性能?这个语言设计中对性能是有充分考量的
  • 难懂?它能用四行代码表达微积分的精髓

Clojure,你成功引起了我的注意!

Tags: clojure