精通 Haskell 是一種怎樣的體驗?


咳,之前 @祖與占 提到要描繪下Haskell社區的大嬸(神),但。。。下面沒有了。。。。

我來上一個Edward Kmett大嬸(因為偷偷尾行過一段時間)的事迹 (github:ekmett),用語會比較誇張,但求一個笑果。

有一天,Ekmett爺想在Haskell里耍一下OpenGL,發現在Hackage上OpenGL綁定的庫有一堆:

Ekmett爺順手挑了一個,上下耍了幾圈,覺得不夠盡興:丫寫的爛庫,不能滿足爺的任(需)性(求)!!於是出現了下面這一幕(友情提醒,注意日期):

可惜應者寥寥。。。。。。

Ekmett爺:不行了,不行了,不能忍了!!!

袖子管擼起來,輪子造起來,打醬油的圍起來~~

幾天以後,Hackage上,一個叫做 gl: Complete OpenGL raw bindings 的庫,被上傳了。。作者:Sat Nov 1 22:44:40 UTC 2014 by EdwardKmett。。。(注意日期注意日期注意日期。。。。)

這個庫包含的模組是這樣的!!!(注意哈,要拉好幾頁哈,我就不拉了......)

這個庫的代碼結構是這樣的:(注意src文件夾哈)

其他Binding庫作者: WTF!!!!!!!!!!

Ekmett爺:俺從來不屑寫binding代碼,每當寫binding時,我寫個生成binding的Generator。。。。

嗯,以上。。。。。。。。。。

Ekmett爺的總結如下:懂Haskell、會數學、任性!!!



感覺@閱千人而惜知己 的答案少了點體驗 :D ,雖然我本身不精通Haskell, 但是偶爾也會觀摩下Haskell社區的大神, 這裡就不說Haskell社區里比較有名的兩個Simon(Simon Peyton Jones和Simon Marlow) 想從三個角度(大神)來說一下"精通"Haskell是怎麼個樣子的....

Edward Kemtt https://github.com/ekmett

Dan Piponi http://blog.sigfpe.com/

Lennart Augustsson http://augustss.blogspot.jp/

精力不夠只說LA好了,謝 @Tang Boyun 說了EK.

LA: 應該是Standard Chartered 里Haskell技術部門的大佬, 根據Haskell wiki的介紹

Standard Chartered has a large group using Haskell for all aspects of its wholesale banking business

根據14年八月這個招聘信息[Haskell-cafe] Haskell developer roles at Standard Chartered

We have more than 2 million lines of Haskell, and our own compiler.

據說可能是世界上最大的商業Haskell codebase, 還記得天津渣打招40個Haskell程序員...

btw, 渣打用的是一個Haskell的方言叫mu, 不過是strict, 而且還可以跟Excel交互, LA也是Haskell其中一個實現HBC的作者, 更多關於渣打對Haskell的使用可以看最底下的ref.

hmm...augustss是怎麼個精通Haskell法捏...其中一個傑作就是在Haskell里寫Basic:

http://augustss.blogspot.com/search/label/BASIC (不帖代碼據說會少點贊

