#導覽列 #文章列表 #許洛豪
在光之翼的守護下訴說著一段段的英雄傳說。 [ 首頁 | 網誌 | 相簿 | 留言 | 訂閱 ]

懷念 Ruby 隨便給你放的陣列嗎?咱家的 Scala 也有!

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

好吧,雖然是很簡單的一件事,但我不知道為什麼我看的 Scala 的書和文件裡都沒有提到這些事,或許這可能不是很重要,又或者是明顯到根本不需要特別提?

不過,我還是寫一下好了……

話說像 Ruby 這類動態型別程式語言,大部份都有一個有趣的特性,那就是陣列裡面隨便你放任何東西。

舉例來說,下面的陣列在 Ruby 裡是合法的:

arr = [1, 3.14159, "我是字串", Time.now]

然而,像在 C / C++ / Java 這類靜態型別程式語言當中,陣列通常會與型別掛勾,陣列裡只能放相同型別的東西,例如下面的陣列,就只能放整數了。

int x [] = new int[10]  // 我只能放整數

這造成了不小的限制,但幸好在 Java 中,由於所有物件都是 Object 的子類別,所以你還是可以像這樣繞道而行,只是程式碼會變很醜而已,而且還得自己想辦法轉型。

// 我能放任何東西,可是請你自己轉型,順道一提,出了錯我不賠錢的
Object x [] = new Object[10]

而在 Scala 之中,一切都變得很簡單了,你有兩個選擇--Structural Type 與 Pattern Matching。

Structural Type 的想法其實就是 Duck Type 的另一種說法罷了--如果他叫起來像鴨子,走起路來也像鴨子,那麼他就是鴨子。

簡單來講,就是我只關心他有沒有實作某個方法或屬性,管他這個方法是哪裡來的,管他是繼承哪個類別或介面,只要他有這個方法或屬性就可以囉!

這麼一來,我就可以宣告一個陣列,裡面放的都是具有 myPrint() 這個方法的物件,但彼此並不互相繼承,也沒有共同的父類別或介面。

這麼一來,我就擁有一個比 C/C++/Java 彈性一點,又比 Ruby 安全一點的陣列(Ruby 可不可以有這樣的限制我不知道,煩請熟悉 Ruby 的朋友幫忙補完),因為我確定我在呼叫陣列裡的每個元素的 myPrint() 方法的時候,絕對不會出錯,一定找得到這個方法。

至於 Pattern Matching 的部份,其實和 Object [] 的概念是一樣的,只是語法簡潔很多,而且在進行型別比對時就幫你轉型,所以你可以很放心不會不小心寫錯。另外,程式碼會漂亮很多,從此脫離 if/else 的魔掌了(大誤)!

以下就是一些實際的程式碼啦,基本上應該有點 Java + Ruby 的基礎就看得懂了,主要的重點大概就是:

  • Any 相當於 Java 與 Ruby 中的 Object,是一切物件的父類別。
  • [T] 是 Generic Type,把他想成 Java 裡的 <> 就好了,例如 List[T] 就是 List<T>。
  • list1.foreach () 就等於 Ruby 的 list.each {|x| ….},只是 x 的部份直接省略為 _ 。
  • match 相當於 Java 的 switch,只是他可以連型別一起比對,並且直接幫你轉型,所以當 case x: ForInt 為真時,x 會轉型成 ForInt 。
/***************************************************************/
/* 簡單定義一些沒有共同父類別的東西                            */
/***************************************************************/

case class ForInt (x: Int) {
    def myPrint () = println ("MyPrint:" + x)
}

case class ForDouble (x: Double) {
    def myPrint () = println ("MyPrint:" + x)
}

case class ForString (x: String) {
    def myPrint () = println ("MyPrint:" + x)
}

/***************************************************************/
/* 利用 Strucural Type 來宣告 List                             */
/***************************************************************/

// 注意以下三個並沒有共通的父類別
val forInt:    ForInt    = new ForInt (3)
val forString: ForString = new ForString ("Hello World")
val forDouble: ForDouble = new ForDouble (3.14159)

// 這個 List 可以放入任何具有 myPrint(): Unit 方法的物件,不論他
// 們是否有共同的父類別,或實作相同的介面。
//
// 第二個 List[T] 是必要的,不然 Type Inference 機制會分析錯誤,
// 造成型別不符的錯誤。
type T = {def myPrint(): Unit}
val list1: List[T] = List[T] (forInt, forString, forDouble)

// 依序呼叫 List 裡每個元素的 myPrint() 方法
list1.foreach (_.myPrint()) 

/***************************************************************/
/* 以 Pattern Matching 實作隨便你放任何東西的 List             */
/* 於執行期再依物件類別決定要做啥                              */
/***************************************************************/

val list2: List[Any] = List (1, "我是字串", 3.45, new ForInt(3))
list2.foreach ( _ match {
    case x: Int    => println ("我是 Int:" + x)
    case x: String => println ("我是字串:" + x)
    case x: ForInt => println ("我是 ForInt:" + x)
    case x         => println ("我是其他東西:" + x)
})
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-12-16 (週三) 11:59:48

CWT23 與 APH 的 Scala DSL。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...
灣娘卡灣娘卡

警告:本文內含 APH  同人與技術混合內容,APH 為一國家擬人化創作,內容不代表真實國家情形及立場。

話說今天去了 CWT23  的攤位,結果都沒看到我原本萌的東西啊,然後在會場被萌到的大概就是灣娘吧,滿滿的灣娘在路上走來走去,真是萌煞人也。

於是,兩天的購物內容就都變成和灣娘有關的東西是也!而今天入手的就是這套 APH  紙牌遊戲,購買的原因……有阿呆毛的灣娘好萌(大誤)!

好吧……回到正題,其實買的時候很掙扎的,僅管我承認我是個阿宅,可是要在會場上像斑目那樣子的買法,我還是沒辦法的啊,更何況我還是個米蟲,還在等上工呢。

所以,在會場的時候,一直在和另一攤的萌獸DNA與戀星這兩套自製遊戲的合輯考慮,而最後還是選了灣娘卡。

理由嘛,其實是因為我覺得這種卡片遊戲好像還滿適合做成手機連線對戰遊戲的,所以打算買回來研究一下!

然後呢……回家後就不小心搞出這個 DSL  了,花費時間,好像十分鐘不到吧!結論就是 Scala  好強好厲害,最後一段設定遊戲的部份,根本就不像程式語言,可讀性莫名其妙的強,搞不好一般人都看得懂。XD

/**
 *  人物卡片擁有三個屬性:名稱、生命值、描述
 */
case class PersonCard (name: String, description: String, hp: Int) 
{
    def hasHP      (hp: Int) = PersonCard (name, description, hp)
    def describeAs (description: String) = 
        PersonCard (name, description, hp)
}

/**
 *  每個遊戲裡有複數張人物卡片
 */
case class GameBoard (title: String, 
                      personCards: List[PersonCard]) 
{
    def hasPersonCards (cards: PersonCard*) = 
        GameBoard (title, cards.toList)
}

/**
 *  神奇的轉型函式,DSL 的秘密就在這裡
 */
implicit def str2PersonCard (name: String) = 
    PersonCard (name, "", 0)

implicit def str2GameBoard (title: String) = 
    GameBoard (title, Nil)

/**
 *  今天在 CWT23 買的 APH 同人遊戲裡的一些國家卡片
 */
"APH 紙上遊戲" hasPersonCards (

    "台灣" hasHP 60  describeAs "一般來說,中國、香港、日本、韓國、台灣" +
                                "裡,台灣最好相處(因為太弱啦),雖然自" +
                                "己不覺得,但是大家都覺得她家的夜市很好" +
                                "玩。",

    "香港" hasHP 70  describeAs "被英國領養了一段時間,最近回大哥家住," +
                                "有點適應不良,對金錢很精明,但人際關係" +
                                "則有點無力,看不大出來他在想什麼。",

    "美國" hasHP 100 describeAs "熱血青年,自認是世界的警察,對於做家務" +
                                "有點懶洋洋,不過墨西哥會從後院的洞偷溜" +
                                "進來工作所以沒關係,是最近一次大流行感" +
                                "冒的病源。"
)
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-12-13 (週日) 19:19:41

我還是覺得這是基礎中的基礎。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

算是 BBS 上的舊文重 PO 吧,畢竟這是自己的一些想法,留在部落格上好像比較好一點。

原先這篇文章是回應 aMaa 網友的這一篇『類別的方法中為什麼可以建立本身類別的物件』的問題,看起來好像沒啥關聯,但我覺得這還是很重要的,至於後續還有一些發展以及想法,我會另外用專文來寫。

簡單的講,我還是覺得這些看似不相干的基礎是很重要的。

當然,如果你寫程式的目的只是為了好玩,只是想做玩具,只想當成手邊解決問題的工具,那麼這些確實可以不用去弄懂它。

但如果你想靠這行吃飯,我覺得還是認命一點,把基礎搞好吧……

就像,你可以拿起鎚子、釘子、木板自己蓋出一間狗屋,但你應該不會想要去住沒有建築基礎專業知識的設計師、建築師蓋出來的房子吧?


【離題】

說實話,這個討論串讓我想到之前在自己的部落格上寫到的這一篇雜記裡關於程式語言學習的部份,有問到到底在學習程式式語言的時候,要從何處入手。

