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

2014/10/27

functional object in scala

通常認為 functional object 的特性,就是使用了 immutable object 的特性。

immutable vs mutable object

immutable objects 比 mutables obejcts 多了幾個優點, 但有一個缺點。

優點是

  1. immutable objects 比 mutable objects 容易理解,因為他們不會隨著時間改變。

  2. 可任意傳送 immutable objects,但如果是 mutable table,則可能需要複製一份,以防止被其他程式碼修改。

  3. 因為不能修改 immutable object 的內容,所以兩個 threads 不能同時存取一個 immutable object。這在 multi-thread 環境會很好用。

  4. immutable objects 可安全地用在 hash table key 裡面,才不會發生這種情況:當 mutable object 用在 HashSet,下次使用 HashSet 時,物件可能就不見了。

缺點是

  1. immutable objects 需要較大的一塊記憶體空間,進行物件複製。

製作一個 Rational Number Immutable Object

// primary constructor,需要兩個 整數,分別是分子與分母
class Rational(n: Int, d: Int) {

    // scala 會直接將 class body 裡面,不屬於任何 field/method 的程式碼
    // 直接放到 primary constructor

    // scala 限制更多, 只能讓 primary constructor,
    // 在第一行呼叫 superclass constructor

    // require 語法是 preconditions
    // 因為有理數的分母不能為 0,必須在建立物件時,加上欄位檢查
    require(d != 0)

    // 66/42 可約分成  11/7
    // 需要計算 gcd(66,42)   greatest common divisor
    private val g = gcd(n.abs, d.abs)

    // 外部可直接使用 r.numer, r.denom 兩個欄位的數值
    val numer = n / g
    val denom = d / g

    // auxiliary constructors
    // 5/1  原本要寫 Rational(5,1)   可省略成  Rational(5)
    // 任何 auxiliary constructor 都可以在第一行 呼叫其他 constructor
    def this(n: Int) = this(n, 1)

    // 有理數的加法,為了保持 immutable object 的特性
    // 運算後,回傳新的 Rational object
    // 定義 operator method
    def +(that: Rational): Rational =
        new Rational(
            numer * that.denom + that.numer * denom,
            denom * that.denom)

    def +(i: Int): Rational =
        new Rational(numer + i * denom, denom)

    def -(that: Rational): Rational =
        new Rational(
            numer * that.denom - that.numer * denom,
            denom * that.denom)

    def -(i: Int): Rational =
        new Rational(numer - i * denom, denom)

    def *(that: Rational): Rational =
        new Rational(numer * that.numer, denom * that.denom)

    // Rational 要處理 r*2 的運算,必須要對 * 作 overloaded
    def *(i: Int): Rational =
        new Rational(numer * i, denom)

    def /(that: Rational): Rational =
        new Rational(numer * that.denom, denom * that.numer)

    def /(i: Int): Rational =
        new Rational(numer, denom * i)

    // 列印物件時,會自動呼叫 toString,否則預設列印出物件的 reference 位址
    // 這裡是覆寫 override 上層的 toString
    override def toString = numer + "/" + denom

    // gcd 最大公因數 greatest common divisor
    private def gcd(a: Int, b: Int): Int =
        if (b == 0) a else gcd(b, a % b)
}

identifier in scala