{-# LANGUAGE ExtendedDefaultRules, OverloadedStrings #-}
import BASIC

main = runBASIC $ do
10 GOSUB 1000
20 PRINT "* Welcome to HiLo *"
30 GOSUB 1000

100 LET I := INT(100 * RND(0))
200 PRINT "Guess my number:"
210 INPUT X
220 LET S := SGN(I-X)
230 IF S &<&> 0 THEN 300

240 FOR X := 1 TO 5
250 PRINT X*X;" You won!"
260 NEXT X
270 STOP

300 IF S &<&> 1 THEN 400
310 PRINT "Your guess ";X;" is too low."
320 GOTO 200

400 PRINT "Your guess ";X;" is too high."
410 GOTO 200

1000 PRINT "*******************"
1010 RETURN

9999 END

這個東西展現了Haskell強大的DSL能力...

回到augustss的老本行,實現.

很多對Haskell的介紹是Haskell是一個用於學術研究的語言bla, bla...究竟是怎麼個學術法? 相信不少人接觸Haskell的第一件事就是去下載安裝一個GHC, 現在好多做研究的就是寫一篇論文, 在Haskell之上加個功能, 然後就成了一個GHC的extension, 又或者成為Haskell一個庫, 所以就有"Sometimes documentation is an academic paper"這種說法, 看Haskell 98, Haskell 2010的spec才多少頁...GHC的文檔又有多少頁, 別以為Monad一個東西有多難懂, 後面paper千千萬...呵呵

而augustss的牛B之處在於, 他能對這些東西的歷史如數家珍!

前端時間社區有好心人搞了個24 Days of GHC Extensions , augustss一個個給這些extension講了下它們的歷史, 背後的那篇peper...

Things that amuse me: A commentary on 24 days of GHC extensions

Things that amuse me: A commentary on 24 days of GHC extensions, part 2

Things that amuse me: A commentary on 24 days of GHC extensions, part 3

A History of Haskell 下一個版本沒有augustss做co-author我不服氣啊!

這麼個樣子應該算是精通Haskell了吧? 我就不說augustss的HBC, 幾年前跑分能跑贏GHC的 (GHC背後有巨大的財團M$支持! ) . 不是精通Type Theory, Category Theory的才叫精通Haskell =. =! 那些只是基本姿勢而已啦 =v=

夾帶個私貨.

Haskell呢, 不是有個很唬人的貨叫monad嘛, spj告訴我們說它好多東西都能用它來抽象(Simon Peyton Jones: papers), 某天看到個python decorator的例子

def before_after(f):
def decorator():
print("before")
f()
print("after")
return decorator

@before_after
def hello_world():
print("Hello, world!")

熟悉OO的人說, 這貨看著像AOP啊...

熟悉Haskell的人說, 這貨看著像Writer Monad啊...

放狗一搜: CiteSeerX — Monads as a theoretical foundation for AOP (還以為我能發paper了TAT

就是這個感覺!

Ref:

youtube.com 的頁面 augustss在CUFP11的介紹

wordpress.com 的頁面 今年Don Stewart(RWH作者)發布的招聘信息, 下面渣打使用Haskell的介紹

haskell.org 的頁面 Don Stewart在Google Talk對渣打使用Haskell的介紹, 謝謝 @Tang Boyun 補充


不存在可以精通 Haskell 的人類。(@霧雨魔理沙 說 Oleg「不是人類」)


回答這問題要冒說自己精通Haskell的風險。首先我並不敢說我精通Haskell,目前學的東西都只是一知半解,雜而不精,這裡我只回答,不承擔風險。也未見有人說自己精通Haskell,都是互相冠以精通的稱號,從來不是自封。SPJ做為Haskell的創造者也沒敢說自己精通Haskell,而更多的是有不明白的虛心向社區里的人們請教,比如Dependent
type, type safe coerce等等,還說如果有人有更好的辦法請跟我討論。我曾經見過SPJ本人,看見過他跟hlint的作者Neil
Mitchell討論問題虛心的態度。我想本貼的題目改成深入了解Haskell再用它來編程是什麼樣的體驗。我的朋友邀請我來回答,我覺得這個題目太大,之前在微博上請了一些強人,但是都推託沒有回答,那我就把我看到的簡單說一下。(註:部分代碼來自Don
Syme對於F#寫C#比較的教學視頻)

1、代數數據類型與類型類(ADT與typeclass):

使用ADT體驗上覺得要比OO輕鬆,尤其是在寫語法樹的時候,如:

data Exp = True
| Not Exp
| And Exp Exp
| Or Exp Exp

對比C#可能要寫成下面這樣,Java可能要更麻煩。

public abstract class Exp {}
public abstract class UnaryOp : Exp{
public Exp First {get; private set;}
public UnaryOp(Exp first){
this.First = first
}
}
public abstract class BinExp : Exp{
public Exp First{...}
public Exp Second{...}
public BinExp
....
}

另外對於Show類型類,即可以列印輸出的類型的集,還有Eq等等是可以通過deriving自動實現的,Java需要重寫toString還有equalTo之類的方法,而且是對於每一個之後定義的類。對於這類明顯可以自動做的事情C#、Java無法做好(如果大家有方法在Java里自動生成toString函數來列印所有成員變數的方法希望告知)如果你知道Haskell的類型系統就知道,Haskell中Exp是類型、而And、Not是值的構造器,而Java、C#將這兩者概念統一,無法進一步抽象,所以是不會有Kind(類型的類型)相關的概念。

之後再淡Haskell類型類:

類型類與介面相似,與abstract
class更近似,但是比介面強大與抽象類又不一樣。首先Haskell的類型類允許定義最小實現與互相實現,如:

class Eq a where
(==) :: a -&> a -&> Bool
(/=) :: a -&> a -&> Bool
x /= y = not (x == y)
x == y = not (x /= y)
{-# MINIMAL (==) | (/=) #-}

只需要定義等與不等的一個你就免費得到了另一個。如果你要寫一個函數需要這個類型同時實現Show與Eq兩個介面,那麼你需要合併兩個介面:

interface Show&{ String show (T a);}
interface Eq&{ boolean eq(T a, T b);}
interface ShowEq& extends Eq&, Show&{}

然後再轉而將你需要的類型實現ShowEq才能應用基於這個介面的類型,這樣就顯得羅嗦。所以說Java的泛型更像是打了一個補丁,並沒有很好地原生支持,相信C#也是一樣。Haskell里要定義一個基於多個類型類的函數完全不需要這樣,只需要把類型類限定加到函數的上下文中即可:

foo :: (Show a , Eq a) =&> a -&> a -&> (Bool,String)

所以我以為面向特質這樣的設計要比介面更好。

下一個說的就是泛型

已然已經忘記Java是在1.5還是1.4出的泛型,受到多人的吹棒,其實C++的Template跟Java泛型都沒有解決本質的問題。記得看過C++的Template是宏實現的,對於不同的類型變數會重新生成新的代碼編譯,並不是嚴格意義上的代碼復用。Java的泛型是在語法基礎上改動,加了靜態的類型檢查,可是寫起來還是相當繁複。並不了解C#泛型機制。這裡比如寫一個C#的swap,將一個tuple的兩個元素調換:

Tuple& Swap (Tuple& t){
return new Tuple&(t.Item1,t.Item2);
}

而Haskell
Prelude中定義的Tuple用起來則方便很多

swap (a,b) = (b,a)

類型的聲明可以省去,因為Haskell可以推斷,而即便加swap
:: (a,b) -&> (b,a)也要比C#或者Java中反覆地加類型標記乾淨省事得多,沒有類型推斷,我們要不斷地重複寫類型T、U作為標記為Java能夠檢查它。而定義一個Tuple這樣的類型使用ADT也相當容易,下面的是單向鏈表

data Pair a b = Pair a b deriving (Show, Eq, Ord)

data List a = Nil | Cons a (List a) deriving (Show, Eq, Ord)

單就上面的兩行可能Java、C#就需要做很多無謂的工作來實現。Java、C#中的泛型在Haskell只是參數化類型,多態函數id、const、flip到處都是,例子多得舉不過來,我也從來沒有見過Haskell把泛型拿出來當特性來宣傳過,因為根本不值得一提。泛型(Generics)一詞在Haskell用來代指一個更強大的特性,通過使用ADT構造的基本方法——Unit、積(Product)、和(Sum)來構造對於任意類型的同構關係,使用序列化、Eq、函子類型類的自動生成成為可能。有興趣的可能讀一下A
Generic Deriving Mechanism for Haskell一文,說到這裡有點跑題了。

2、高階函數

Java與C#中是沒有高階函數的,即返回函數,以函數為輸入的特性。它們只能把函數包在class裡面然後進行傳遞,不具有一般的特性。常常我們需要做一些重複的事情,比如把一個類型,列表或者其他的容器里的值應用一個函數,傳統的語言里就是寫一個for,然後對於新建一個容器,對每一個都應用一下函數,再返回新的容器。這樣的模式總是出現,而Haskell或者其他函數式語言里直接用map就可以了,對於樹,還有其他容器Haskell的Functor類型類就是做這個的。再加之有fold函數,這些東西總是在反覆在實踐編程中出現,支持高階函數的語言直接就全用高階函數做好了。有人在Haskell吐槽過($)還有(.)運算符,我的學生不理解,我研究生的同學,網上也見有人說這運算符很奇怪,這只是參數從右向左依次進入函數體中的一個運算符:

Prelude&> not $ even $ (+1) $ 5

5+1 = 6 --&> even 6 = True --&> not True = False

只要你喜歡,你可以定義一個(|&>)
= flip ($)把上邊的寫成

5 |&> (+1) |&> even |&> not

這就跟F#一樣了,F#可能更喜歡這種口味吧,順序式就是從上至下、從左至右,改變一下就不爽了。而這種東西在Haskell中恰恰是最自由的,十分隨意。

也正是由於高階函數與類型系統,try與catch這樣的東西在Haskell里完全可以用函數來實現,所以你會見到各種庫自己會有自己的處理異常的函數,這就是靈活引起的壞處之一,不得不說Haskell的異常管理就是個災難。任何單一的異常庫可能都滿足不了所有的需要,所以大家都寫自己的。

3使用哲學

初入門的時候覺得Haskell的類型系統真的不難,並且有很多很傻的東西,為什麼會有id
:: a -&> a、const :: a -&> b -&> a這麼傻的函數,鬼才知道有什麼用(剛學的時候真的是這麼認為的),Applicative中的(&<*&>)
:: f (a -&> b) -&> f a -&> f b就是把一個容器里的函數應用到一個容器里的值中,為什麼一切在Haskell里看上去都是那麼傻,看上去都是這麼無用?在之後的研究與學習中發現自己知道的越少,自己暴露的愚蠢就越多,這些東西在Haskell里都是有大用的,尤其是Applicative具有很強的一般性。Haskell就是這樣,由於每一個函數做的事情非常有限,粒度非常小,所以你單獨看他們會覺得都是沒什麼用的,你單獨看id
:: a -&> a函數的時候,無論你給什麼值,它都會返回你給的那個值,你輸入5就返回5,你輸入True就返回True,你當然看不出有什麼用處。舉兩個例子,一是Parser組合子里的選擇&<|&>,直接返回return,順序&>&>=,分析一次以上的some、分析0次以上的many、char、digit這些最容易的小函數組合起來最終會得到可以分析上下文無關文法的大的Parser組合子。另一個例子是控制並發的STM,主要只實現了返回return、順序&>&>=、選擇&<|&>、重試retry
4種函數就完全可以實現了你在並發里需要的一切。當你不屑於理解這些小東西的時候你就永遠也無法理解它們的組合。你在單獨審視一個函數的時候是沒有用的,只有當你小心地把它們組合起來你才會知道它們可以用來做什麼,這就是很多人覺得Haskell很多概念東西無厘頭的原因。

哲學中最重要的不過Monad了,總是有人罵Monad,現在說說Monad,這東西剛一學真是不理解為什麼要用,當初唯一的感覺——這是虎人的!因為Monad並不能給你帶來更新的特性,它只是帶來了一種更有效地組織代碼的方式。之前的STM與Parser都是Monad,Monad是一種設計模式來幫助你分離有不同作用代碼。當代碼中需要管理資源的分配與回收,你需要Resource
Monad,當要記錄狀態你需要State Monad、當你需要用元編程來生成Haskell代碼你需要Quotation
Monad,當你同時需要他們的時候,你用Monad轉換器把他們組合起來,他們之前不會互相影響並且每個Monad的行為都被嚴格地限定了,你不可能幹很出格的事情。

Haskell中的Lazy
IO十分難以控制,也並不符合Haskell的使用邏輯,於是就有了Iteratee這樣的庫。通過stream來源、中間變換處理、還有處理這些stream去向的Sink間的組合達到高度的模塊化,自動控制資源回收、內存使用的目的。(前段時間看好像Scala目前正缺這玩意)

4類型系統

除了簡單的HM類型系統以外,還有任意Ranked的類型,可以更為嚴格地控制函數與其行為,最為著名的例子就是ST
Monad與SYB中的Data類型類中的一些函數,這也使得Haskell處理動態類型成為可能。Haskell可以像Scala一樣做動態類型匹配,但是有些麻煩。

類型之上還有類型的類型,即為Kind,需要一個類型參數的類型構造器記為*
-&> *,像Java中的Vector就這樣,而兩個的則為
* -&> * -&> *,之前的Tuple就是這樣。假定我們要想辦法來表示類型,以便在運行時查詢一個值的類型,就需要定義下面這樣的重複。(Java中不需要主要是這樣,因為有instanceof關鍵字,主要是因為Java中沒有類型構造器一說,Exp類型中And也做為了類型繼承了Exp,竊以為這在工程邏輯有些混亂,And構造出來的是一個值,而並不應該具有類型屬性,可以Java中需要定義他為一個class,對於抽象的方式我更喜歡ADT)

class Typeable a where
typeOf :: a -&> TypeRep
class Typeable1 t where
typeOf1 :: t a -&> TypeRep
class Typeable2 t where
typeOf2 :: t a b -&> TypeRep
...

以上的t分別需要0、1、2個參數,Kind分別為*、*
-&> *、* -&> * -&> *,所以需要分別定義,而有了Kind多態,則可以一次完成。不同的是需要一個代理類型來代表a的具體類型。無論是寫成t,t
a這種都是不對的,因為我們並不需要有一個具體的值,所以用Proxy來指代。

class Typeable (a :: k) where
typeRep :: Proxy a -&> TypeRep

這樣在動態查看Tuple&這樣的類型在Haskell裡面做起來就十分方便。有了kind多態、再加上Proxy還有把值「提升」到類型上,就可以干一些比較有趣的事情了。

data Nat ...
data Vec :: * -&> Nat -&> * where
Nil :: Vec a 0
Cons :: a -&> Vec a n -&> Vec a (n+1)

通過這樣定義類型可以指明一個列表的長度,如此一來就可以限定我們的函數只在某些特定長度的列表上工作減少可能出現的錯誤,更為重要的是這種錯誤是在編譯期就能找到的,可以進一步地應用到矩陣的運算上,通過定義Matrix
(m::Nat) (n::Nat) Int限定一個m乘n大小的矩陣。這種類型對於某些問題還是不能進一步限定, 因為在運行時函數不能從類型中取得值,但是隨著GHC的進化,已經可以很好地做到這一點了。比如下面來自GHC手冊的例子,其中Proxy
2是類型,2是類型,而非值。 這裡就是Dependent
type了雖然還做得不好,比起Agda要差。

&> :m +GHC.TypeLits
&> :set -XDataKinds
&> natVal (Proxy :: Proxy 2)
2

說到這裡,可能更多的人是在問這些有什麼用。我覺得這裡問題不是什麼,而是你是否會用,怎麼用。我的理解就是這種類型可以在一個類型的基礎上限定出一個類型從而更好地讓類型系統幫助你組織出正確的程序,下面是唐鳳的代碼:

data Matching :: Symbol -&> * where
Matching :: KnownSymbol regex =&> Matching regex
Match :: String -&> Matching regex

instance Show (Matching regex) where
show m@Matching = symbolVal m
show (Match string) = string

前面的Vec
5 Int類型中我們可以說一個長度為5,裡面全是Int列表,而這裡我們則可以說Matching
「a?b?c?」是一些滿足「a?b?c?」的正則表達式的字元串,而不是所有全體字元串String類型。

xs :: [Matching 「a?b?c?」]
xs = list 100 serises
[,a,b,ab,c,ac,bc,abc]

類型起文檔作用,程序員直接就可以理解這樣的函數,使用起來也相當方便,藉助惰性求值,這樣的符合這樣正則表達式的字串要多少給你多少,(當然這裡只有這麼多,沒有100個)方便了自動測試工作。

5 元編程與引用

Ruby一直說自己有元編程能力,其中Haskell里也有,但是Haskell從來沒特意宣傳。元編程可以極大地消去重複的代碼,比如:

zip :: [a] -&> [b] -&> [(a, b)]
zip3 :: [a] -&> [b] -&> [c] -&> [(a, b, c)]
zip4 :: ...

這些函數可以先定義一個「元函數」zipN
:: Int -&> Decl,zipN 4就會幫你生成zip4。使用元編程可以幫助我們自動化很多事情。比如aeson庫自動生成FromJSON與ToJSON的類型類實例,完全不需要怎麼動手,並且生成FromJSON跟ToJSON類型類實例可以並在一句一起生成。Java需要藉助IDE工具來生成訪問器與設置器函數實在不是好的解決方法,借用Haskell的Lens庫一行就可以生成需要的函數。配合引用(QuasiQuote)可以沒有限制地向Haskell里加入語法糖。可以直接引用XML、JSON,Haskell會自動幫你生成對應用的data。引用一門語言的源代碼(多數為DSL)QuasiQuote可以幫你自動生成AST,可以真正做到一勞永逸,不寫重複的、機械的代碼。

6 GHC的效率與惰性求值

首先說一門語言慢是不對的,討論的應該是對應有編程器實現。廣受爭議的GHC的效率是一個很令人頭疼無語的問題。見過很多人用C寫一段程序然後用Haskell寫一段程序,編譯一下然後得出GHC慢的結論,再對惰性求值這種拖慢速度的特性踩上幾腳。更無奈的是你沒有辦法進行反駁,因為這不是幾句話能說清楚。每次我都想回為什麼你不比誰的代碼更美觀。我見過很多插中GHC軟肋的代碼,不知道他們是有心無心。常見的例子有用遞歸算階乘。因為運行的時候結果主要是通過一個地址指向一個內存中Thunk,對Thunk
force求值再讀到CPU進行運算。再加之其他運行系統以及寄存器使用等原因,GHC函數返回使用的是跳轉,而非ret(在Reddit上看Simon
Marlow有討論,那個Fibonacci函數總是要比C慢3倍),結果GHC要比GCC慢十幾倍。Haskell是否可以寫出更快的代碼呢?當然是可以的,注意一點,Haskell惰性求值只是特性,不代表Haskell只能惰性求值,Haskell里的嚴格求值,!模式,UNPACK、RULES、INLINE編譯器標記,Unboxed的類型都是可以用來提高效率的。但是層數太多的遞歸還是會比GCC慢,但是實際中遞歸的深度有多少能超過2000的?惰性求值是不是一個好特性?這個問題應該改成你會不會正確地使用,或者改成Haskell可以嚴格求值,那麼默認Lazy是否是一個好的選擇?Haskell的表達式形式分為3級,WHNF(弱首範式)、HNF(首範式)、NF(範式),其中NF是一個不可以再繼續化簡的表達式,對於其他語言如F#跟Scala我不是特別了解, 但是表達式化簡的過程由繁入簡是符合直覺的,從WHNF到HNF到NF對應默認的惰性求值seq($!)與deepseq($!!)。那麼GHC的效率是否高?這個問題我想不如改成誰來用GHC效率會高?我看到的普遍現象是大多數人的第一語言並不是Haskell,沒有在Haskell這種學習曲線本來就陡峭的語言上投入更多時間與精力,所以自然不會寫出比Java、C更快的代碼,寫出了幾行Na?ve的Haskell代碼就跟C比然後得到了GHC比GCC慢的結論,明顯是不公平的。我覺得一個人可能要花比C、Java多幾倍的精力來學Haskell才能寫出高效的Haskell代碼。

我沒有過實際的Haskell開發經驗,所以並沒經驗可供分享,不過就我看到的真正的實踐中的API如Aeson,分析JSON的效率在2.2GHz的i7上可以達到40M/s,編碼可以達到100M/s。而對於其他語言JSON相關的庫我並不清楚。aeson: Fast JSON parsing and encoding。並不清楚cjson會不會更快。

Simon Marlow在Facebook用Haskell實現的Haxl也要比原有的C++框架快,並且核心數增加,這個差距幾乎不變。另外唐鳳也在IBM做了一個字符集轉換的程序,用Perl每天在340MHz的IBM主機上只能處理到2.5GB,遠沒有達到一天處理25GB的要求。而且他已經試過用C/C++彙編去改寫某些特定的部分都沒有達到要求。也因為Haskell有惰性求值特性,內存使用大幅減少,效率也只比直接複製慢了2%(在視頻的第34分鐘)。唐鳳-函數式編程的商業使用
(推薦大家看看這個視頻) 並不是說GHC一定會比C快,但至少可以說明GHC是可以編譯出高效的代碼的。當然語言也是有適用性的,如果你真的需要一些結構體、內存變數、想手動管理內存分配回收,那麼可以用C再通過FFI給到Haskell,把更抽象的邏輯交給Haskell,語言都是有適用範圍的而且沒有萬能葯。如果一個人對於表達式形式WHNF、HNF、NF以及求值順序,運行時生成報告這些概念都不懂就說Haskell慢多半是人云亦云的。你需要更多精力學習才能寫出快的Haskell代碼。

7 使用的感覺

有一點已經沒有必要爭論,函數式語言包括Scala、F#、Haskell完成一件事情的代碼量要少很多,少1~10倍是多數情況,極端情況少20倍左右也不是不可能,這一點在《黑客與畫家》上有提,用Haskell解決8皇后問題5~6行,而Java我足足用了50行而且我不覺得有人會寫得更精簡。不相信的話可以試著把下面一門toy語言的AST定義譯成Java或者C#,看看要多少行代碼:

data Exp = Val Int | Var Name | App Op Exp Exp deriving (Show, Eq)
data Op = Add | Sub | Mul | Div
deriving (Show, Eq)
data Prog = Assign Name Exp
| If Exp Prog Prog
| While Exp Prog
| Seqn [Prog] deriving (Show, Eq)

在研究語言時常常要干這些事,比如實現Hoare邏輯計算、在這個toy語言上做Model
Checking,顯然函數式語言是首選。研究生的時候寫一個Lambda演算計算器只有了幾個小時,書里的練習可以自動幫我做好。考試的時候寫了一個LTL轉Buchi自動機的程序也沒有很費時間,更重要的是Haskell中的定義可以跟數學過程一一漂亮地對應,絕對不可能出錯。在學習計算機語言原理時,課上是用Haskell,但是我為了了解一個概念看了一本書用Java實現一門語言的過程,對於沒有經驗的我就是災難。需要寫的代碼少了,使用函數式語言對於程序員個體的戰鬥力的提高就相當顯著了,而我覺得非FP的語言想以這種倍速提高戰鬥力有些困難。我也會看到其他語言在宣傳各種特性,如Java8加入的Lambda表達式,我賤賤一笑,心想:這功能竟然會引起大的騷動。Ruby的元編程Haskell也是有的。Python的協程Haskell有庫實現,不需要語法支持(通過Monad與CPS實現)。Nodejs的非同步同樣也有庫實現,同樣不需要語法支持。至於goroutine我並不是特別明白為什麼呼聲如此高,可能我沒工程經驗吧,但是給我的感覺就是一個非守護的輕量級線程,這Haskell的輕量級線程還有與非同步的庫很像。由此我想說的是Haskell是相當靈活的,其他語言的特性不是有原生的實現就是有庫的實現。

Haskell是一門很精深的語言,有人說Scala比Haskell更難,我覺得專精C++、Haskell與Scala都是很困難的,難的地方還不太一樣,曾經以為學完了C++的指針、Template就完了,結果看到Boost庫的實現才知道之前學到的只是皮毛。我花了幾年的時間研究Haskell再去參加一些會還有看視頻才能大概理解他們說的是什麼東西。去年這個時間看Oleg實現的Iteratee的代碼還是不明白,現在看了很多視頻還有文章、用了Pipe、Conduit這些庫才能略窺一二。有一位Yale畢業的博士朋友,現任職於Intel,他寫的50行的非同步轉協程的代碼我不太能理解他在幹什麼,雖然CPS我也明白一些,但是無法理解。另外一位蘇格蘭的博士實現了GHC中的OverloadedRecordField擴展,我看了一下實現,能理解,使用了好多特性,但是覺得這個實現好優雅,吾不能及也。這就是我的感覺,學到現在一樣還是有很多看不懂的。相信可以讓大家從側面看出學習的陡峭的程度,並且GHC正在以非常快的速度進化。

這裡我相信唐鳳一定算得上專精Haskell了,對於函數式編程的感覺,在他上面的視頻里的第9分鐘是這樣講的:「大事化小,小事化無,以無事取天下」。把很小的東西,一個一個組合起來就可以得到一個非常非常複雜的程序。視頻最後他說到IO的定義,IO以真實世界的狀態為參數,返回真實世界新的狀態跟一個類型為a的值,你會產生幻覺,以為真實世界不過是函數里的一個參數。Haskell的目的就是要觀察這個世界,要改變這個世界,創造新的價值。這裡再給一次視頻地址唐鳳-函數式編程的商業使用

對Haskell的普遍誤解:

Haskell因為惰性求值很慢。之前已經在第6部分討論了,取決於你多了解Haskell。

Haskell沒有辦法輸出中間結果。unsafePerformIO函數專門來干這事,只是Haskell不推薦你這樣做。可以用Debug.Trace庫

Haskell是純的,沒有變數,所以沒法debug。邏輯不對,Debug.Trace就是干這個的,GHCi也可以設置斷點,只是Debug的方式可能跟你想得不一樣,不可以拿順序式來要求函數式。Haskell一樣有IORef作為變數。

Haskell沒有OO里的一些東西,所以不適合做工程。太扯,不是只有OO語言才能應用OO的思想,視頻中唐鳳也有說他也有把OO譯到Haskell,OO的封裝、多態、泛型FP語言里一樣有。Haskell的大項目之一就是GHC本身,已經過了25年還是可以維護,不知道可不可以說明Haskell適合工程。只是沒有那麼多Pattern,Pattern是反覆出現的東西,大分部這在Haskell里早就被做好了,只等你用。

Haskell沒有成熟的東西。Haskell有Yesod、Fay、Perl、XMonad。你會用咩?

回應在Haskell
Core Dump貼中的Haskell沒有Core Dump。這個是從Haskell
wiki上來的https://www.haskell.org/haskellwiki/Introduction
有人說strong typing means no core dumps邏輯不通。我的理解是:這裡主要是說類型檢查可以檢查出來導致崩潰的錯誤(當然有吹牛的成分),Haskell不需要管理內存,又是純函數式,所以不需要core
dump,作為懂行的人你看看就行啦,什麼快速排序、代碼易懂、沒有Core dump這些都是騙人進來學Haskell的。後面的代碼根本不易懂,Monad,Monad
Transformation,Arrow虐爆人的頭,出了錯誤當然要適當分析,分析System
FC,core dump、彙編,Haskell之前還有type
family以及coerce引起的段錯誤,這都是有可能的。為騙人進來學的一頁wiki搞得這麼認真,作為懂行的人都看不出來,你輸了。

回應Haskell的類型系統設計不嚴密,沒有達到設計者吹噓的樣子。(在Haskell
Core Dump貼中看到的)。Coq、Agda的類型系統很嚴密,Agda本身沒有被驗證,但是是Haskell寫的,Coq據說用手工驗證過,必然比Haskell嚴密,你會去用他們嗎?Haskell是在鼓吹Strong
typing,怎樣?其他語言加個泛型、lambda就各種宣傳,人們為什麼不能接受Haskell吹吹強類型?如果Haskell說依賴類型有多少人懂,能吸引多少人進來?

Haskell真正要乾的事情是主頁上說的rapid
development of robust, concise, correct software,快速地開發健壯、精確、正確的軟體。效率沒有在上面,雖然它可以很快。很多功能都沒有體現,只是把簡單的露在上面,下邊巨大的冰山你不走近是看不到的。

最後我覺得為了某種特性去了解一門語言是好的,Go、Ruby都好。你要是為了學習一門語言中思考的的邏輯與思想,那就是極好的,上之上也。為了了解函數式的各種概念,Haskell值得你一看。


曾經精通過 還專門去讀了phd 精通以後發現還是ocaml比較容易出活 現在天天寫ocaml...


說到底語言都是工具,精通一門語言的話,就去發揮它的優勢,避開它的缺點,用它征服難題唄。你說是吧 ; ) 感覺答非所問,不過樓主是不是應該問點specific的問題呢。


看了 @閱千人而惜知己 的回答,能感受到其功底深厚,只是要求受眾得有一定Haskell基礎才能看懂。我作為一名接觸Haskell短短兩三年的本科碼農,更不敢說自己精通Haskell,也斗膽來分享一下關於Haskell的一些體驗。

----------------------------------------------------------------------------------------------------------------

先簡要回答樓主的問題,:

Haskell(以及其它函數式語言)和C/C++等命令式的語言的區別主要還是在於思想:

命令式語言需要教計算機「怎麼做」;而函數式語言是要告訴計算機「是什麼」.

具體到代碼中,就是等號的作用,

命令式語言中的等號,是一個賦值操作;

函數式語言中得等號,是在下一個定義。

舉個栗子:

命令式:

西紅柿炒雞蛋 = 炒(西紅柿,雞蛋)

這個時候無論你要不要,計算機已經把西紅柿炒雞蛋已經做好了。即使你不想吃了也不能退單了,菜都已經給你端上來了,不想吃自己扔掉好了,反正錢(CPU,內存)是要花掉的。至於那個等號,只是為了方便在你想吃的時候可以拿這個內存地址取到這盤菜而已。

函數式:

西紅柿炒雞蛋 = 炒 西紅柿 雞蛋

這個時候你只是下了個定義,相當於把菜譜念給計算機聽,告訴它西紅柿炒雞蛋這道菜,是西紅柿和雞蛋炒在一起的結果,計算機只是記住了菜譜而並沒有去炒菜。

命令式語言是工程師的語言,函數式語言是科學家的語言。

很小的時候老師問我長大後的夢想,儘管俗,但是我真的想成為一名科學家。後來因為生(cheng)活(ji)所(tai)迫(cha),變成了一枚工程師。在我幾乎忘記了當初雄心壯志的時候,Haskell,讓我覺得離夢想近了一些。。。

雖然我這輩子可能都沒辦法「精通」Haskell了,但當我打開Notepad++寫Haskell的時候,就是這樣一種體驗。。。

----------------------------------------------------------------------------------------------------------------

前面是體驗,後面就是瞎扯了,主要是看了@祖與占同學的回答,覺得有些東西不吐不快。

三年前從一家非985,211的大學計算機系小本畢業,來到了某家銀行供職。

剛入職沒多久,老大丟過來三本書,一本《Think in Java》(無奈,就是這麼惡俗),一本《Future, Option and other Derivatives》(算是投行必讀了),還有一本,就是《Real World Haskell》。。。

另外兩本都能讀懂,不過這本《RWH》完全看不懂,只好找網上翻譯的中文版,只有前幾章被翻譯了,後面的Monad是個球啊,完全理解不能。。。在我困惑的時候,發生了以下對話:

我:這個Monad看不懂啊。

同事:別問我,我也不懂,問XXXXXXX(另一個同事的員工編號)去。

我(在outlook里鍵入XXXXXXX):Donald Stewart? 這是誰啊?

同事:作者。

我:啥?

同事:《RWH》的作者。

我:卧槽!

《RWH》: Real World Haskell

後來稍微會了一點,開始改Haskell程序,不會的地方,就去查Hoogle,碰到不會了的地方,向同事請教。

我:我想實現BALABALA,有沒有什麼現成的API可以用啊。

同事:別問我,我不知道,問XXXXXXX(另一個同事的員工編號)去。

我(在outlook里鍵入XXXXXXX):Niel Mitchell? 這又是誰啊?

同事:寫Hoogle的。

我:卧槽!

Hoogle: Hoogle

再後來,寫了一個遞歸的函數,我覺得自己沒問題,編譯總是報錯,又請教同事。

我:這個為毛編不過啊,幫我看看,我覺得沒啥問題啊。

同事:奇怪,語法應該沒問題,估計是編譯器的事。

我:那怎麼辦?

同事:問XXXXXXX(另一個同事的員工編號)去。

我(在outlook里鍵入XXXXXXX):Lennart Augustsson? 別告訴我咱用這編譯器是他寫的啊。

同事:就是他。

我:卧槽!

Lennart: Lennart Augustsson

學Haskell的時候,真的是懷著無比敬畏的心情在一步一步學的。一開始還不敢打擾大神,只敢在群聊裡面提問題,跳出來耐心回答我的,總是以上三位大神之中的某一個,其實還有另一位梳著辮子的大神也一樣耐心,恩,那是Lennart的老大。。。往往我覺得自己還沒把自己的問題表達清楚,他們已經理解並且給我提出解決方案了。。。

這些大神都在同一個部門,他們的職位都是量化分析師(或者量化分析師的老大),能跟他們打上交道的部門總共也沒幾個,碰巧當時我就被分配在了其中一個。剛畢業的時候,能在一群科學家呵護下成長,不得不承認是一件非常幸運的事情。

半年前,為了撫慰自己不安分的心,跳槽去了另外一個城市的另外一家銀行,就再也沒有Haskell用了,做新的Java項目的時候我申請用Scala,終於也被駁回了。。。

如果再讓我選擇一次要不要跳槽,我應該還是會選擇會,因為到了新的城市之後,我有妹子了(姚明臉)~~

----------------------------------------------------------------------------------------------------------------

最後,分享一些關鍵詞,來幫助新手快速理解Haskell基本思想,

最最最基本的要算是遞歸

進入正題,要看 λ演算

然後要理解λ演算等價於圖靈機

接著要知道柯里化

如果願意深入到哲學層面,可以試著理解哥德爾不完備定理

我貼得都是中文WIKI,如果覺得不夠詳盡的話,請自行看英文版。


我是沒本事來回答這問題的,我只是說說感想。

當我看到「精通Python是一種怎樣的體驗」的時候,我很高興地點進來想看看牛逼的人是怎麼樣的。

當我看到「精通C++是一種怎樣的體驗」的時候,我在想這麼牛(bian)逼(tai)的人知乎上會有嗎?

然後我看到本題的時候我就覺得,「精通Haskell是一種怎樣的體驗」,這真的不是什麼邏輯悖論嗎?


作為初階使用者,我猜想所謂精通就是可以從左往右寫代碼吧……


初學者剛接觸 每次做練習都感覺自己蠢爆了 感覺精通後 數學能力max+


推薦閱讀:

Haskell等語言中的模式匹配在C++中如何實現?
編程語言中的「組合性」是什麼意思?
柯里化對函數式編程有何意義?
C++、Haskell、Scala 和 Rust 究竟哪個最複雜?

TAG:軟體開發 | Haskell | X是種怎樣的體驗 |