這一串討論看下來,看來看去,只有一個想法:會有這種疑問,根本就是因為不了解整個 von Neumann架構/程式語言/編譯器/機器語言/虛擬機器之間的關連所造成的嘛……

以下,可能很長,可能很多看似無關緊要的東西(就算你不懂,你的程式還是可以動),可是我自己認為這是值得去了解的基礎,如果你真的用心了解下面所講的東西,基本上就不會再出現類似的疑惑了。

【回題】

其實原 PO 的問題本質上和另一個問題一樣:為什麼像是下面的遞迴程式裡,第 6 行的時候,明明 sum 還明有定義完,卻可以呼叫自己呢?

// 用遞迴計算 1 + 2 + ... + n
int sum (int n) {
   if (n == 1) {
       return 1;
   }

   return n + sum(n-1);
}

為什麼下面的程式裡,Node 明明還沒定義完,裡頭卻又出現另一個 Node 呢?

class Node {
   private Node next;
}

我真的很想大叫:不要鬧了!大家到底知不知道 Java 程式語言 / Bytecode / Virtual Machine / von Neumann 電腦架構 / 原生可執行檔 / 機器語語之間的關係,到底知不知道一個 Java 程式是怎麼執行的啊?!

請問你自己一個問題:Java 程式執行的時候,是執行你寫的原始碼嗎?

(答:不是,實際上執行的是 bytecode,也就是 javac 翻譯出來的 .class 檔)

請問你自己第二個問題:你在執行你的 Java 程式 (Bytecode) 的時候,你知道 Bytecode 到底是什麼東西,做什麼用的嗎?

(答:可以將 Bytecode 視為一套虛擬的『類機器語言』,將其交由 Virtual Machine 解譯後,可以產生電腦 CPU 真正能夠理解的機器語言指令)

請問你自己第三個問題:你知道什麼叫 von Neumann 架構嗎?你知道一個『原生電腦程式』是如何在 von Neumann 架構上執行的嗎?

(答:如果不知道,請參閱『小人電腦』,裡面是簡化版的說明,但基本上目前的所有電腦都不脫離這個架構。)

如果你看完了上面的小人電腦,請你問你自己第四個問題:你在小人電腦裡,有看到『資料結構』、『函數』、『物件』、『類別』這種東西嗎?

(答:沒有,只有記憶體/指令/暫存器/Program Counter ,而且記憶體位置好像也都知能存數字【註】)

(謎之音:那我的函數、物件、資料夾構在哪裡?提示:我們還有可執行檔以及 Bytecode 這兩個東西沒講到。)

【註】嚴格來說這並不正確

請再問你自己第五個問題:你知道我們剛剛『定義』出來的計算總合的函式,如果翻譯成小人電腦的機器語言,用上述網頁中的機器語言表示出來會長什麼樣子嗎?

(答:你會看到 sum(n-1) 被翻成一個 CALL 指令,其參數 XX 的部份會是此函數的開頭,而 return 會被翻成 RETURN ,如果你不知道這是什麼意思,請把小人電腦再認真看一次,並注意最後一個函數呼叫的例子)

以上,是回答為什麼函數可以呼叫自己。

接下來,請再問你自己:記憶體 / 指標 / 參考 / 物件之間,究竟是什麼關係,你知道第二個 Node 的 Java 程式碼,被載入到記憶體後,到底長什麼模樣嗎?

(答:指標和參考本質上都是相同的,都是『記憶體位置』)

這不就很明顯了嗎?private Node next,說的是『next 是一個記憶體位置,而且這個記憶體位置應該要指到一個長得像 Node 的物件』。

但 next 真的必需指到 Node 物件嗎?(提示:多型/強制轉型/執行期錯誤)他不過就是個數字,來表示記憶體位置,誰管他指到什麼地方啊!

所以說,這有什麼好訝異的?不過就是『在 Node 的這個物件裡,有一個欄位是一個數字,他指到某一個記憶體的位置,而這個記憶體位址上的內容,應該要是另一個Node 物件(但不必然是)。』

這很直覺,很正常啊!

最後,回到原 PO 的問題,如果再把原 PO 的問題更簡化,可以問另一個問題,那就是以下的 Java 程式碼是否合法,如果它合法的話,執行這段 Java 程式碼裡的 test() 到底會發生什麼事,記憶體裡產生了哪些變化?

class Hello
{
    public void test ()
    {
        Hello hello = new Hello();
    }
}

public class Test
{
    public static void main (String [] args)
    {
        Hello hello = new Hello ();
        hello.test ();
    }
}

接下來,你要問你自己,Java 裡 new 出來的物件,到底在是住在記憶體的哪裡?而指到各物件的參考又活在哪裡?是在 Stack 還是在 Heap ?指標裡存的又是什麼?

再來, hello.test() 這一行會翻譯成什麼小人電腦的組合語言?(提示:Method call 和 function call 本質上是一樣的東西)

如果,你能回答出以上的問題,基本上就不會有『為什麼函數沒有定義完還可以呼叫自己,為什麼物件可以 new 自己,為什麼明明就還沒定義完,Node 裡卻可以有 Node』 的這種疑問了。

謎之音:所以說,這篇『爪哇學校的危害』還是有他的道理在的啊!

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-12-05 (週六) 11:27:06

[Android] 實現 Functional 的 Cursor。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

退伍了,終於有比較多的時間可以東摸西搞了,想當然爾,現在的我想玩的,就是 Scala + Android 啦!

Android 確實是個很不錯的平台,可是也有他弱的地方,其中我最不喜歡的,就數 Content Provider 啦。

在 Content Provider 的架構下,的確可以達到不同應用程式間溝通的的做用,可是操作的階層實在是太低了,竟然直接動到資料庫的指標去了。

這對於已經漸漸習慣了 Functional 的我而言,真的是很痛苦啊,想到要在那邊 move 來 move 去,就一整個煩人。

幸好,這個時候,很強大的 trait 就登場啦,只要下面短短幾行的程式,馬上就可以把 Cursor 變成 Functional 啦!

class CursorSeq (cursor: Cursor) extends Seq[Cursor]
{
    require (cursor.moveToFirst)
 
    override val length = cursor.getCount
    override val elements = new Iterator[Cursor] {
        override def hasNext = !cursor.isLast &&
                               !cursor.isAfterLast &&
                               !cursor.isClosed
 
        override def next : Cursor = {
            cursor.moveToNext()
            cursor
        }
    }
 
    override def apply (n: Int) : Cursor = {
        cursor.moveToPosition (n)
        cursor
    }
}

然後,再配上一些 Wrapper class……

case class ReminderItem private (id: Int, title: String,
                                 description: String, triggerTime: Int,
                                 maidID : String)
{
    def uri : Uri = Uri.parse (Reminder.ContentUri + "/" + id)
}

object ReminderItem
{
    private def get (cursor: Cursor) : Option[ReminderItem] =
    {
        if (cursor == null) {
            return None
        }
 
        val colID = cursor.getColumnIndex (Reminder.ID)
        val colTitle = cursor.getColumnIndex (Reminder.Title)
        val colDescription = cursor.getColumnIndex (Reminder.Description)
        val colTriggerTime = cursor.getColumnIndex (Reminder.TriggerTime)
        val colMaidID = cursor.getColumnIndex (Reminder.MaidID)
 
        val id = cursor.getInt (colID)
        val title = cursor.getString (colTitle)
        val description = cursor.getString (colDescription)
        val triggerTime = cursor.getInt (colTriggerTime)
        val maidID = cursor.getString (colMaidID)
 
        Some (ReminderItem (id, title, description, triggerTime, maidID))
    }
 
    def getAll (context: Context) : Seq[Option[ReminderItem]] =
    {
        var result : Seq[Option[ReminderItem]] = Nil
        val cursor = new CursorSeq (context.getContentResolver.
                                     query (Reminder.ContentUri,
                                            null, null, null, null))
 
 
        for (row <- cursor) yield get (row)
    }
}

就可以寫出 Functional,並且超簡潔的程式啦!例如:

val item = ReminderItem.getAll.filter (_ != None)

// 取出標題是 Hello 的 ReminderItem
item.filter (i => i.title == "Hello")

// 取出所有 ReminderItem 的 Title
item.map ( i => i.title)

這樣比起直接下 Query 來得清楚多囉。

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-11-26 (週四) 10:26:29

趁手的兵刃--Scala。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

有一些人應該知道,我挺喜歡有事沒事玩一玩一些莫名其妙,沒啥知名度的程式語言,像是之前有玩過 Pike 或是 D  語言之類的。

其實除了好玩外,有一部份也是總覺得自己在正規教育中學的諸如 C  語言或是 Java 這類著名的程式語言,用起來有一些不順手的地方。

畢竟,工欲善其事,必先利其器。就像行走江湖的武林人士都要有一把趁手的兵器一樣,程式與言就如同是程式設計師的兵器,當然希望能夠找到合自己意的,這也是為什麼我之前會看上 D  語言的原因。

但話說回來,有的時候事情沒有那麼美好的啊!選擇這種冷門的程式語言,常常得面對的就是各式各樣支援以及各種函式庫缺乏的問題,D 語言當然也不例外。

而且 D  語言還有許多更嚴重的問題--兩個程式語言規格,兩種標準函式庫,還不夠成熟的編譯器,本來是想簡化 C  以及 C++  的,但最近卻反而愈變愈複雜。

於是最後,我還是選擇放棄了這個看起來不錯,但實際上問題相當多的選擇。

但我還是一直在找趁手的兵刃,最後終於找到啦!那就是我正在用來寫 Android  程式的 Scala  這個程式語言。

雖然和 D  語言比起來,少了系統程式設計方面的優勢(例如指標等底層的操作),但後來我也發現,其實我也根本沒在做系統程式嘛!

以目前我想往 Android  應用程式走的方向而言,Scala 真的是剛剛好啊!

而且與 D  想要在程式碼的層次上向下相容不同,Scala 一開始走的就是程式碼不相容,而是在 Byte Code  上相容的路,於是可以丟掉很多包袱。

總而言之,Scala 提供的諸如 Tuple、Traits、型別推測以及 Closure 以及 Pattern Match 等等,再和 Android 搭起來,讓寫 Android  程式變得超簡單又有趣的啦,不再像以前一樣一堆多餘的語法。

總而言之,接下來就是繼續試驗看看 Scala  是不是一把真的能夠用來行走江湖的好兵器囉!

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-09-26 (週六) 16:34:57

Maidroid Omikuji(Maidroid 抽籤) Release 1.2 正式發佈!

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...
讓傲驕小鏡幫你抽籤吧!讓傲驕小鏡幫你抽籤吧!
讓傲驕小鏡幫你抽籤吧!(View at PicasaWeb)

由於又要回營區了,所以簡單帶過一下,詳細的經過等下下個星期留守回來後再寫。

總而言之,Maidroid Omikuji 這個抽籤小工具發佈 1.2 版了,這個版本應觀眾要求,加上了選擇人物的功能,可以用任何你喜歡的角色當做小工具圖示,例如說,傲驕的小鏡。如果內建沒有你喜歡的,也可以從 SD 卡裡選擇。

照例的,若您有 Android 手機,請到 Market 裡的 Lifestyle 裡下載使用,如果你和我一樣是沒錢的模擬器一族,可以直接下載 APK 檔回去安裝。

在開發的部份,這個版本我整個用 Scala 改寫過了,和 Java 比起來整個就感覺漂亮簡潔多了,Scala 真的是好物啊!若您對用 Scala 寫的 Android 程式有興趣,程式碼可以到這裡瀏覽,至於開發心得就留待下次。

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-09-13 (週日) 15:05:36

Programming Android 1.5 application with Scala.

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

Sorry for my poor English, but I think this maybe useful for those people who want program Android 1.5 application with Scala.

Thanks for cris’s work, I finally found out how to build Android 1.5 applications with Scala 2.7.5.

Here is how to do it.

  1. First, you will need install ProGuard, and here is the android_rules.xml ANT build XML I modified.
  2. Copy it and overwritten the original android_rules.xml under ${ANDROID_SDK}/platforms/android-1.5/templates/ directory.
  3. This build file should work both on pure-Java source code and Scala source code mixed with Java source code, if your android application is written by Java,  just run ant debug/reinstall/release as usual.
  4. If your android application is written in Scala, then you need create an tools directory and copy both scala-compiler.jar and scala-library.jar into it like Step 3 in chris blogs.
  5. Now, run ant reinstall to test and enjoy your Scala-powered Android application.
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-09-12 (週六) 09:17:25

『將手機宅化以侵略藍星』作戰計劃第一彈!

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...
主人要記得寫標題喔!主人要記得寫標題喔!
主人要記得寫標題喔!(View at PicasaWeb)

話說這幾天都在搞 MaidroidGTD 這隻程式,遇寫愈覺得 Android 根本就是一整個犯規嘛!幾乎什麼都可以改,什麼都可以惡搞,而且方法還很簡單,簡直就是在鼓勵人們來惡搞嘛!而且改不出來的時候還可以直接挖原始碼出來看,這真是太邪惡了!

愈來愈期待將來 Android  會怎麼樣改變人們對於手機和行動裝置的想像。

回歸正題,這兩天一邊看著官方網站的說明文件,一邊看著星期四去買的這一本『Android SDK 開發範例大全』,把 MaidroidGTD  的收件匣的部份給弄好囉。

目前還很陽春,就是一般資料庫的增刪查改而已,然後再加上看起來好像還不夠萌的萌化措施。

簡而言之,這個程式的核心出發點很簡單:用萌化以及對話的方式,讓 GTD 的管理流程變得有趣,然後會吸引我自己去用。

最終的目的是讓這隻程式跑在將來我買的 Android 實體手機上,然後讓 GTD 的管理模式融入自己的生活中。(謎之音:其實這家伙內心想的最終的目的是看能不能把他放到 Android Market 上撈上一筆,或是看趕不趕得及參加第二屆的 Android 應用軟體大賞來出個名。)

這就是『將手機宅化以侵略藍星』作戰計劃的第一步!

如果這隻程式能順利完成,我想第二彈應該會是萌化的計帳軟體,理由一樣,我試過很多記帳方式,不過都沒有持之以恆的動力啊!我要靠把手機萌化來拯救自己的生活!(謎之音:這樣應該只會愈陷愈深吧?)

順道一提,有沒有人會用模擬器內附的谷歌拼音輸入法啊?我怎麼按就是按不出來。>_<

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-05-31 (週日) 11:11:11

[Haskell] 真的比較好懂和不容易出錯嗎?

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

話說在 Haskell 的官方網站以及教學文件中,都提到了 Haskell 的好處之一是能寫出 Bug 比較少、比較短、比較清楚的程式。

在 Real World Haskell 中也提到下面這樣的觀點:

Even with years of experience, we remain astonished and pleased by how often our Haskell programs simply work on the first try, once we fix those compilation errors.

雖然我目前只讀到那本書的第三章,關於函式的定義和使用的部份,但做了一些練習題後,其實我有些懷疑 Functional Programming 社群這樣子的宣稱,是不是有點言過其實。

在接下來,我會拿 Real World Haskell 裡一個實際的練習題,說明我為什麼會有這樣子的想法。

首先,我們來看書中這個練習題的題目:

Write a function that computes the mean of a list, i.e. the sum of all elements in the list divided by its length.

在 Haskell 裡的 List,其實就等於是陣列,因此這個題目很簡單:寫一個函式計算陣列的平均值。我想這個程式應該很簡單,任何宣稱自己會寫程式的人應該在三分鐘之內就能把程式寫好,並跑出正確的結果才是。

OK,為了再突顯出問題的所在,我們把問題再簡化一點,要求出陣列元素的平均,我們要先求陣列裡每個元素的加總。

所以,我們先把問題改成:寫一個函數,計算陣列裡所有元素加總後的數值。

好,首先我們先來看 D 語言是怎麼做的,我想這也是大部份的人一看到這個題目,第一個想到的方法。

int sumD (int [] list)
{
    int sum = 0;
    foreach (int element; list) {
        sum = sum + eelemnt;
    }
    return sum;
}

相當標準的做法,而且我相信即使你沒學過 D 語言,只要有 C-Syntax 程式與言寫作的經驗,應該也猜得出來那個 foreach 迴圈是在做什麼用的。

好,接著我們再來在看 Haskell 裡,我們應該要怎麼寫這隻程式。

sumHaskell xs | null xs   = 0
              | otherwise = 
                    (head xs) +
                    sumHaskell (tail xs)

什麼?沒學過 Haskell 看不懂?沒關係,我們把這個程式改寫成 D 語言的版本,但不去更改它的演算法,就會變成下面這個樣子:

int sumHaskell (int [] list)
{
    if (list.length == 0) {
        return 0;
    }

    return list[0] + 
           sumHaskell (list[1..list.length]);
}

WOW!我們竟然沒有宣告任何存放結果的變數,就可以達到和之前的程式相同的效果,多神奇啊!

可是先等等,為什麼我們弄兩份不同的程式碼出來?不能直接用第一個版本走遍天下嗎?

喔喔,抱歉,不行,因為在 Haskell 裡有下面兩個特性,讓你不可能實做出第一個版本的程式:

  1. 所有的變數都是常數,在第一次定義後就不能變動,因此你不可能用一個變數來累加。
  2. 根本沒有迴圈。

有了以上兩個限制,我們在 Haskell 裡,只能實做出第二個版本,而第二個版本……遞迴!

於是爭論就開始了,第二個版本真的比較簡單、清楚,又不容易出錯嗎?很抱歉,我並不這樣想。

在第一個版本裡,我們的處理邏輯很簡單,宣告一個變數存放最終的加總值,用一個迴圈依序取出陣列裡的每一個元素,並一直累加到之前宣告的變數裡。

我相信你給任何一個小學生一個不規則數列,請他們在紙上加總,高達八成的小學生,會利用以上的方法來做這個題目,我們很乖的把第一二個數字相加,再加上第三個數字,再加上第四個,再加……

那麼 Haskell 的版本呢?遞迴!看一下程式碼,嗯,有一個 list[0],所以我們先把陣列的第一個元素拿出來,加上後面那一串。

呃,後面那一串用一個方法處理了 list[1..list.length],也就是陣列裡剩下的部份。

OK,所以我們也是從陣列的第一個元素開始算,再加第二個元素,再加第三個……

叭噗!錯錯錯!完全錯!在第二隻程式裡,我們實際上是從陣列的最後一個元素開始加總,在 [3, 5, 7, 9] 這樣的陣列裡,我們的實際計算過程如下:

  1. 9 + 0 = 9
  2. 7 + 9 = 16
  3. 5 + 16 = 21
  4. 3 + 21 = 24

