首页 > 其他分享 >Clojure概念简介

Clojure概念简介

时间:2022-09-18 11:26:33浏览次数:94  
标签:函数 level 简介 概念 线程 println Clojure name

故事要从<< 黑客与画家 >>这本书说起,这本书讲述了硅谷创业之父Paul Graham的创业故事和人生体会。其中最有感触的有几点:1. 财富是创造出来的,世界的财富是在渐进增长,钱只是交换财富的媒介,由政府背书,只有创造财富才能变得富有;2. 不要与固执的人辩论,询问你的观点时候,可以说你既不赞成也不反对,这与政治人物对舆论事件的回应是一样的道理。3. 真正重要的是做出人们需要的东西,而不是加入某个公司。上述几点都是我觉得有道理的,毕竟有些东西就是定理一样,能让自己快速认清楚所处的场景,时时刻刻提醒自己不要身陷其中。同时,书中作者极力推荐使用Lisp语言开发程序应用,因为创业时作者就是使用Lisp编写互联网应用获得成功后被yahoo收购的,总之就是很励志的样子。

某天早上地铁通勤看到手机里有一本<< Clojure for the brave and true >>的书,而书中的Clojure是一种能编译成为Java字节码并运行在Jvm上的Lisp方言,能无缝调用Java,语法简单强大,这让我觉得可以把这本书再看一遍加深印象。

程序调试

本文中程序的代码是在 https://www.mycompiler.io/new/clojure 上调试的,线上控制台可以快速验证代码运行结果是否符合预期。下图中左边用来输入表达式,右边控制台可以用来查看运行的结果:

函数式编程

所谓函数编程就是将一个个函数组装成程序的过程,有点类似于计算机通过0和1组合就能模拟出现实世界。在函数编程中,函数和变量处于相同的地位,既可以作为方法的参数又能作为方法的返回值。同时函数编程中有两个重要的概念:函数是否为“纯函数”以及数据类型的“不可变性”。

纯函数”可以理解为当传入相同的参数时无论调用多少次都会返回相同的结果,并且没有副作用(side effects),副作用指的是修改函数外部的状态或写文件之类的IO操作。以下就是一个“纯函数”的例子,假设每次传入name相同,无论调用多少次都是返回相同的结果,并且没有修改函数外部的状态或者发生IO操作。

;;; 纯函数示例
(defn callName [name](str "your name is " name))

不可变性”指的是字面量数据结构是不可修改的,只能通过新生成变量来反映状态的变化。在面向对象中,带有属性的对象在方法和类传输中每时每刻都可能在发生变化,即对象是可变的。而函数编程把对象的变化过程看成一系列连续不变的值组成,遇到变化就生成新的对象不修改原来对象的状态,即对象是不变的。函数编程中实现“不可变性”具体的做法就是利用编程语言中数据类型不能修改的特性和递归算法来实现的。、

如果能够很好地将以上两个概念运用到程序上时,程序会变得更加透明和稳定,在多线程环境中也能避免很多状态混乱的问题。Clojure属于函数式编程语言,自然将“纯函数”以及“不可变性”带在语言设计中,通过学习它帮我们写出更好的代码。坦白讲,学习一门编程语言基本都按照数据类型、数据结构、方法以及多线程的顺序循序渐进学习,下文开始介绍Clojure语言中的各种概念。

表达式(Forms)

Clojure的语法简单,和其他Lisp方言一样采用统一的结构。代码是由很多的forms(表达式)组合而成,每个forms都有一个对应的返回值,这里的forms指的是字面量数据结构和S-表达式。字面量数据结构是指数字、字符串、数组或者字典等数据类型,编译器能够很快的识别出它们。S-表达式是典型的 Lisp 表达式:首先取列表第一个元素为操作符,然后遍历所有剩下的元素,将它们作为操作数。代码示例如下:

;;; 字面量数据结构
"a"
1.2
[1 2 3]

;;; S-表达式(操作符 参数1 参数2 参数3)
(+ 1 2)

