zoukankan      html  css  js  c++  java
  • (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

  • 相关阅读:
    java static关键字的四种用法
    修改Intellij IDEA模板注解@author变量user内容
    iIDEA: 运行Scala代码右键没有Run选项
    idea 无法创建Scala class 选项解决办法汇总
    scala之idea下如何新建scala工程
    scala之windows环境安装与配置
    @RestController注解
    @component的作用详细介绍
    springboot 日期参数前后台转换问题
    MySQL数据库硬件选择
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/7137597.html
Copyright © 2011-2022 走看看