(cljs/run-at (JSVM. :all) "细说函数")

简介:

前言

 作为一门函数式编程语言,深入了解函数的定义和使用自然是十分重要的事情,下面我们一起来学习吧!

3种基础定义方法

defn

定义语法

(defn name [params*]
  exprs*)

示例

(defn tap [ns x]
  (println ns x)
    x)

fn

定义语法

(fn name? [params*]
  exprs*)

示例

(def tap
  (fn [ns x]
    (println ns x)
    x))

其实defn是个macro,最终会展开为fn这种定义方式。因此后面的均以fn这种形式作说明。

Lambda表达式

定义语法

#(expr)

示例

(def tap
  #(do
     (println %1 %2)
     %2))

注意:

  1. Lambda表达式的函数体只允许使用一个表达式,因此要通过special formdo来运行多个表达式;
  2. 入参symbol为%1,%2,...%n,当有且只有一个入参时可以使用%来指向该入参。

Metadata——为函数附加元数据

 Symbol和集合均支持附加metadata,以便向编译器提供额外信息(如类型提示等),而我们也可以通过metadata来标记源码、访问策略等信息。
对于命名函数我们自然要赋予它Symbol,自然就可以附加元数据了。
其中附加:privatedefn-定义函数目的是一样的,就是将函数的访问控制设置为private(默认为public),但可惜的是cljs现在还不支持:private,所以还是要用名称来区分访问控制策略。
示例:

;; 定义
(defn
 ^{:doc "my sum function"
   :test (fn []
             (assert (= 12 (mysum 10 1 1))))
   :custom/metadata "have nice time!"}
  mysum [& xs]
        (apply + xs))

