Haskell 屠龙之技 (恢复)

luikore · 2013年01月05日 · 最后由 snailpp 回复于 2017年02月18日 · 14833 次阅读

上一篇我光是在扯 Haskell 多 nb 却没说学 Haskell 到底有什么好处或者用处... 我想了很久,列出几点又被自己反驳了...

增进人与人之间的沟通和理解?学 C 可以理解 Go 语言的发明者对前半生的深深遗憾 (误), 学 C++ 可以理解 D 语言的发明者对 C++ 爱是有多深 (大误), 学 Python 可以窥见豆瓣改版的某些因素 (超大误)... 学 Java ... 如果你的同事用了 ThreadPoolExecutor 去改进性能,结果程序就挂了,学了 Java 就会拍拍同事的肩,叹一口气,说"王架构早告诉你别在线程池里创建线程池了"... 吾在玩了片轮少女后,非常喜欢里面盲眼的 Lily, 心想以后万一真的遇到这样的少女肿么办,至少得会用布莱叶写"好き"吧... 但用函数式语言的人很少了,用 Haskell 的人实在是更少,不是 Haskell 程序员却和 Haskell 程序员同事的人就更少了,这无穷小量何处去找 (下次杭州 Ruby Tuesday?).

利用 Haskell 处理并发并行的优势,玩转 Actor (轻量级并发) 和 STM (软件事务性内存), 拥抱多核的时代?老实说几千核的 GPU 已经普及了但 100 核的 CPU 还很少见... 赶紧研究显卡和 OpenCL 才对吧...

以子之矛攻子之盾,学习函数式编程,给论坛上那些鼓吹 Haskell 的人狠狠的打脸?我想起 Jeremy Weirich, 这么老的一个 Ruby 程序员,在 Ruby Kaigi 2012 上分享在 Ruby 中写 Y-combinator 的过程,比起在 haskell 中写个 y f = f y f 就说 "实现了" 更有挑战性...

我们每天在杀猪,为什么还要研究龙的空气动力学, 为什么还要训练对龙格斗术? 所以还不如反过来,在这里摘录一些 wiki books 上的屠龙宝典的介绍,揉合一些杂七杂八的内容,把 没必要学 Haskell 的理由补充充分...

站在 Ruby 程序员的角度,Haskell 很多特性都是处于反面:

  • Ruby 是缩进不敏感的,不过用参数 -W2 可以提示某些地方缩进与代码块不一致的地方。Haskell 的缩进是语法的一部分,标示着块的开端。写了 do 就不用写 end
-- 函数入口是 main
main = do putStr "hello"
  putStrLn "world"

缩进风格上 Haskell 和各种 lisp 一样,习惯和上一行的第二个词对齐,比 Ruby 要多产生很多空格,所以上面的代码会变成这样:

{-
    Haskell 缩进风格:
    putStrLn 和上一行的 putStr 对齐
-}
main = do putStr "hello"
          putStrLn "world"

命名风格上 Ruby 是蛇行习惯,下划线看起来像空格。Haskell 是驼峰习惯,少敲一个键,除了函数/参数以小写字母开头外,其它东西 (包名,数据类型,type class,constructor...) 都用大写字母开头。

函数调用风格上 Haskell 是 "反正你都要写那个空格,干脆逗号也不要算了".

  • Ruby 里有常量/变量/方法之分,Haskell 却没有变量和常量

在 Haskell 里

f = 3

其实在 Ruby 里相当于

def f
  3
end

= 并不是赋值运算符,而是定义的意思,要念出来的话就是 is.

  • 英语是 Ruby 的原语,但象形文字 (hieroglyph) 才是 Haskell 的原语 orz ...

例如在 Ruby 里声明一个 lambda

lambda {|x| x != 5}

在 Haskell 里就变成

\x -> x /= 5

在书里就显示成

λ x  x  5

后来 Ruby 也稍微向 Haskell 靠拢了点

-> x {x + 5}

另外一些象形文字还有

  • 左鱼 <-<
  • 右鱼 >->
  • 猫头鹰 ((.)$(.))
  • ...

不是开玩笑!你和 Haskell 程序员说 right fish 他应该明白就是指 >->!

但也有不太明显的符号,记住下面这个词汇表,基本就能高声朗诵 Haskell 代码了:

::   has type (类型声明)
:    cons (列表构造)
=>   implies (类型约束)
.    compose (函数复合)
<-   drawn from (从...中拖出来)
-<   arrow
&&&  both
|||  either
++   append
>>=  bind
>>   then
<*>  applied over
!    index
()   unit
(,)  tuple (2元组)
(,,)  triple (3元组)
(,,,)  quadruple (4元组)
(,,,,)  quintuple / pentuple (5元组)
(,,,,,)  sextuple / hextuple (6元组)
(,,,,,,)  septuple (7元组)
(,,,,,,,)  octuple (8元组)
(,,,,,,,,)  nonuple (9元组)
(,,,,,,,,,)  decuple (10元组)
(,,,,,,,,,,)  hendecuple / undecuple (11元组)
(,,,,,,,,,,,)  duodecuple (12元组)
(,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,) centuple (百元组)
-- 其实... n元组念 n-tuple 就可以了 XD
  • Ruby 里字符串是最重要的数据结构,但函数式语言里 List 才是最重要的数据结构 (Lisp 之所以叫 Lisp 就是因为人家是玩 List 的...). 又因为 List 的特性,在 XMonad 或者 Haskell 实现的编辑器可以看到 Zipper / Double Zipper 之类的奇怪有趣的数据结构。

构造一个 List:

[]         -- 一个空 List
1 : []     -- [1]
[1] ++ [2] -- [1,2]

常见 List 处理函数的山寨实现

{-
head []      -- error
head [1,2,3] -- 1
-}
head x:xs = x

{-
tail []     -- []
tail [1..3] -- [2,3]
-}
tail x:xs = xs

{-
init [1..3] = [1,2]
-}
init (x:_:[]) = [x]
init (x:xs) = x:init xs

{-
last [1..3] = 3
-}
last (x:[]) = x
last (_:xs) = last xs

{-
take 3 [1..] -- [1,2,3]
-}
take 0 _ = []
take n (x:xs) = x:take (n - 1) xs

{-
drop 3 [1..4] -- [4]
-}
drop 0 xs = xs
drop n (x:xs) = drop (n - 1) xs

{-
判断是否为空
null []     -- True
null [1..3] -- False
-}
null [] = True
null _ = False

{-
length [1..3] -- 3
-}
length [] = 0
length (x:xs) = 1 + length xs

说到函数式,在 ruby 中我们有 select, map, inject, find, 在 haskell 中我们有 filter, map, foldl/foldr, find. 其实像 ruby 那样把 lambda 放后面是比较好写的,block 可以换到下一行,像 haskell 那样把 lambda 放前面就略不好写,但是 haskell 的做法又能享受 curry 的好处:因为把 map 等函数和一个 lambda 绑定住多次使用,比和一个 list 绑定住的情况要多得多。

{-
map (*2) [1..4] -- [2,4,6,8]
-}
map f [] = []
map f (x:xs) = f x:map f xs

{-
filter (>3) [0..6] -- [4,5,6]
-}
filter f [] = []
filter f (x:xs) = if f x then x:filter f xs else filter f xs

{-
find (>3) [0..6] -- Just 4
-}
find f [] = Nothing
find f (x:xs) = if f x then Just x else find f xs

reduce 在 haskell 中分为 foldl 和 foldr 两种

foldl (+) 0 [1..5] == ((((0+1)+2)+3)+4)+5
foldr (+) 0 [1..5] == 1+(2+(3+(4+(5+0))))
-- 作为体会 Haskell 无用性的练习, 就留给读者实现了

当然 Haskell 中内建大量的 List 操作函数, 某些函数还有 strict 或者特殊用途的版本。在代码中 import Data.List 或者在 ghci 中 :m Data.List 就能使用了。讨厌学习的人完全可以不 import, 自己实现一套。

Haskell 的字符串类型其实是 [Char], 也就是 Char 的 List, API 统一了也可以少学好多东西...

first "hello" -- 'h'
  • Ruby 和 Erlang, Clojure 一样是动态类型的,Haskell 是静态类型的。在 Ruby 里,你不去关心对象的出身,而去关心对象的表现,Haskell 却是非常势利非常关心学历的。但 Haskell 又像一位温柔的 HR 姐姐,不直接问你要学历,而是暗中揣测 (类型推导), 只在必要的时候才无厘头的问一句 "你高数老师是不是脖子上有个彗星形状的胎记" 让你方寸大乱...

主动报学历或者主动造学历也是程序员应聘面试技巧之一... 在 ghci 看类型用 :t 命令

Prelude> :t 1
1 :: Num a => a

输出按照上面的密码词典翻译,就是 1 has type 'a', 'a' implies Num, Num 是 class, 下面会继续解释。

Haskell 的常见类型有 Void, Unit, Int(小整数), Integer(任意长整数), Float(单精度浮点), Double(双精度浮点), Char, String, Bool 等。

Void 是这么一个类型:任何东西都不属于 Void, 它是一个没有值的类型... 如果把类型看作集合,属于一种类型的东西是集合元素,Void 就是空集。真要问 Void 是什么东西,答案就是它的定义就是 "平时我们不会遇到也不会用到的东西"... 在类型运算中 Void 是零元,就和 0 在四则运算中的地位一样。

Unit 是这么一个类型:它只有一个值 (), 它和其它 tuple 一样,是类型的笛卡尔积,但它是 0 个类型的积,也就是乘法单位元,和 1 在四则运算的地位一样。(关于 Void 和 Unit 以后要专门发一篇帖子讨论代数数据类型..). Scala 中也有 Unit 类型,其实和 C / Java 里返回 void 是一样的。

类型声明可以在源代码中用,但不能在 ghci 中用 (我会告诉你因为 ghci 其实是个 Monad 吗??). 例如你可以在 main 上面加个类型 main :: IO (), 念法就是 main 的类型是 IO 没有返回值...

main :: IO ()
main = do printLn "hello world"

class 这个坑爹的词,但是说白了就是实现函数重载的工具。因为用参数类型来匹配函数重载不够高端 (java 的 interface 可以看作用类型匹配函数重载), 所以 Haskell 用参数的 type 的 class 来匹配。

挖了一个弥天大坑... 下面是待完工的部分... 务求一定要将不用学 Haskell 的理由写得充分完整...


其实,在静态语言里,List 里每个元素类型要一样,这不是略蛋疼的一件事,而是非常非常蛋疼的一件事,在一个静态语言里处理 json 有让人想死的冲动...

  • Ruby 是语法上 OO 的,Haskell 是语义上 OO 的,前面说了此 class 非彼 class 你又来说 OO ? master foo (无名师) 说过,闭包和对象是一体...

  • Ruby 的函数调用是 strict 的,也就是参数在调用前要求值,而且一调用就要出货,Haskell 的函数调用是 lazy 的,参数在调用后也不需要求值,到了 IO 时才真的去计算 (是不是想起那个可以用 O(1) 的时间解决任何问题,只是没有 IO 的编程语言?). 所以 haskell 的程序往往会多耗一些内存 (保存未求值的调用) 但少做一些计算。

  • Ruby 程序的正确,我们用直觉和测试去保证 (我觉得我的程序是对的), Haskell 程序的正确,我们用证明去保证 (你能证明你的程序是对的吗?)...

  • Ruby 把各种不纯的东西混在里面,Perl + Smalltalk + Lisp + ... 连 awk 的写法都无耻的拿来用了,是编程经验的积累,实用得很。Haskell 是纯纯的函数式语言,内置 Hindley Milner 推导系统的加强版,是类型化 lambda 代数历史的沉淀,不实用得很... 但有句话叫做 "愚者从经验中学习,痔者从力屎中学习"...

打开类型运算参数后,Haskell 的类型运算自身已经构成图灵完备的语言 ...

{-# LANGUAGE EmptyDataDecls, TypeOperators #-}

利用 type deducer 是可以做些事情的,例如实现一个 quickCheck (need citation here)

  • CRuby 有全局解释器锁,你虽然可以创建多个线程,但是线程间是同步的,每个进程最多只能吃掉一个核 (JRuby 开一个进程就能把所有核占满 (这是好事还是坏事?...)), Haskell 是 HPC (高性能计算) 中的高手,写复杂并行程序还不会因为锁的问题搞得一团糟。在 Ruby 里调用 Haskell 可以用 Hubris, 在 Haskell 中调用 Ruby 可以用 Hubris-Haskell. 在 Rails 程序里用 Hubris 把一些 CPU 密集型的运算交给 Haskell...

Google Cache 上恢复的 50 楼后的评论

lgn21st 51 楼,1 天前

_@_luikore 学习一门新语言,我特别希望了解这门语言的发展史,比如诞生之初是怎样的,发展过程中有哪些产生重大影响的人或者事情,Ruby 语言的发展史 Matz 大叔讲了很多了。JavaScript 得发展史 Brendan Eich 在 JSConf 2010 上也有详尽得介绍。

能不能介绍一些 Haskell 得发展史,科普一下?

2880

luikore 52 楼,1 天前

#51 楼 _@_lgn21st 这个?http://research.microsoft.com/en-us/um/people/simonpj/papers/history-of-haskell/history.pdf 有图有照片,略长...

Large_f08750e605d58ff54872d955169ce0c3

lgn21st 53 楼,1 天前

#52 楼 _@_luikore 这... 怎么感觉像是把历史写成了论文... 科普文要不要这么严肃啊~~~

Ca7d6e25a44498e666f6b65f8578ffc1

Johnk 54 楼,1 天前

#48 楼 _@_kenshin54 我觉得是理解问题。我理解的"蛋疼"是不够强大,可我觉得它写大规模的项目也完全没问题,只要掌握好规范。lua 不是面向对象的语言没有类,可是通过某种规范,也可以面向对象编程有类,js 也是一样的。我觉得,成见一直在影响着我们的交流,有些大牛说这门语言是残疾的,大家就轻易地把 js 视作一门残疾语言,这里面是存在理解问题的,我不认为 js 是一门残疾语言,其他语言能做到的,js 也能做到。咱们什么时候能够有真正的独立思维的能力?或者说真正理解那些大牛的话?

2880

luikore 55 楼,1 天前

#53 楼 _@_lgn21st 嗯... 我也觉得是,那文章里还提 XX 会议什么的编年史挺闷的...

几个关键词是:
最早是 lambda calculus (这还没带类型的考虑), 实用化成了 lisp
后来有了类型化 lambda calculus, 实用化成了 ML, 也发展出各种类型推导的系统
然后就是 lazy 的研究,实用化成了 Haskell

7ff5fdf2175e6e31a0cf105201d7065c

fleuria 56 楼,1 天前

#51 楼 _@_lgn21st 乱入一段无关的拙译 =v=

柯里生平 http://fleurer-lee.com/2009/10/31/ke-li-sheng-ping.html

2880

luikore 57 楼,1 天前

#56 楼 _@_fleuria
lazy, curry and fun : 蕾丝咖喱粉 ~

7ff5fdf2175e6e31a0cf105201d7065c

fleuria 58 楼,1 天前

#57 楼 _@_luikore
warm, fuzzy and meow~ ( ´ ▽ ` ) ノ

2 楼 已删除
3 楼 已删除

评论都恢复了

#2 楼@jjym:我这里招 js 大牛,感兴趣不呢?

6 楼 已删除

想调整范式不容易,特别是思维的范式

需要 登录 后方可回复, 如果你还没有账号请 注册新账号