看到了嗎?我們寫出的程式是和直覺相反的!在這樣的情況下,你要怎麼宣稱你寫的程式是簡潔、易懂、好維護又不容易出錯呢?

確實,我們的第二個版本比第一個版本簡潔多了,但是真的比較容易理解和不會出錯嗎?我們第一個版本的做法,我相信任何一個小學生都能夠理解其中的道理,並且能用紙筆重覆出所有的步驟。

但第二個程式呢?你要怎麼向小學生解釋遞迴的概念,還有為什麼實際跑的時候,我們會從 9 + 0 開始算,而那個 0 又是怎麼來的?

一個無法讓小學生理解的計算方法,真的可以算是容易懂嗎?一段看了之後還得去想為什麼要這樣寫的,會發生什麼事的程式碼,真的比較容易維護嗎?過了三個月之後,你再回頭看這段程式碼,是不是還能夠在三秒內反應過來他整個的運算過程,並且確定他是對的?

在我看來,程式會出錯不外乎兩個原因:

  1. 由語法的小錯誤造成程式結果的錯誤。
  2. 演算法本身的邏輯就有問題。

第一點我們在 C 語言之中就常見到,譬如在實做第一個版本時,for 迴圈的邊際條件設錯,造成陣列索引超出範圍或過小,而導至程式當掉或計算結果錯誤。

這是與法上的錯誤造成的,而不是演算法的錯誤。

確實,Haskell 是一個很嚴厲的語言,語法有一點小錯誤或沒有對好,甚或是型態錯誤,編譯器就一直發出抱怨,叫你要修改程式,否則不給你執行。

但其他非 Functional Programming 典範,比較新的程式語言,也同樣可以避免這類的錯誤。舉例而言,在第一個版本裡,我們用了 D 語言裡頭的 foreach 迴圈,確保我們一定可以完整地走完整個陣列,而且不會超過邊界。

這樣一來,兩個版本在第一點上就沒有什麼差異了,因為不論是 Haskell 或者 D 語言,在設計上都讓你盡量避免第一點的錯誤。

那麼第二點呢?在兩個版本裡頭,我們用的演算法相當不同。第一個版本是典型的 Imperative Programming 的做法,我們清楚的告訴編譯器我們要怎麼完成這件工作--從數列開頭,一個個數字拿出來,並且再把他們加到另一旁已經計算好的數字上。

三句話,小學生都能懂的事。

然而第二個版本呢?遞迴,可怕的遞迴,約耳談軟體裡說可以砍掉一大半的學生的遞迴,程設考卷上永遠有人寫不出來最後結果的遞迴,以及講了半天還是沒啥人懂的河內塔演算法。

這還只是給學生一個遞迴的程式問你是什麼意思時會發生的事,還沒叫他們設計遞迴演算法呢!

邊界條件是什麼?化約方式又是什麼?要怎樣才能確保我的遞迴程式是正確的,不會限入無止境的 Stack Overflow 或是 Segmentation Fault ?

如果你的程式語言,只允許使用遞迴來解大部份的問題,你要怎麼大聲的對那些新手,或是少了遞迴的那根筋的學生說:用這個語言,你寫出來的程式比較容易懂和不容易出錯?

因為在我看來,沒有任何一個使用遞迴的演算法是易懂的,也沒有任何一隻遞迴的程式是不容易出錯的--為什麼第二隻程式我們要 return 0 ,難道 1 不行嗎?助教。

Update (2008/10/01):

Thinker 提供了另一個 Haskell 的版本,我把它稍作整理和同樣改寫成 D 語言的版本如下:

sumHaskell v lst | null lst = v
                 | otherwise = 
                       sumHaskell 
                           (v + (head lst))  
                           (tail lst)
int sumHaskell2 (int v, int [] list)
{
    if (list.length == 0) {
        return v;
    }

    return sumHaskell2 (
        v + list[0], 
        list[1..list.length]
    );
}

這次我先不解釋這隻程式到底在幹嘛,也不加註解,請各位自行推測看看。

我個人覺得這個版本比之前的還難解釋(我想我真的是沒有遞迴那根筋的那種人),因為變數更多了,看起來更奇怪了。到底我們第一次呼叫這個函式要計算的時候,v 要傳什麼進去才對呢?後面的化約條件又是為什麼要那樣寫?v 在整個函式裡到底扮演什麼樣的角色?

在第一個遞迴版本中,我還可以想到用一句話來解釋:每一次的運算,我們都先算完後面陣列的總合,再加上我們目前所在的原素,sumHaskell (list[1..list.length]) 會幫我們算後面的陣列總合,所以我們可以不用理它。

如果不去深究遞迴的計算方法,這個解釋還是很合理的--我們先用一個函式計算完 list[1..list.length] 這個陣列,也就是去掉第一個元素後剩下的總合,再加上目前的這個元素,就會是全部的總合。