;; 获取Var的metadata
(meta #'mysum)
;;=>
;; {:name mysum
;;  :custom/metadata "have nice time!"
;;  :doc "my sum function"
;;  :arglists ([& xs])
;;  :file "test"
;;  :line 126
;;  :ns #<Namespace user>
;;  :test #<user$fn_289 user$fn_289@20f443>}

若只打算设置document string而已,那么可以简写为

(defn mysum
  "my sum function"
  [& xs]
  (apply + xs))

虽然cljs只支持:doc

根据入参数目实现函数重载(Multi-arity Functions)

示例

(fn tap
  ([ns] (tap ns nil))
  ([ns x] (println ns x))
  ([ns x & more] (println ns x more)))

参数解构

 cljs为我们提供强大无比的入参解构能力,也就是通过声明方式萃取入参

基于位置的解构(Positional Destructuring)

;; 定义1
(def currency-of
  (fn [[amount currency]]
    (println amount currency)
    amount))

;; 使用1
(currency-of [12 "US"])

;; 定义2
(def currency-of
  (fn [[amount currency [region ratio]]]
    (println amount currency region ratio)
    amount))

;; 使用2
(currency-of [12 "US" ["CHINA" 6.7]])

键值对的解构(Map Destructuring)

;; 定义1,键类型为Keyword
(def currency-of
  (fn [{currency :curr}]
    (println currency)))

;; 使用1
(currency-of {:curr "US"})

;; 定义2,键类型为String
(def currency-of
  (fn [{currency "curr"}]
    (println currency)))

;; 使用2
(currency-of {"curr" "US"})

;; 定义3,键类型为Symbol
(def currency-of
  (fn [{currency 'curr}]
    (println currency)))

;; 使用3
(currency-of {'curr "US"})

;; 定义4,一次指定多个键
(def currency-of
  (fn [{:keys [currency amount]}]
    (println currency amount)))

;; 使用4
(currency-of {:currency "US", :amount 12})

;; 定义5,一次指定多个键
(def currency-of
  (fn [{:strs [currency amount]}]
    (println currency amount)))

;; 使用5
(currency-of {"currency" "US", "amount" 12})

;; 定义6,一次指定多个键
(def currency-of
  (fn [{:syms [currency amount]}]
    (println currency amount)))

;; 使用6
(currency-of {'currency "US", 'amount 12})

;; 定义7,默认值
(def currency-of
  (fn [{:keys [currency amount] :or {currency "CHINA"}}]
    (println currency amount)))

;; 使用7
(currency-of {:amount 100}) ;;=> 100CHINA

;; 定义8,命名键值对
(def currency-of
  (fn [{:keys [currency amount] :as orig}]
    (println (:currency orig))))

(currency-of {'currency "US", 'amount 12}) ;;=> US

可变入参(Variadic Functions)

通过&定义可变入参,可变入参仅能作为最后一个入参来使用

(def tap
  (fn [ns & more]
    (println ns (first more))))

(tap "user.core" "1" "2" "3") ;;=> user.core1

命名入参(Named Parameters/Extra Arguments)

 通过组合可变入参和参数解构,我们可以得到命名入参

(def tap
  (fn [& {:keys [ns msg] :or {msg "/nothing"}}]
    (println ns msg)))

(tap :ns "user.core" :msg "/ok") ;;=> user.core/ok
(tap :ns "user.core") ;;=> user.core/nothing

Multimethods

 Multi-Arity函数中我们可以通过入参数目来调用不同的函数实现,但有没有一种如C#、Java那样根据入参类型来调用不同的函数实现呢?clj/cljs为我们提供Multimethods这一杀技——不但可以根据类型调用不同的函数实现,还可以根据以下内容呢!

  1. 类型
  2. 属性
  3. 元数据
  4. 入参间关系

 想说"Talk is cheap, show me the code"吗?在看代码前,我们先看看到底Multimethods的组成吧
1.dispatching function
用于对函数入参作操作,如获取类型、值、运算入参关系等,然后将返回值作为dispatching value,然后根据dispatching value调用具体的函数实现。

;; 定义dispatching function
(defmulti name docstring? attr-map? dispatch-fn & options)

;; 其中options是键值对
;; :default :default,指定默认dispatch value的值,默认为:default
;; :hierarchy {},指定使用的hierarchy object

2.method
具体函数实现

;; 定义和注册新的函数到multimethod
(defmethod multifn dispatch-val & fn-tail)

3.hierarchy object
存储层级关系的对象,默认情况下所有相关的Macro和函数均采用全局hierarchy object,若要采用私有则需要通过(make-hierarchy)来创建。

还是一头雾水?上示例吧!
示例1 —— 根据第二个入参的层级关系

(defmulti area
  (fn [x y]
    y))

(defmethod area ::a
  [x y] (println "derive from ::a"))
(defmethod area :default
  [x y] (println "executed :default"))

(area 1 `a) ;;=> executed :default
(derive `a :a)
(area 1 `a) ;;=>derive from ::a

示例2 -- 根据第一个入参的值

(defmulti area
  (fn [x y]
    x))

(defmethod area 1
  [x y] (println "x is 1"))
(defmethod area :default
  [x y] (println "executed :default"))

(area 2 `a) ;;=> executed :default
(area 1 :b) ;;=> x is 1

示例3 -- 根据两入参数值比较的大小

(defmulti area
  (fn [x y]
    (> x y)))

(defmethod area true
  [x y] (println "x > y"))
(defmethod area :default
  [x y] (println "executed :default"))

(area 1 2) ;;=> executed :default
(area 2 3) ;;=> x > y

 删除method

;; 函数签名
(remove-method multifn dispatch-val)

;; 示例
(remove-method area true)

分发规则

 先对dispatching value和method的dispatching-value进行=的等于操作,若不匹配则对两者进行isa?的层级关系判断操作,就这样遍历所有注册到该multimethod的method,得到一组符合的method。若这组method的元素个数有且仅有一个,则执行该method;若没有则执行:default method,若还是没有则抛异常。若这组method的元素个数大于1,且没有人工设置优先级,则抛异常。
通过prefer-method我们可以设置method的优先级

(derive `a `b)
(derive `c `a)

(defmulti test
  (fn [x] (x)))

(defmethod test `a
  [x] (println "`a"))

(defmethod test `b
  [x] (println "`b"))

;; (test `c) 这里就不会出现多个匹配的method
(prefer-method `a `b)
(test `c) ;;=> `a

层级关系

 层级关系相关的函数如下:

;; 判断层级关系
(isa? h? child parent)
;; 构造层级关系
(derive h? child parent)
;; 解除层级关系
(underive h? child parent)
;; 构造局部hierarchy object
(make-hierarchy)

上述函数当省略h?时,则操作的层级关系存储在全局的hierarchy object中。
注意:层级关系存储在全局的hierarchy object中时,Symbole、Keyword均要包含命名空间部分(即使这个命名空间并不存在),否则会拒绝。

(ns cljs.user)

;; Symbole, `b会展开为cljs.user/b
(derive 'dummy/a `b)
;; Keyword, ::a会展开为cljs.user/:a
(derive ::a ::b)

另外还有parentancestorsdescendants

(derive `c `p)
(derive `p `pp)

;; 获取父层级
(parent `c) ;;=> `p
;; 获取祖先
(ancestors `c) ;;=> #{`p `pp}
;; 获取子孙
(descendants `pp) ;;=> #{`p `c}

局部层级关系

 通过(make-hierarchy)可以创建一个用于实现局部层级关系的hierarchy object

(def h (make-hierarchy))
(def h (derive h 'a 'b))
(def h (derive h :a :b))

(isa? h 'a 'b)
(isa? h :a :b)

注意:局部层级关系中的Symbol和Keyword是可以包含也可以不包含命名空间部分的哦!

Condition Map

 对于动态类型语言而言,当入参不符合函数定义所期待时,是将入参格式化为符合期待值,还是直接报错呢?我想这是每个JS的工程师必定面对过的问题。面对这个问题我们应该分阶段分模块来处理。

  1. 开发阶段,对于内核模块,让问题尽早暴露;
  2. 生产阶段,对于与用户交互的模块,应格式化输入,并在后台记录跟踪问题。
    而clj/cljs函数中的condition map就是为我们在开发阶段提供对函数入参、函数返回值合法性的断言能力,让我们尽早发现问题。
(fn name [params*] condition-map? exprs*)
(fn name ([params*] condition-map? exprs*)+)

; condition-map? => {:pre [pre-exprs*]
;                    :post [post-exprs*]}
; pre-exprs 就是作为一组对入参的断言
; post-exprs 就是作为一组对返回值的断言

示例

(def mysum
  (fn [x y]
      {:pre  [(pos? x) (neg? y)]
       :post [(not (neg? %))]}
      (+ x y)))

(mysum 1 1)  ;; AssertionError Assert failed: (neg? y)  user/mysum
(mysum -1 1) ;; AssertionError Assert failed: (pos? x)  user/mysum
(mysum 1 -2) ;; AssertionError Assert failed: not (neg? %))  user/mysum

 在pre-exprs中我们可以直接指向函数的入参,在post-exprs中则通过%来指向函数的返回值。
虽然增加函数执行的前提条件,而且可以针对函数的值、关系、元数据等进行合法性验证,但依旧需要在运行时才能触发验证(这些不是运行时才触发还能什么时候能触发呢?)。对动态类型语言天然编译期数据类型验证,我们可以通过core.typed这个项目去增强哦!

总结

 现在我们可以安心把玩函数了,oh yeah!
尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/7137597.html ^_^肥仔John

如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!

 

本文转自^_^肥仔John博客园博客,原文链接:http://www.cnblogs.com/fsjohnhuang/p/7137597.html,如需转载请自行联系原作者

 

相关文章
|
3月前
|
Java
多线程中的run方法和start方法有什么区别?
多线程中的run方法和start方法有什么区别?
|
9月前
|
C语言 C++
【C++】 --- 写个函数在main函数执行前先运行
【C++】 --- 写个函数在main函数执行前先运行
79 0
|
Java API
多线程中run()和start()的异同详细分析(全)
目录前言1. 定义2. 代码区别 前言 为什么不直接调用线程中的run方法,而要通过调用start方法才可以实现线程的异步执行互不干扰? run方法和start的调用方法区别在于哪? 相信看到此处的人们,都会有类似的疑问,这篇文章就为你打开新世界,铭记脑海中 1. 定义 查看其官方的api接口定义 start方法 “ “Causes this thread to begin execution; the Java Virtual Machine calls the run method of this t
128 0
多线程中run()和start()的异同详细分析(全)
|
负载均衡 算法 Java
bthread源码剖析(二): 工作窃取与TaskGroup的run_main_task()
上一篇文章,介绍了TaskControl(简称TC)的初始化逻辑、worker的基本概念,并引出了TaskGroup(简称TG)的主要函数:run_main_task()。在谈run_main_task()之前,我们先看一下TG的几个主要成员。
344 0
|
域名解析 网络协议 关系型数据库
无需修改代码,用 fcapp.run 运行你的 REST 应用
解锁客户不修改代码部署存量REST、网页应用场景,解决客户函数调用返回404问题,支持标准的网关产品与开源组件集成函数计算。
无需修改代码,用 fcapp.run 运行你的 REST 应用
|
Java Linux 程序员
Java的wait()、notify()学习三部曲之二:修改JVM源码看参数
线程同步相关的JVM源码分析系列之二,修改源码查看运行时的虚拟机参数
195 0
Java的wait()、notify()学习三部曲之二:修改JVM源码看参数
妹子问我为啥启动线程时使用 start 而不是 run
今天团队里面的妹子问阿粉,为什么在启动线程的时候,都使用 start 方法,而不是 run 方法呢 还好阿粉平时一直有在学习,要不真的被妹子问住了 在多线程中,如果想让一个线程启动,你使用的方法一定是 thread.start() 方法,而不是 thread.run() 方法(啥,你用的不是 thread.start() 方法?乖,你的打开方式不对哦,下次不要这样了 有没有疑惑,为什么每次我们都习惯调用 start() 方法,为什么不直接调用 run() 方法来启动线程呢? 而且如果去看源码的话,你会发现,在 thread.start() 方法中,其实最后还是调用了 thread.ru
妹子问我为啥启动线程时使用 start 而不是 run
|
Java 编译器 程序员
从JVM 源码看init和clinit到底什么区别
从JVM 源码看init和clinit到底什么区别
114 0
从JVM 源码看init和clinit到底什么区别
|
Java 调度
多线程 start 和 run 方法到底有什么区别?
昨天栈长介绍了《Java多线程可以分组,还能这样玩!》线程分组的妙用。今天,栈长会详细介绍 Java 中的多线程 start() 和 run() 两个方法,Java 老司机请跳过,新手或者对这两个不是很理解的可以继续往下看。