变量绑定

Clojure中声明变量与Java没有什么不同,它是使用def将值绑定到变量的,这里的值可以是数字、字符串等字面量的数据结构,声明绑定变量后就可以在S-表达式中使用它。与声明全局变量def相对的是let,它用来绑定局部变量。当存在局部变量名和全局变量名相同时,局部变量生效,全局变量失效:

;;;; def 全局变量
(def number 1)

;;; let 局部变量
(let [number (inc number)]
    (println (str "number is " number)))

控制流程

控制流程操作操作符主要有if、do和when,代码如下所示:

;;; ifelse表达式
(if true 
    (println "The weather is fine today!!") 
    (println "Bad weather today!!"))
;=> "The weather is fine today!!"

;;; do表达式能连续执行多个操作,
(do (print "cl") (print "o") (print "j") (print "u") (print "re") "clojure")
;=> "clojure"

(if true
  (do (println "Success!")
      "good return value")
  (do (println "Failure!")
      "bad return value"))
;=> "Success!"

;;; when表达式相当于do和if的组合,只不过没有false分支
(when true
  (println "Success!")
  "good return value")
;=> "Success!"

除了控制流程的代码外,循环也是必不可少的:

;;; 循环示例,循环打印3次后退出
(loop [interation 0]
    (println (str "Iteration " interation))
    (if(> interation 3)
    (println "Goodbye!")
    (recur (inc interation))))

;=> "Iteration 0"
;   "Iteration 1"
;   "Iteration 2"
;   "Iteration 3"
;   "Iteration 4"
;   "Goodbye!"

数据结构

Clojure中的数据结构和其他的编程语言并没有什么不同,只是在表达方式上不同而已。像Java中集合都是实现Iterator接口一样,Clojure的集合数据结构都可以看做是序列(Seq)的抽象。下表列举常用的数据结构:

数据结构(名称:关键字) 表达式 备注
关键字:Keywords :a 以分号开头的常量,值等于变量本身的常量
数组:Vector [a b c] 查询操作时间复杂度O(1),其他操作O(n)
链表:Lists `(a b c) 时间复杂度O(n),操作时从首结点开始遍历至尾节点
集合:Sets #{a b c} 不含重复元素,常用于去重
哈希表:Maps {:a 1 :b 2} 查询复杂度近似O(1),键值常用Keywords表示
序列:Seq (1 2 3) Clojure操作前会对集合做序列转换

Clojure把集合当成Seq处理,以下是示例的代码:

;;; 对数组中的数字依次加1
(map inc [1 2 3])
;=> (2 3 4)

;;; 对集合中的数字依次加1,只不过作用的数据类型不同,但set和list都实现了Seq抽象
(map inc #{1 2 3})
;=> (2 4 3)

;;; 比较数组和集合,它们被解析后都转换为序列Seq
(if (= (seq [1 2 3]) (seq '(1 2 3)))
    (println "they are the same!!")
    (println "they are difference!!"))
;=> they are the same!!

;;; into表达式可以将序列还原为本来的数据类型
(into [] (seq [1 2 3]))
;=> [1 2 3]

(into #{} (seq #{1 2 3}))
;=> #{1 3 2}

;;; 合并两个序列,返回原来的数据类型
(into [0] [1])
;=> [0 1]

;;; conj 添加元素至数组末尾
(conj [0] 1 2 3 4)
;=> [0 1 2 3 4]

函数声明

函数是都是由函数名称、参数、方法体和返回值组成的,示例代码如下:

;;; 定义一个echo函数
(defn echo
   "say hello to something"
   [something]
   (str "hello " something))

;;; 调用echo函数
(echo "world!")
;=> "hello world!"

除普通函数外,Clojure还提供匿名函数:

;;; 括号中的 (fn [name] (str "hello " name)) 就是匿名函数
(map (fn [name] (str "hello " name)) ["a" "b" "c"])
;=> ("hello a" "hello b" "hello c")

递归函数也是必不可少的,以下是一个对列表进行累加的递归函数:

;;; 对列表进行累加
(defn sum
  ([vals]
     (sum vals 0))
  ([vals accumulating-total]
     (if (empty? vals)
       accumulating-total
       (recur (rest vals) (+ (first vals) accumulating-total)))))
;;; 累加数组
(sum [1 2 3])
;=> 6

Clojure提供了很多集合函数,常用的有map、 reduce、concat、some、filter、reduce、apply等,它们在seq抽象的各种数据结构中表现的行为是一致的。

;;; map 依次对集合中的元素应用函数
(map inc [1 2 3])
;=> (2 3 4)

;;; reduce 传入一个聚合函数和列表,最后转聚合为一个结果
(reduce + [1 2 3])
;=> 6

;;; filter 过滤列表的数值,生成一个新的满足条件的Seq
(filter number? [1 "a" 3])
;=> (1 3)

;;; some 判断满足函数的元素是否存在,真则返回true,假则返回nil
(some string? [1 3])
;=> nil

;;; concat 合并两个序列
(concat #{1} [2 3])
;=> (1 2 3)

;;; apply 依次取出seq列表中的元素应用到函数上
(apply max [1 2 3])
;=> 3

并发编程

近年来CPU的时钟频率几乎没有增加,摩尔定律正在失效,与此同时双核甚至四核的电脑逐渐成为主流。作为开发我们不得不学习多线技术充分发挥计算机的性能。通过利用多个CPU核心增强程序性能的想法看起来很有吸引力,但多线程环境下的不确定性和管理状态让人难以捉摸,这些多线程的问题与电脑的硬件、操作系统和编程语言都可能有关系,排查起来不是那么简单。在Clojure中定义了多种工具解决多线程问题,以下介绍常用的API。

Future会立刻将任务运行在新建的线程而不阻塞主流程,它执行后会返回一个引用,通过使用deref或'@'符号可以获取结果。另外,future中的任务只会执行一次并将结果缓存起来,同时提供设置超时等待时间的功能。

;;; 执行future任务 2s 后打印
(future (Thread/sleep 2000)
        (println "print after 2 seconds"))
;=> "print after 2 seconds"

;;; 立刻打印
(println "print immediately")
;=> "print immediately"

;;; deref 和'@' 解引用future结果,(future (Thread/sleep 3000)(+ 1 1))只会运行一次
(let [result (future (Thread/sleep 3000)(println "Future执行1次")(+ 1 1))]
    (println "result deref is " (deref result))
    (println "result '@' is " (@result)))
;=> "Future执行1次"
;=> 2
;=> 2
 
;;; 等待10ms,如果超时则返回5作为结果。下面代码明显超时
(deref (future (Thread/sleep 1000) 0) 10 5)
;=> 5

;;; realized? 判断任务是否完成,结果为false
(realized? (future (Thread/sleep 2000)))
;=> false

Delays允许不立即运行任务,它和future一样任务只会执行一次并且将结果缓存起来,这对于多线程中要求某个操作只执行一次的场景非常好用:

;;; 下面遍历开启多线程打印用户,one-time-method只会被调用一次
(def users ["u1" "u2" "u3"])
(defn echo [a](println a))
(let [one-time-method (delay (println "Delays只会执行1次"))]
    (doseq [u users]
    (future (echo u)(force one-time-method))))
;=> "Delays只会执行1次"

Promise是先声明一个未来结果的凭证而不是先定义任务,主动方通过定义一个promise交给接收方,然后接收方等待未来的某个时点主动方将结果放到promise中,常用作线程之间同步的一种手段:

;;; 通过promise等待未来的结果
(def my-promise (promise))
(deliver my-promise (+ 1 2))
(println @my-promise)
;=> 3

;;; 以下是一个寻找幸运数字的多线程函数
(defn sleep1s 
    [number]
    (Thread/sleep 1000)
    number)

(defn luncky? 
    [number]
    (and (> number 99)
         (<= number 101) number))
(time
 (let [my-promise (promise)]
   (doseq [number [100 200 300]]
     (future (if-let [number (luncky? (sleep1s number))]
               (deliver my-promise number))))
   (println "My luncky number is:" @my-promise)))

; 运行结果
; My luncky number is: 100
; "Elapsed time: 1023.902918 msecs"

Channel管道主要用于线程之间存取消息,当线程从管道添加和获取消息时需要阻塞等待至条件满足(管道有空间时添加消息,管道有消息时获取消息)才能继续操作,这个过程有点类似于阻塞队列:

;;; 定义channel管道
(def echo-chan (chan)) 
;;; go在线程池中获取一个线程运行任务,阻塞等待至channel中有消息可以获取
;;; 线程池的大小等于2+主机cpu的数量
;;; >!和<!对应协程(协程停摆的时候可以切换重复使用线程),>!!和<!!对应线程(线程阻塞时只能等待)
(go (println (<! echo-chan)))
;;; 发送消息到channel中,使上面的go的线程打印信息
(>!! echo-chan "输入信号到go协程中") 
;=> "输入信号到go协程中"

;;; 定义channel缓存的大小为2,超过2个消息时阻塞
(def echo-buffer (chan 2)
(>!! echo-buffer "ketchup")
; => true
(>!! echo-buffer "ketchup")
; => true
(>!! echo-buffer "ketchup")
; This blocks because the channel buffer is full

Thread与future都可以用来创建线程,不同点在于thread返回channel,而future返回引用。一个使用thread的场景可能是:当你有一个需要长时间运行的任务,如果将这个任务放在go语句块中时,长时间的任务时就会占用线程池中有限的线程(之前说过线程池的数量等于2+cpu的数据量,那在双核计算机上线程池的数量就是4个),这样很容易会导致其他任务没有空闲的线程可以运行,造成任务阻塞,严重影响程序的性能:

;;; 创建线程
(thread (println (<!! echo-chan)))
(>!! echo-chan "mustard")
; => true
; => mustard

多线程中同样存在短路操作,即多个功能相似的接口中只要有一个满足条件就立刻返回,对于这种场景,core.async中也有相应的实现:

;;; 上传图片方法,(rand 100)模拟不同上传时间
(defn upload
  [headshot c]
  (go (Thread/sleep (rand 100))
      (>! c headshot)))

;;; 将最快完成的channel的结果打印出来
 (let [c1 (chan)
      c2 (chan)
      c3 (chan)]
  (upload "serious.jpg" c1)
  (upload "fun.jpg" c2)
  (upload "sassy.jpg" c3)
   (let [[headshot channel] (alts!! [c1 c2 c3])]
    (println "Sending headshot notification for " headshot)))
 ;=> "Sending headshot notification for serious.jpg"

原子变量

Java中的原子类在clojure中也有对应的体现,atoms引用类型(reference type)可用于拷贝变量,并且可以通过swap!转变为新的状态,内部通过cas做状态的转移,所以不用担心引用类型有线程的问题:

;;; 以下将map复制后绑定给变量meow
(def meow (atom {:name "cat"
                 :level 123}))
;;; 获取meow的值
@meow
;=> {:name cat, :level 123}

;;; swap!变换状态,内部使用cas确保变量是线程安全的
(swap! meow
       (fn [current-state]
         (merge-with + current-state {:level 10})))
;=> {:name cat, :level 133}

;;; reset!在不检查原来的状态进行重置
(reset! meow
      {:name "cat" 
       :level 0})
;=> {:name cat, :level 0}

Watches能根据前后状态的变化做不同的操作,以下是示例代码:

;;; 定义引用类型
(def meow (atom {:name "cat"
                 :level 123}))

;;; 判断等级是否大于520打印不同的信息
(defn meow-alert
  [key watched old-state new-state]
  (let [level (:level new-state)]
    (if (>= level 520)
      (do
        (println "Oh! nice cat"))
      (do
        (println "It's Ok! Can do better")))))

;;; 添加监控的方法
(add-watch meow :level meow-alert)

;;; 123 + 397 = 520,满足level大于等于520
(swap! meow update-in [:level] + 397)
;=> "Oh! nice cat"

;;; 复位为level=123,不满足level大于等于520
(reset! meow {:name "cat"
              :level 123})
;=> "It's Ok! Can do better"

除了监控变量前后状态变化外,还可以使用Validators检查新状态是否合法:

;;; 为meow设置一个校验函数,如果违反约束(level不在0和1000之间)则抛出异常
(defn meow-meow-level-validator
  [{:keys [level]}]
  (or (and (>= level 0)
           (<= level 1000))
      (throw (IllegalStateException. "That's too much!!"))))
  
(def meow (atom {:name "cat"
                 :level 123} 
             :validator meow-meow-level-validator))
 
;;; 123 + 1000 = 1123超过1000,所以会抛出异常
(swap! meow update-in [:level] + 1000)
;=> "That's too much!!"

atoms只能对单个变量进行修改,Clojure还能将多个变量放在一个事务里面同时修改,用么全部变量修改成功,用么都修改失败,可以类比成关系型数据库提供的ACID特性,区别在仅在于Clojure是在内存中实现的而已。以下是代码示例:

;;; 主线程打印的counter为0
;;; future开启一个新线程,并且在一个事务中对couner增加了2次,只有当整个事务结束时才会提交修改
;;; 处于同一个事物中的变量都做cas修改,如果其中一个失败则整个事务重试
(def counter (ref 0))
(future
  (dosync
   (alter counter inc)
   (println @counter)
   (Thread/sleep 500)
   (alter counter inc)
   (println @counter)))
(Thread/sleep 250)
(println @counter)

Clojure还提供了动态变量,用于不同上下需要变化的变量,可以看作可以修改的变量,与def定义的变量不能修改相对,因此使用起来更加灵活:

;;; 定义一个全局变量,并且可以动态绑定
(def ^:dynamic *notification-address* "[email protected]")
(binding [*notification-address* "[email protected]"]
  *notification-address*)

操作Java

由于Clojure程序最终是编译成字节码运行在Jvm上,所以Clojure提供了操作Java的类、对象以及方法的能力,以下是一些操作Java的实现代码:

;;; 转换为大写
(.toUpperCase "By Bluebeard's bananas!")
;=> "BY BLUEBEARD'S BANANAS!"

;;; 查找字符的位置
(.indexOf "Let's synergize our bleeding edges" "y") 
; => 7

;;; 创建字符串对象
(String. "Hello world!")
; => "Hello world!"

;;; 往堆栈中添加字符串
(java.util.Stack.)
; => []
(let [stack (java.util.Stack.)] 
  (.push stack "12")
  (.push stack "34")
  (.push stack "56")
  (println stack))
;=> [12, 34, 56]]

其他操作还有很多,感兴趣的朋友可以参考<< Clojure for the brave and true >>书中的第12章。

多态实现

Clojure利用multimemethods提供了方法的多态,即相同的函数可以根据传入的参数的不同调用不同实现:

;;; 根据传入参数的食物类型调用不同的函数
(defmulti eat (fn [food] (:food-type food)))
;;; 实现1
(defmethod eat :meat
           [food]
           (println (str "the meat is " (:name food))))
;;; 实现2
(defmethod eat :fruit
           [food]
           (println (str "The fruit is " (:name food))))

;;; 实现3,如果没有找到对应的实现,则会调用该方法兜底
(defmethod eat :default
           [food]
           (println (str "The food is " (:name food))))

;;; 调用实现1
(eat {:food-type :meat
      :name     "pork"
      :weight    500})
;=> "The fruit is pork"

;;; 调用实现2
(eat {:food-type :fruit
      :name     "apple"
      :weight    50})
;=> "The fruit is apple"

;;; 调用实现3
(eat {:food-type :rice
      :name     "rice"
      :weight    50})
;=> "The food is apple"

Clojure提供了在命名空间生效的协议 protocols,它相当于面向对象的接口,可以扩展类的实现:

;;; 定义协议
(ns data-psychology)
(defprotocol Psychodynamics
  "Plumb the inner depths of your data types"
  (thoughts [x] "The data type's innermost thoughts")
  (feelings-about [x] [x y] "Feelings about self or other"))

;;; 扩展Java对象类的实现
(extend-type java.lang.Object
  Psychodynamics
  (thoughts [x] "Maybe the Internet is just a vector for toxoplasmosis")
  (feelings-about
    ([x] "meh")
    ([x y] (str "meh about " y))))

总结

编程语言的概念都是相似的,Clojure简洁语法确实让人印象深刻,让注意力用在如何编写代码上,而不是迷失在种类繁多的API中。因为我没有使用过Clojure完成项目,不是很清楚Clojure开发大规模应用的实际情况,后面有机会再深入学习。本文只对<< Clojure for the brave and true >>书中的部分概念加以理解简介,如果您也对Clojure感兴趣的话可以到 https://www.braveclojure.com/clojure-for-the-brave-and-true 下载电子版PDF阅读。以上完,谢谢!

标签:函数,level,简介,概念,线程,println,Clojure,name
From: https://www.cnblogs.com/smkr/p/16695030.html

相关文章

  • JavaSe-day02-基本概念
    Java基本语法本章内容有注释,关键字,字面量,变量!注释什么是注释注释是在程序指定位置添加的说明性信息,就是对代码的一种解释。注释的分类1.单行注释格式://注释信息......
  • mongodb 基本概念
    文档是mongodb的最小数据集单位,是多个键值对有序租户在一起的数据单元,类似于关系型数据库的记录集合一组文档的集合,文档存放的是数据,集合内的结构是可以不同的,集......
  • 01-项目简介
    当你站在我的面前看我时,你知道我心里的悲伤吗商城模式B2C模式 项目技术&特色前后端分离开发,并开发基于Vue的后台管理系统springcloud全新的解决方案应用监......
  • django中的模板层简介
    1.什么是模板层模板层可以根据视图中传递的字典数据动态生产相应的HTML页面2.模板层的配置1.在项目下创建一个与同名文件夹平行的templates文件夹2.在settings.py中的T......
  • linux 信号概念
    信号的概念信号(Signal)是一种软件中断,比如Ctrl+C的退出命令实质上就是使用了信号。信号在Linux操作系统中提供了一种处理异步事件的方法,可以很好地在多个进程之间进行同步......
  • 第一章. 操作系统简介
    1操作系统的概念、功能和目标常见的操作系统:Windows,Android,iOS,MacOS,Linux1.1操作系统的概念操作系统的概念:1.管理协调硬件和软件等计算机资源2.为上层应用......
  • SDL开发库简介
    什么是SDLSimpleDirectMediaLayer是一个跨平台开发库,旨在通过OpenGL和Direct3D提供对音频、键盘、鼠标、游戏杆和图形硬件的低级访问。它被视频播放软件、模拟器和......
  • Lambda简介
     1、什么是Lambda?Lambda就是一个匿名函数。 2、为什么要使用Lambda?使用Lambda表达式可以对一个接口进行非常简洁的实现(如下图,分别是三种方式实现接口的对比)。 3......
  • 说说中国高校理工科教育中的基础概念混乱问题
    https://www.youtube.com/watch?v=7EXDp6c9n-Q&lc=Ugydwl8gppB5FWE8Y5V4AaABAg.9fcFRMWuCpg9fcrI479gE2      建议你好好研究清楚概念,ASIC是专用集成电路,GPU......
  • 反冲简介
    反冲简介畏缩是Facebook的React实验状态管理系统。它提供了几种开箱即用的功能,这些功能仅靠React⚛️很难实现。Recoil可让您创建一个数据流图,该图流自原子通......