`
songry
  • 浏览: 83412 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

Clojure-JVM上的函数式编程语言(3) 函数定义和java交互 作者: R. Mark Volkmann

阅读更多

 原帖地址:http://java.ociweb.com/mark/clojure/article.html#DefiningFunctions

 作者: R. Mark Volkmann

 译者: RoySong

 

函数定义

    使用defn宏可以创建一个函数,它的参数是函数名,可选的函数说明(用doc可以查看这个说明),

参数列表(vector,可以为空),以及函数体。函数体中最后一个表达式的值做为函数的返回值。

每个函数都会返回一个值,虽然这个值有可能是nil。例子如下:

(defn parting
  "returns a String parting"
  [name]
  (str "Goodbye, " name)) ; concatenation

(println (parting "Mark")) ; -> Goodbye, Mark

 

    函数必须在使用前先行定义,有时候由于一组函数相互调用而无法预先定义函数时,可以采用特殊form“declare”来预声明函数,declare一次可以声明多个没有具体实现的函数名。例子如下:

(declare function-names)
 

    采用defn-宏定义的函数是private的,这意味着它们仅能在被创建的命名空间中使用。其他定义private的宏,比如 defmacro-defstruct-,都在 clojure.contrib.def命名空间中。

 

    函数可以接收一系列的参数,其中可选参数必须出现在参数列表的末尾,它们以名字前面加&的形式出现在参数列表的末尾。例子如下:

(defn power [base & exponents]
  ; Using java.lang.Math static method pow.
  (reduce #(Math/pow %1 %2) base exponents))
(power 2 3 4) ; 2 to the 3rd = 8; 8 to the 4th = 4096

 

    函数可以拥有多个参数列表以及相应的函数体,每个参数列表必须包含不同的参数个数,这样就支持了基于参数的函数重载。通常在采用不同数量的参数调用同一个函数时为某些参数赋予初始值的场景下是非常有用的,例子如下:

(defn parting
  "returns a String parting in a given language"
  ([] (parting "World"))
  ([name] (parting name "en"))
  ([name language]
    ; condp is similar to a case statement in other languages.
    ; It is described in more detail later.
    ; It is used here to take different actions based on whether the
    ; parameter "language" is set to "en", "es" or something else.
    (condp = language
      "en" (str "Goodbye, " name)
      "es" (str "Adios, " name)
      (throw (IllegalArgumentException.
        (str "unsupported language " language))))))

(println (parting)) ; -> Goodbye, World
(println (parting "Mark")) ; -> Goodbye, Mark
(println (parting "Mark" "es")) ; -> Adios, Mark
(println (parting "Mark", "xy"))
; -> java.lang.IllegalArgumentException: unsupported language xy
 

    匿名函数没有名字,它们通常做为参数传递给一个实名函数。它们一般拥有一个简短的函数定义且仅用于一处。有两种方式定义匿名函数,如下:

(def years [1940 1944 1961 1985 1987])
(filter (fn [year] (even? year)) years) ; long way w/ named arguments -> (1940 1944)
(filter #(even? %) years) ; short way where % refers to the argument
 

    当采用fn定义匿名函数时,函数体可以包含任意数目的表达式。

 

    当采用#(...)这样的缩略方式定义匿名函数时,函数体仅能包含一个表达式。

如果需要使用多个表达式,可以采用do将多个表达式包含起来。

如果仅有一个参数,可以采用%来指定它。

如果拥有多个参数,可以采用%1,%2这样累加的方式来指定。例子如下:

(defn pair-test [test-fn n1 n2]
  (if (test-fn n1 n2) "pass" "fail"))

; Use a test-fn that determines whether
; the sum of its two arguments is an even number.
(println (pair-test #(even? (+ %1 %2)) 3 5)) ; -> pass

 

    java的方法可以基于参数类型重载,而Clojure的函数只能基于参数数量重载。不过,Clojure的多重方法(multimethods),可以基于任何东西重载。

 

    联合采用defmulti和 defmethod宏可以定义多重方法。 defmulti的参数是方法名和一个分发函数,分发函数的返回值将用于选择某个方法。 defmethod的参数是方法名,触发方法使用的分发值,参数列表以及方法体。其中有一个特殊的分发值 :default用于方法调用时没有任何分发值符合定义的情况。每个拥有同样多重方法名的 defmethod定义必须拥有相同数量的参数。多重方法接收到这些参数后会传给分发函数。

 

    下面是一个基于类型重载的多重方法例子:

(defmulti what-am-i class) ; class is the dispatch function
(defmethod what-am-i Number [arg] (println arg "is a Number"))
(defmethod what-am-i String [arg] (println arg "is a String"))
(defmethod what-am-i :default [arg] (println arg "is something else"))
(what-am-i 19) ; -> 19 is a Number
(what-am-i "Hello") ; -> Hello is a String
(what-am-i true) ; -> true is something else
 

    分发函数可以是任何函数,包含自定义函数,这个可能性是无止境的。举个例子,可以自定义一个分发函数检查参数并返回指定大小的关键字比如::small , :medium或者 :large。然后,每个关键字对应的方法可以根据具体的大小来执行业务逻辑。例子如下:

user> (defn size-of [arg]
	(cond
	  (> (count arg) 10) :large
	  (< (count arg) 10) :small
	  :else :medium))
user> (defmulti is-this-large size-of)
user> (defmethod is-this-large :large [arg] (println arg " is large"))
user> (defmethod is-this-large :medium [arg] (println arg " is medium"))
user> (defmethod is-this-large :small [arg] (println arg " is small"))
user> (is-this-large "1234567890") ;->1234567890  is medium
user> (is-this-large "12345678901") ;->12345678901  is large
user> (is-this-large "1234567") ;->1234567  is small
 

    下划线可以用作函数参数的占位符,因为这个参数不会被使用,所以不需要名字。这通常用在被传给其他函数的回调函数(callback function)上面,某个特别的回调函数或许不会使用所有接收到的参数。举个例子:

(defn callback1 [n1 n2 n3] (+ n1 n2 n3)) ; uses all three arguments
(defn callback2 [n1 _ n3] (+ n1 n3)) ; only uses 1st & 3rd arguments
(defn caller [callback value]
  (callback (+ value 1) (+ value 2) (+ value 3)))
(caller callback1 10) ; 11 + 12 + 13 -> 36
(caller callback2 10) ; 11 + 13 -> 24
 

    complement函数会根据已有函数生成一个新函数,但新函数的返回值跟原有函数的返回值逻辑相反。举个例子:

(defn teenager? [age] (and (>= age 13) (< age 20)))
(def non-teen? (complement teenager?))
(println (non-teen? 47)) ; -> true
 

    comp函数能够组合任意数量的函数成一个新函数,这些函数依照参数列表中从右向左的顺序依次调用。举个例子:

(defn times2 [n] (* n 2))
(defn minus3 [n] (- n 3))
; Note the use of def instead of defn because comp returns
; a function that is then bound to "my-composition".
(def my-composition (comp minus3 times2))
(my-composition 4) ; 4*2 - 3 -> 5
 

    partial函数根据已有函数生成一个新函数,这个新函数会为原函数的调用提供一个固定的初始化参数,

这叫做“局部应用”( partial application)。

举个例子,*是接收任意数量的参数,并返回它们相乘的结果。

假设我们需要一个新版本的乘法函数,需要把每次相乘的结果再乘以2:

; Note the use of def instead of defn because partial returns
; a function that is then bound to "times2".
(def times2 (partial * 2))
(times2 3 4) ; 2 * 3 * 4 -> 24
 

    下面是一些采用map和partial函数的有趣应用。我们将定义一个函数,它能够计算任意多项式,

根据传入的x值获得对应的导数。多项式通过vector装载系数来表示。

然后,我们定义函数并采用 partial来定义指定的多项式和它的导数。最后,我们采用这些函数来求值:

(defn- polynomial
  "computes the value of a polynomial
   with the given coefficients for a given value x"
  [coefs x]
  ; For example, if coefs contains 3 values then exponents is (2 1 0).
  (let [exponents (reverse (range (count coefs)))]
    ; Multiply each coefficient by x raised to the corresponding exponent
    ; and sum those results.
    ; coefs go into %1 and exponents go into %2.
    (apply + (map #(* %1 (Math/pow x %2)) coefs exponents))))

(defn- derivative
  "computes the value of the derivative of a polynomial
   with the given coefficients for a given value x"
  [coefs x]
  ; The coefficients of the derivative function are obtained by
  ; multiplying all but the last coefficient by its corresponding exponent.
  ; The extra exponent will be ignored.
  (let [exponents (reverse (range (count coefs)))
        derivative-coefs (map #(* %1 %2) (butlast coefs) exponents)]
    (polynomial derivative-coefs x)))

(def f (partial polynomial [2 1 3])) ; 2x^2 + x + 3
(def f-prime (partial derivative [2 1 3])) ; 4x + 1

(println "f(2) =" (f 2)) ; -> 13.0
(println "f'(2) =" (f-prime 2)) ; -> 9.0
 

    还有另一种方法来实现多项式函数(Francesco Strino提出的建议)。

对于一个拥有系数a,b,c的多项式,可以这样根据x来计算值:

%1 = a, %2 = b, result is ax + b
%1 = ax + b, %2 = c, result is (ax + b)x + c = ax^2 + bx + c

(defn- polynomial
  "computes the value of a polynomial
   with the given coefficients for a given value x"
  [coefs x]
  (reduce #(+ (* x %1) %2) coefs))
 

    memoize函数接收一个函数做为参数,并返回一个以映射形式储存了原函数参数以及对应返回值的新函数。

新函数采用映射可以在以相同参数调用函数时不必重复计算。这样能获得更好的性能,

但会占用更多的内存来储存映射关系。

 

    time宏对一个表达式求值,打印出表达式的执行时间,并返回执行的结果。接下来会使用它来对多项式的执行计时。

 

    下面的例子展示了缓存多项式函数:

; Note the use of def instead of defn because memoize returns
; a function that is then bound to "memo-f".
(def memo-f (memoize f))

(println "priming call")
(time (f 2))

(println "without memoization")
; Note the use of an underscore for the binding that isn't used.
(dotimes [_ 3] (time (f 2)))

(println "with memoization")
(dotimes [_ 3] (time (memo-f 2)))
 

    上面的代码执行结果如下:

priming call
"Elapsed time: 4.128 msecs"
without memoization
"Elapsed time: 0.172 msecs"
"Elapsed time: 0.365 msecs"
"Elapsed time: 0.19 msecs"
with memoization
"Elapsed time: 0.241 msecs"
"Elapsed time: 0.033 msecs"
"Elapsed time: 0.019 msecs"
 

    这个输出提供了很多观察结果,第一次对函数f的调用,“主调用”(priming call),比起其他调用来花费了相当长的时间。这时完全不管是否使用了缓存。第一次调用缓存方法比第一次对“非主调用”(non-priming call)花费的时间要长,因为缓存结果付出了开支。随后的缓存调用就要快得多了。

 

Java 交互

    Clojure程序可以调用所有的java类和接口,如同在java中一样,调用java.lang包下面的东西是不需要引入的。如果需要使用其他包中的java类或者接口,只需要指定对应的包或者采用 import函数来引入对应的类或者接口就可以了。示例如下:

(import
  '(java.util Calendar GregorianCalendar)
  '(javax.swing JFrame JLabel))
 

    同样可以参考ns宏中的 :import 指令,在之后的章节中会进行描述。

 

    有两种方式来使用java类中的常量,示例如下:

(. java.util.Calendar APRIL) ; -> 3
(. Calendar APRIL) ; works if the Calendar class was imported
java.util.Calendar/APRIL
Calendar/APRIL ; works if the Calendar class was imported
 

    在Clojure代码中调用java方法同样非常简单。因为这个缘故,Clojure并没有提供很多公用方法而是直接依赖于java方法。举个例子,Clojure并不提供对浮点数取绝对值的函数,因为java.lang.Math类中的abs方法已经提供了相同的功能。而另一方面, java.lang.Math类中的 max方法只提供对两个数取最大的功能,所以Clojure提供了可以获取多个数中最大数的max函数。

 

    有两种方式来调用java类中的静态方法,示例如下:

(. Math pow 2 4) ; -> 16.0
(Math/pow 2 4)
 

    有两种方式能调用构造函数来生成一个java对象,稍后有示例。

注意采用了def来对新生成的对象的引用保持了一个全局绑定,这并不是必要的。

这个引用可以用多种方式来保持,比如将它添加到某个集合中,或者将它传递给某个函数。

(import '(java.util Calendar GregorianCalendar))
(def calendar (new GregorianCalendar 2008 Calendar/APRIL 16)) ; April 16, 2008
(def calendar (GregorianCalendar. 2008 Calendar/APRIL 16))
 

    有两种方式来调用java对象的实例方法,示例如下:

(. calendar add Calendar/MONTH 2)
(. calendar get Calendar/MONTH) ; -> 5
(.add calendar Calendar/MONTH 2)
(.get calendar Calendar/MONTH) ; -> 7
 

    上面的例子中,通常建议优先采用方法名在第一位的调用方式。

而对象在第一位的调用方式通常在宏定义中使用,因为句点引用可以用来替换字符串连接。

在阅读过"Macros "章节后,这一段的意义会更加明确。

 

    可以采用宏来进行链式方法调用,能够将前一个方法调用的结果做为后一个方法调用的主体,示例如下:

(. (. calendar getTimeZone) getDisplayName) ; long way
(.. calendar getTimeZone getDisplayName) ; -> "Central Standard Time"
 

    同样,在clojure.contrib.core命名空间中的 .?.宏在链式调用方法时遇到任何方法返回null就能够中止调用并返回nil,这样就避免抛出 NullPointerException。

 

    doto函数用来调用同一对象中的多个方法,它返回的是第一个参数即被调用的目标对象。

这样就可以很方便地采用表达式来创建目标对象(参见在 "Namespaces "章节中创建JFrame GUI对象)。

示例如下:

(doto calendar
  (.set Calendar/YEAR 1981)
  (.set Calendar/MONTH Calendar/AUGUST)
  (.set Calendar/DATE 1))
(def formatter (java.text.DateFormat/getDateInstance))
(.format formatter (.getTime calendar)) ; -> "Aug 1, 1981"
 

   memfn宏能够扩展代码运行java方法做为第一类函数处理,这是采用匿名函数来调用java方法的替代方案。

当采用 memfn来调用拥有参数的java方法时,每个参数必须指定参数名。 这指明了方法被调用时的参数数量。

这些参数名可以任意指定,但必须保障它们都是独特的,因为在生成的代码中会采用这些名字。

下面的例子对第一个集合中的java对象(String)调用了substring实例方法,

将第二个集合中对应的元素(int)传递给了方法做为参数。

(println (map #(.substring %1 %2)
           ["Moe" "Larry" "Curly"] [1 2 3])) ; -> (oe rry ly)

(println (map (memfn substring beginIndex)
           ["Moe" "Larry" "Curly"] [1 2 3])) ; -> same

 代理

    proxy函数创建了一个继承指定java类以及(或者)实现了零个或者一到多个java接口的java对象。

这通常需要在监听对象上实现回调方法,这个监听对象必须实现一个指定的接口以便从其他对象处获取通知。

举个例子,参见本文结束处的 "Desktop Applications "章节,其中的对象继承了JFrame GUI类并实现了ActionListener接口。

 

线程

    所有的Clojure函数都实现了 java.lang.Runnable 接口和 java.util.concurrent.Callable 接口。

这使得它们能够很轻松地在新的java线程中执行。实例如下:

(defn delayed-print [ms text]
  (Thread/sleep ms)
  (println text))

; Pass an anonymous function that invokes delayed-print
; to the Thread constructor so the delayed-print function
; executes inside the Thread instead of
; while the Thread object is being created.
(.start (Thread. #(delayed-print 1000 ", World!"))) ; prints 2nd
(print "Hello") ; prints 1st
; output is "Hello, World!"

异常处理

    Clojure代码抛出的所有异常都是运行时异常(runtime exceptions)。Clojure代码中调用的java方法抛出的异常仍然是已检查异常(checked exceptions)。而Clojure中的特殊form:try,catch,finally,throw,在功能上和java当中的版本非常类似。示例如下:

 (defn collection? [obj]
  (println "obj is a" (class obj))
  ; Clojure collections implement clojure.lang.IPersistentCollection.
  (or (coll? obj) ; Clojure collection?
      (instance? java.util.Collection obj))) ; Java collection?

(defn average [coll]
  (when-not (collection? coll)
    (throw (IllegalArgumentException. "expected a collection")))
  (when (empty? coll)
    (throw (IllegalArgumentException. "collection is empty")))
  ; Apply the + function to all the items in coll,
  ; then divide by the number of items in it.
  (let [sum (apply + coll)]
    (/ sum (count coll))))

(try
  (println "list average =" (average '(2 3))) ; result is a clojure.lang.Ratio object
  (println "vector average =" (average [2 3])) ; same
  (println "set average =" (average #{2 3})) ; same
  (let [al (java.util.ArrayList.)]
    (doto al (.add 2) (.add 3))
    (println "ArrayList average =" (average al))) ; same
  (println "string average =" (average "1 2 3 4")) ; illegal argument
  (catch IllegalArgumentException e
    (println e)
    ;(.printStackTrace e) ; if a stack trace is desired
  )
  (finally
    (println "in finally")))
 

   上面代码产生的输出如下:

obj is a clojure.lang.PersistentList
list average = 5/2
obj is a clojure.lang.LazilyPersistentVector
vector average = 5/2
obj is a clojure.lang.PersistentHashSet
set average = 5/2
obj is a java.util.ArrayList
ArrayList average = 5/2
obj is a java.lang.String
#<IllegalArgumentException java.lang.IllegalArgumentException:
expected a collection>
in finally
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics