2014年12月21日 星期日

[Scala][教學] 神奇的Currying

        話先說在前頭,本篇文章跟NBA的Curry完全一點關係都沒有,標準圖文不符.最近在Coursera上按表操課上著scala的課程,每一堂十分鐘的課程真的從頭到尾要弄懂每個語法細節大概都要花上一個多小時,跟當初看著codecademy自學python順順走的經驗差超多Orz.所以做的分享也只能是很簡單的到處搜集資料的結果,離應用還差上超大一截.
        Currying這個特性,根據Wikipedia的解釋,是指將多個參數的方法,轉換成只有一個參數的方法(其他參數將另外指定).看起來很玄,一個簡單的範例(出自http://www.codecommit.com/blog/scala/function-currying-in-scala)像這樣.

  • 非Currying

def add(x:Int, y:Int) = x + y
 
add(1, 2)   // 3
add(7, 3)   // 10


  • Currying化
def add(x:Int) = (y:Int) => x + y
 
add(1)(2)   // 3
add(7)(3)   // 10


  • 更簡略的寫法

def add_(x: Int)(y: Int) = x + y
add_(7)(3) //10

透過這樣的寫法,可以讓每個方法更容易被其他方法所使用.
來看看以下這些範例,可以猜到結果會是如何嗎?
def add_7(x: Int) = add(7)(x)
add_7(3)

def add_7_1(x: Int) = add(7)
add_7_1(3)

def add_7_2 = add(7)
add_7_2(3)

其實我也不是很熟語法細節,只能靠實作來理解,以下是解答:
def add_7(x: Int) = add(7)(x)
add_7(3
// add_7[](val x: Int) => Int
// res1: Int =10

def add_7_1(x: Int) = add(7)
add_7_1(3)
// add_7_1[](val x: Int) => Int => Int
// res2: Int => Int => <function>

def add_7_2 = add(7)
add_7_2(3)
// add_7_2[] => Int => Int
// res3: Int = 10

  • 第一個add_7,吃一個Int(x),實作時會將他傳給定義中的(x),而(x)為add_7的第二個參數.
  • 第二個add_7_1,也是吃一個Int(x),雖然看起來跟第一個很像,但是實作時,x 傳不到等號右邊的定義裡(因爲沒有同名為x的參數);而add(7)雖然有給第一個參數,但是實作時沒有第二個參數,所以跑不出結果.
  • 第三個add_7_2,沒有設定參數,所以是沿用原本add的參數,因此實作時的參數(3),會直接傳給add(7),變成add(7)(3),跑出正確結果.
這是簡單的例子,稍微瞭解一下何謂Currying的概念,接著透過官網的例子,看一個比較複雜的應用(http://docs.scala-lang.org/tutorials/tour/currying.html
def filter(xs: List[Int], p: Int => Boolean): List[Int] =
  if (xs.isEmpty) xs
  else if (p(xs.head)) xs.head :: filter(xs.tail, p)
  else filter(xs.tail, p)

def modN(n: Int)(x: Int) = ((x % n) == 0)

val nums = List(1, 2, 3, 4, 5, 6, 7, 8)

println(filter(nums, modN(2))) //List(2, 4, 6, 8)
println(filter(nums, modN(3))) //List(3, 6)

這個例子我真的看超久的,五行code看了快一小時吧Orz
這個例子有兩個方法:
  • modN:是一currying的方法,吃兩個變數n和x,方法會透過 (x % n ==  0)這個邏輯判斷,吐回一個Boolean值.
  • filter:主要的方法,吃一個List(xs)和一個function(p),這個function會吃Int然後吐出Boolean值.內容會去將List的第一個數(xs.head),傳到function(p)中,判斷是否為真,如果為真,會將xs.head,接到(::)後面的陣列.這個方法難懂得在於,這邊還有使用遞迴的技巧.後面的陣列(filter( xs.tail, p)是一個遞迴函數,將剛剛除了xs.head,剩下的陣列(xs.head),又放回filter中再做一次篩選.
  • 最後來看如何使用這兩個函數:filter(nums, modN(2)),filter第一個參數吃的是陣列,沒有問題.接著吃的函數為modN(2)這個currying函數.先前提到Currying函數的特色就是將多個參數轉成單一參數的函數.原本modN有(n)和(x)兩個函數.在帶入filter時,modN(2)代表了modN(2)(x)這個函數,讓filter來使用.
光解釋以上觀念也打了一個多小時Orz...希望以後更加活用這個概念.



沒有留言:

張貼留言