有四種

  1. alphanumeric identifier
    以 或 a letter 開始, 後面可以是 letters/digits/,$ 也算是一個 character,但這是保留給 scala compiler 使用,建議不要使用 $ , 以免跟 compiler 產生的 id 有衝突。

  2. operator identifier
    包含 1~多 個 operator characters (ex: + : ? ~ # )
    ex: + ++ ::: <?> :->

  3. mixed ifentifier
    包含 a alphanumberic identifier, 後面是 , 然後是 operator identifier。
    ex: unary
    + myvar_=

  4. literal identifier
    用 (...) 包含在裡面的任意 string
    ex: x <clinit> yield
    yield 是 java Thread class 的 static method,但在 scala 不能寫 Thread.yeild(),因為 yield 是 scala 的保留字,所以要改寫成 Thread.yield()。

implicit conversion

上面的 Rational 可支援 r2,但是無法支援 2r 的計算,為了解決這個問題,scala 讓我們建立一個 implicit conversion,自動轉換 integer 為 rational number。

implicit conversion 定義有 scope 的限制,如果要在 scala interpreter 裡面使用,則必須在 interpreter 裡面執行 implicit conversion 的定義。如果把 implicit method 定義放在 Rational class 裡面,則對 inpterpreter 來說,是沒有作用的。

implicit def intToRational(x: Int) = new Rational(x)

測試

scala> val r = new Rational(2,3)
r: Rational = 2/3

scala> 2 * r
res0: Rational = 4/3

雖然 implicit conversion 很好用,但可能會造成程式可讀性降低。

scala 的程式特性是簡潔,但在簡化程式碼的時候,同時要考慮到 readable 與 understandable 的要求,才不至於寫出難以維護的程式碼。

2014/10/20

迷你書 阿里巴巴的技術力量

因應阿里巴巴在美國上市的重大消息,InfoQ 將一些對阿里巴巴工程師的專訪,集結成一本迷你書 阿里巴巴上市背後的技術力量,我們可以從這些互動的 FAQ 訪問中,了解到這個 公司的文化與態度。

阿里巴巴的目標,是要幫助大陸的中小企業,讓他們也能很快地使用網路平台進行銷售與交易,平台使用的關鍵問題在使用者,而且是關於賣方與買方雙方面的問題。

甚至連技術深度導向的核心業務優化團隊,同樣也要面對使用者,開發者必須要將自己的優化成果推廣出去,系統夠好也要有人使用,除非你開發的東西已經超過目前全人類的想像,已經開發到了未來。

在政治集權的市場中,建立互信金流機制

在阿里巴巴上市現場,記者提出對 trust 的疑問,阿里巴巴如何說服美國投資者,面對中共政權,如何讓投資者相信,這是一個可以被信任的企業。



馬雲說明,阿里巴巴為了提供一個讓中小企業可以使用的 ecosystem,必須跟中共政權對話,建立互信基礎,這是必要的,政府跟企業必須合作,才能在非常糟糕的信用卡市場中,完成一個網路交易平台。 Every trust takes time to build.

架構師的歷程

要成為一個架構師,更重要的學習是來自於實踐,所謂一個行業的專家指的並不是他能力有多強,是指他碰到過了這個行業裡面所有的問題,同時他解決了,他就能成為專家。

架構師不僅僅要了解技術,同時要進行業務分析,更全面地了解問題與方法,才能更容易去掌握問題的全貌。

淘寶的架構變革

  1. 2003/5 ~ 2004/5 LAMP
  2. 2004/2 ~ 2008/3 weblogic -> JBoss,開發了 TFS, iSearch, TDBM, CDN
  3. 2007/10 ~ 2009/11 系統走向產品化、服務化,支援大型團隊的並行開發,逐步模組化、中心化、可快速擴充、提昇可用性。非核心資料庫由 Oracle 移至 MySQL,建立訊息系統與服務框架,淘寶開放平台(TOP)上線
  4. 2009/8 ~ now 逐步提供系統自動化,減少操作失誤機率

資料庫的演進

阿里巴巴一開始是使用 Oracle,發展至今,已經慢慢地藉由拆分的方法,移轉到 MySQL,至於 NoSQL 則是依照業務的需求而使用,因為阿里巴巴的核心業務比較複雜,並不適合直接改用 NoSQL,而是在適當的業務場景中,採用 NoSQL。

當資料庫性能不足時,就會考慮使用 Cache,目前有集中式與分散式兩種 Cache 解決方案,也有使用 memory cache。數據資料由集中式演進為分散式,可跨多個 IDC 進行容錯備份,數據資料異地同步。

拆分資料庫有專責單位,最重要的任務是,如何在拆分的過程中,達成不間斷服務,平順地過渡到新架構上。第一次拆分最大的 table,花了兩年的時間,現在進步到2~3個月。

企業訊息系統

訊息系統對阿里巴巴來說,是非常重要的,但是阿里巴巴沒有直接採用既有的訊息系統,反而選擇以 KafKa 的概念,重新實作一個,最重要的原因是「系統維護」,因為阿里巴巴是使用 Java 開發語言,而 KafKa 是用 Scala 開發的。

訊息系統最重要的是要維護資料的一致性,目前的規模是每天處理百億個訊息,系統 Loading 在 4~5 左右。

前端性能優化的重點

  1. 減少 http request
  2. 減少 redirect
  3. preload 資源
  4. 盡量減少 cookie 的大小
  5. delay loading
  6. asynchronous ajax,減少 DOM 節點數,加快render與 first byte時間
  7. CDN 加速
  8. delay render
  9. pre-resolve DNS
  10. js 不放在 header,避免 blocking concurrent render tasks

系統必須根據網站的特點進行優化,每個網站的轉換率、流量與使用者的特性不同,必須根據實際的數據,判斷優化是否對轉換率及流量是有幫助的,並不是無止境的優化。

JVM 團隊

核心系統開發部一開始,是在集團內部尋找需求,優化後,得到十倍的效能提昇,漸漸地累積出優化成果,內部其他團隊就會自動找開發部協助解決效能問題。

優化的方式,並不是要求應用程式修改程式碼,而是直接針對 JVM 進行修改與調整。但如果是計算密集的應用,就要用調整演算法的方式處理。

因為有優化 JVM 的經驗的工程師不多,因此這個單位大多都是由應屆畢業生進行專業培養。

這個單位的 KPI 不單純只看優化的結果,重要的是要對優化結果,找到適當的應用,把成果推廣出去,但有時候會因為各種原因,而用不上你修改後的東西。

Open Source

雖然阿里巴巴將自身開發的一些軟體以 Open Source 的方式發布出來,但對於阿里雲飛天平台,企業認為這是阿里巴巴的核心業務與價值,因此沒有開源。

開源對阿里巴巴來說,並不是一個核心企業價值,阿里巴巴是個需要賺錢的公司,這些開源專案,都是因為跟某些原始專案的團隊搭配上的問題,才刻意 fork 並調整的專案,而這些修改,都是因應阿里巴巴公司規模成長業務需要而去做的。

2014/10/19

洞 - 路易斯.薩其爾

洞是一本很奇特的兒童文學小說,故事的大綱,可以到 洞 wiki page 查閱,奇特的地方是,小朋友跟我讀過後的感受有些不同,這是一部1999年得獎的作品,我相信作品會得獎,一定有它特別的道理。

小朋友讀完後,認為這是一部故事情節很刺激的小說,主角史丹利.葉納慈特殊的倒楣家族歷史,雖然故事的前半段,有點不知所云,主角被意外送入了綠湖營,但情節從最後一個洞那裡開始,就狠緊張。

我還特別問了他一個問題,那些洋蔥跟水是從哪裡來的,他很快就回答出來,是山姆種的,還說了「一吻奪命」凱蒂.巴婁跟山姆的關係,重要的是,他了解故事情節有些部份是回顧過去發生過的事情,這個回答確定他是看得懂這個故事的。

在我剛讀完的時候,我的第一個直覺是,這麼特殊的情節,有情殺案、偷竊、集中營,為什麼這樣的故事會得獎。唯一的亮點,應該址有史丹利跟零蛋兩個人發展出來的友情,最後互相幫助並存活下來的過程,這個部份也是整個故事中,比較正面的情節,其他部份都是在描寫黑暗面。

故事的脈絡,是簡單而且直接的,沒有什麼隱藏的內情,我想這也是小朋友能看得懂的一個重要的原因。簡單的脈絡中,包含了綠湖營裡面的團體生活,少年之間如何互相鬥爭,藉以獲取最大的生存利基。另外是凱蒂.巴婁跟山姆之間發生的故事,還有左若尼夫人的詛咒,這三個部份巧妙地融合在故事當中。

書本最後的折頁中,另外介紹了三本得獎的作品,其中第一本讓我驚訝了一下,因為書名是「嗑藥」,很明顯就是一個在討論青少年在青春期過程中,為了種種原因誤入歧途,選擇用藥品麻醉自己,最後發現自己該換一個方式與角度看世界的故事,而這樣的故事也可以取得兒童文學獎,我得考慮一下,才能想清楚該不該買來給小朋友看。

2014/10/6

Classes and Objects in Scala

Scala 特地將 object 從 class 中取出來,讓 object 直接作為 singleton object 的保留字,原本在 Java 中需要透過 Singleton Design Pattern 才能達成的工作,Scala 內建了 singleton object,我們不需要再去了解 Design Pattern,只需要專注在開發 object 內容的工作上。

static method

在 object 中定義可以使用的 method,實際上用起來就像是在 Java 呼叫 static method 一樣。通常我們會在撰寫 Utility 程式中,用到 singleton 與 static method 的想法去實做,畢竟使用公用程式還要先產生物件,會覺得非常麻煩。

object StringUtil {
    def trimHead(s:String) = s.dropWhile(_ == ' ')

    def main(args: Array[String]):Unit = {
        println(StringUtil.trimHead(args(0)))
    }
}

測試

>scala testscala.StringUtil "   test  22 "
test  22

companion object

如果在同一個 scala 檔案中,包含了同樣名字的 class 與 object 兩個定義,在 scala 就稱為 companion object。

class StringUtil {
    val s = "     I am a string in class StringUtil"
}

object StringUtil {
    def trimHead(s: String) = s.dropWhile(_ == ' ')
    def getClassString(): String = {
        val su = new StringUtil()
        trimHead(su.s)
    }

    def main(args: Array[String]): Unit = {
        println(StringUtil.trimHead(args(0)))
        println(StringUtil.getClassString())
    }
}

如果使用 StringUtil.xxx ,實際上是呼叫 singleton object StringUtil,如果是 val su = new StringUtil(),實際上是產生了 StringUtil class 的 instance。

因此 su 是沒辦法呼叫 trimHead 這個定義在 object 的 method,而 StringUtil.s 也同樣是沒有辦法使用的。

仔細看一下 companion object,其實就跟定義一個 Java class,裡面寫上幾個 static method/field 完全一樣,scala 明確地用 object 與 class 兩個關鍵字,將 class/object 內部的靜態與一般區塊分開。

對於programmer來說,有了更簡潔的語法,更簡短的程式碼,又不需要了解 Singleton Design Pattern,整體來說,有什麼道理,不該繼續往 scala 前進呢?

Scala IDE in Eclipse 4.4 Luna

另外再提供一個資訊,目前 Scala IDE 正式版只能在 Eclipse 4.3 Kepler 上運作,如果是使用 Eclipse 4.4 Luna,則我們得要改用開發版的 Scala IDE。

雖然Scala IDE 4.0.0 Milestone 3在網頁上說可以用在 Eclipse Luna,不過實際上測試後是不行的,必須要使用Scala IDE Lithium (4.0), Nightly版本。