2014/11/3

scala: 不在迴圈中使用變數

通常在撰寫迴圈時,最直覺的寫法,就是帶入變數,隨時在迴圈中檢查變數的值,也可以利用變數控制,是否要跳出迴圈,但 functional style programming 的重點,除了希望 programmer 能用更易讀的方式撰寫程式,同時希望程式的效能,可以得到顯著的提昇,因此消滅不必要的迴圈與變數,成了 functional programming 的另一項重要任務,而 java programmer 要學習的,是忘記那些 OO Design Patterns,讓程式碼更精簡。

Java 的定位是商用語言,以往的歷史也證實,在 Server Side 的運算環境中,採用 Java J2EE solution 是個很好的選擇,但也因為是 Server Side 的語言,java programmer 比較不在意使用了多少變數,消耗了多少記憶體,著眼點通常放在要把功能做好,模組切割要合理,系統要穩定,只要選個好一些的 Server 多一些記體體就可以運作了。

Function Programming 讓我重新回到撰寫 C 語言程式碼的心情,C 語言追求卓越的速度,在意自己使用了多少記憶體,有時候甚至還要動用 assembly,相容於 Java 的 Scala 是個雙面人,我們該沿用 Java 帶來的物件導向分析習慣,將系統模組化,接下來在實作時,思考如何撰寫高效率的 FP 程式。

if expression

  var filename = "default.txt"
  if (!args.isEmpty)
    filename = args(0)

如果換個寫法,就可以不需要使用 var,程式也比較短,使用 val 也比較接近 functional style,要使用 variable 之前,要先想一下是不是可以 改寫成 expression,另外要盡可能使用 val,可讓程式更容易 refactor。

  val filename =
    if (!args.isEmpty) args(0)
    else "default.txt"

while loops

unit value 寫成 (),() 的存在,就是跟 java void 不同的地方,因為 greet() 會回傳 Unit, 所以 greet() 就會等於 ()。

  scala> def greet() { println("hi") }
  greet: ()Unit

  scala> greet() == ()
  hi
  res0: Boolean = true

以 java 的習慣,可能會這樣寫

  var line = ""
  while ((line = readLine()) != "")
    println("Read: "+ line)

但因為 line = readLine() 的結果是 () Unit,而 Unit 一定不會等於 "",所以就會永遠是 true

  var line = ""
  do {
    line = readLine()
    println("Read: "+ line)
  } while (line != "")

使用 recursion 取代 while

如果要計算 g.c.d 可以這樣寫

  def gcdLoop(x: Long, y: Long): Long = {
    var a = x
    var b = y
    while (a != 0) {
      val temp = a
      a = b % a
      b = temp
    }
    b
  }

改用 functional style: recursion 撰寫 gcd, 同時可以省去很多不需要的vars。

  def gcd(x: Long, y: Long): Long =
    if (y == 0) x else gcd(y, x % y)

generator

  val filesHere = (new java.io.File(".")).listFiles

  for (file <- filesHere)
    println(file)

file <- filesHere 的語法稱為 generator

如果要對一堆檔案重複進行某個運算,也許我們會很直覺地這樣寫

  for (i <- 0 to filesHere.length - 1)
    println(filesHere(i))

這種寫法的缺點是需要產生一個變數 i,因此這種寫法,在 scala 是不常見的,我們必須修改成,直接對 file collection 做 iteration,而 iteration 可能有下面四種問題:filtering、nested iteration、mid-stream variable binding、producing a new collection。

filtering

取得檔名是 .scala 結尾的 檔案

  val filesHere = (new java.io.File(".")).listFiles
  for (file <- filesHere if file.getName.endsWith(".scala"))
    println(file)

也可以使用兩個以上的 filter

  for (
    file <- filesHere
    if file.isFile
    if file.getName.endsWith(".scala")
  ) println(file)

nested iteration

在 for 裡面,也可以同時使用多個 generator

  def fileLines(file: java.io.File) = 
    scala.io.Source.fromFile(file).getLines().toList

  def grep(pattern: String) =
    for (
      file <- filesHere
      if file.getName.endsWith(".scala");
      line <- fileLines(file)
      if line.trim.matches(pattern) 
    ) println(file +": "+ line.trim)

  grep(".*gcd.*")

mid-stream variable binding

上面的程式碼,重複執行了 line.trim,如果想要只運算一次,就要產生一個變數 trimmed = line.trim,trimmed 用了兩次, 一次在 if, 一次在 println。

  def grep(pattern: String) =
    for {
      file <- filesHere
      if file.getName.endsWith(".scala")
      line <- fileLines(file)
      trimmed = line.trim
      if trimmed.matches(pattern)  
    } println(file +": "+ trimmed)

  grep(".*gcd.*")

producing a new collection

用 yield, 可以把過濾後的結果 array 紀錄起來,結果會得到 Array[File], 因為 filesHere 是 array, file 是 File。yield 必須放在 for 的外面,語法為 for clauses yield body。

  def scalaFiles =
    for {
      file <- filesHere
      if file.getName.endsWith(".scala")
    } yield file

try - catch - finally

在 finally 中關閉 file, 確保檔案一定有被關閉,scala 可以在 finally 裡面 return value, 但會把結果覆蓋掉。

  val file = new FileReader("input.txt")
  try {
    // Use the file
  } finally {
    file.close()
  }

match expression

類似 java 的 switch,預設是 _ ,每個 case 裡面不需要寫 break,不只可以用 int, enum, 也可以用 string,可直接將 match 結果,儲存到變數中。

  val firstArg = if (!args.isEmpty) args(0) else ""

  val friend =
    firstArg match {
      case "salt" => "pepper"
      case "chips" => "salsa"
      case "eggs" => "bacon"
      case _ => "huh?"
    }

  println(friend)

no break and continue

因為 scala 沒有 break 跟 continue,最簡單的方式,就是把 continue 換成 if,把 break 換成 boolean 變數。

  var i = 0
  var foundIt = false

  while (i < args.length && !foundIt) {
    if (!args(i).startsWith("-")) {
      if (args(i).endsWith(".scala"))
        foundIt = true
    }
    i = i + 1
  }

更好的寫法是 recursive 呼叫 seachFrom

  def searchFrom(i: Int): Int =
    if (i >= args.length) -1
    else if (args(i).startsWith("-")) searchFrom(i + 1) 
    else if (args(i).endsWith(".scala")) i
    else searchFrom(i + 1)

  val i = searchFrom(0)

Variable scope

scala 的 scoping rule 跟 java 大部分都一樣
只有一點不同,scala 可在 nested scopes 中定義同樣的 variable name。

  val a = 1;
  {
    val a = 2 // Compiles just fine
    println(a)
  }
  println(a)

但是在 scala interpreter 中,看起來雖然像是同一個 scope,但實際上是不同的,因此在 interpreter 中,變數可重複定義。

  scala> val a = 1
  a: Int = 1

  scala> val a = 2
  a: Int = 2

  scala> println(a)
  2

Reference

Programming In Scala by Martin Odersky, Lex Spoon, and Bill Venners