可是第二個版本,我究竟要怎樣解釋才講的清楚呢?我是在什麼時候做最後的加總的?計算的方法又是什麼?為什麼 list 空的時候要返回 v?我第一次呼叫這個函數的時候 v 要填什麼?

另外,SayYa 上的 letoh 版友提到:

在 D 語言版本的程式也有一行 int sum = 0; 難道 sum = 1 不行嗎? 這是一樣的問題 我能想到的簡單解釋 就是加法單位元素 也許你會有機會看到實作 factorial 範例 為什麼裡面的初始值是 1 不是 0? 我想得到的答案也是一樣 因為要設成乘法單位元素作為終止 我相信這和遞迴好不好懂應該無關:-)

的確,我在寫這篇的時候,並沒有想到第一個版本也有 int sum = 0 的這個問題,但我事後想了一想,我會忽略 int sum = 0 和特別挑出 return 0 的原因,是兩個敘述對我而言,在解釋的難易度上會有差別,而且我最後用的問句也不完整。

int sum = 0;
很明顯的,這是我們總計的結果,當然是從零開始加。
return 0;
助教,為什麼會有兩個 return,前面這個 return 是幹嘛用的?為什麼後面要 0,用 1 不行嗎?

第一個範例裡,我們很明顯的可以看出 sum 是做什麼的,進而推論到 0 是合理的預設值,可是第二個 return 如果對遞迴不熟,就很難去解釋『這行到底要幹嘛』(因為你必須解釋遞迴到最後一層的時候,會用到這個值,所以他等於是一開始的預設值 <== 好繞口)。

我想這是解釋難度上的差別,因為第一個範例實在太好解釋了,那個零有很大的機會不用去解釋。我自己的經驗是,大部份的學生只要知道我們的迴圈是『從頭一個個加』,就不會對於那個 int sum = 0 感到意外。

可是第二個就好難解釋啊,我每次光是解釋為什麼要設邊際條件,和邊際條件的 return 值就累死了。XD

最後順到一提,其實初學 Haskell 到現在,他最讓我感到激賞的是 Type Inference / Type Class,你可以不用宣告函式的標頭,就能夠擁有強型態檢查。而且如果你沒宣告,同一個函式就可以用在各種合理的型態上,成為 Generic 程式(像上面的 Haskell 版本,可以用在整數陣列、浮點數陣列都沒問題),不像 C++/Java/D 還要用 Template 來達成(上面的 D 語言版本只能用在整數陣列)。

這個在寫小程式的時候真的很方便。:p

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2008-09-30 (週二) 19:36:31

[Haskell] 序言。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...
就是這本書誤了我就是這本書誤了我
就是這本書誤了我(View at PicasaWeb)

先聲明,我還是個函數式程式設計 (Functional Programming) 的新手,而且對於使用這種 Paradigm 的程式語言我也只認識 Haskell 而已,因此這個系列裡有很多想法可能只是我的偏見、誤解與錯覺,所以如果我講錯了,就還請多多包涵。

對 Functional Prgoramming 起興趣,是在很久以前在約耳談軟體系列中看到『爪哇學校的危害』一文,裡頭談到了這種程式設計典範,說了下面這句話:

In a purely functional program, the value of a variable never changes, and yet, it changes all the time! A paradox!

想當然爾,完全沒接處過 Functional Programming 的我,腦海中冒出的第一個念頭就是:變數不能改動,那是要玩什麼啊!

之後在網路上找了一下 Functional Programming 的資料,就把目標鎖定在 Haskell 這個看來不錯的程式語言,而且也到學校的圖書館找了一本英文的教科書來看(中文的根本沒有)。

但不幸的是,讀了半天,我還是搞不懂 Functional Programming 到底是什麼,有什麼好處,因為教科書上的程式碼範例和思考邏輯,都和我自己在寫的程式沒什麼兩樣。

舉例而言,想要把一個圖形給黑白轉的時候,就寫一個函式,參數是原先的圖形,反回值是新的圖形,函式的內容是把每一個像素由黑變白、由白變黑。

這不是廢話嘛!不然還得怎麼做?就這樣,我很快地放下手邊的教科書,不再去理他。

一直到前一陣子我在 Stack Overflow 這個程式設計的討論網站裡看到有些人提到,學 Functional Programming 可以學到另一種思考的方式,我這才決定重新好好學起。

而且拜這本 Real World Haskell 所賜,雖然我還只讀到第三章,但這次我終於比較知道函數式程式設計到底是什麼東西,還有 Haskell 到底有什麼特別之處。

因此接下來的這個系列,我會記錄我在學習 Haskell 時碰到的問題、解法,以及對於 Functional Programming 和 Haskell 的一些想法。

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2008-09-30 (週二) 19:28:23