Haskell 屠龙之技 (恢复)

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

上一篇我光是在扯 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...
共收到 4 条回复

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楼 已删除

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

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