翻译于:http://amtal.github.com/2011/08/25/why-haskell-is-kinda-cool.html
我使用Hakell有一段时间了,尝试着把它运用到大型的 ISH 项目。
学习它是个费时的过程,但也十分值得。
使我长时间迷恋于它的原因有以下几点:
一开始,对于你可以做什么不可以做什么,语言规定得似乎很严格。只因它憎恶副作用:写入到一个文件或一个套接字是一件需谨慎的事,你不能随心所欲就这么做。
我使用Haskell语言编程的体会之一就是,Haskell可以通过许多奇淫技巧来解决自身的局限性。这有点像设计师给自己挖一个陷阱,然后再愉快寻找解决之道(尽管这问题是他们自己创造的)。
最后我想说,我看了足够多的Haskell代码小片段,这让我看到了Haskell的全景。我所看到的,让我感到惊讶。
Haskell 让你精确地控制副作用。不只是避免,而是不惜一切代价,因为它首要做的事就是:控制他们——只在你真正知道并需要它们的地方,才能使用它。
你可以只看一眼函数,就能立即知道它会产生什么副作用,不管其它函数如何调用它。
你也可以编写你想要的有副作用的函数,只有这些副作用(没有其它不想要的副作用);写新的代码时,不会破坏已有的代码(这不是你想要的)。
由于我在其他语言的BUG,都是从不必要的代码块之间的相互作用而产生的副作用所引起的,这是一个很大的优势。
以下是对它如何工作的一个高层次的概述,忽略了一些“为什么”细节。
Hakell 有一套非常严格的类型系统。每一样东西都有一“类型签名”,如果类型签名不一致,编译器会提示你错误。
Hakell 代码是纯净的:函数不能修改全局变量,即使是它们自己拥有的参数。函数能够修改的东西
只有它的输出。
简单说来,这意味着通过查看类型签名,你就能够大概知道,函数能做什么。你也能够知道函数不能做什么:如果它没有涉及到类型签名,它就不会发生!
一些类型方面的示例(注释开始于”–“,类型签名包含”::“):
-- 一个函数需要3个integer参数,返回1个结果
foo :: Int -> Int -> Int -> Int
foo a b c = a + b + c
pi :: Double
pi = 3.14
编译器会检查类型一致性。像"foo pi pi pi"会抛出错误。
类型签名可以有参量。_ 下划线表示这里的值可以是任意类型,对于静态签名它能够始终保持相容性,并且在代码运行时也是相容的。
-- 一个函数接受3个任意类型的参数,返回1个结果
-- 注意,返回类型'仍然'要和参数类型保持一致
ignore :: a -> b -> c -> a
ignore a _ _ = a
-- 一个参量数据类型用于表示‘失败’,有点像 Java/C 中的 NULL
data Maybe a = Just a | Nothing
-- 类型可以有多个参量,相对于 Maybe 这有更多的改进,可以用于表示‘错误信息’
data Either a b = Left a | Right b
-- 此'单词'用于填充参量类型:更多的容器,举例
data Vector a
(!) :: Vector a -> Int -> a
data Map k a
insert :: Ord k => k -> a -> Map k a -> Map k a
“Ord k => " 是什么东西?Map 抽象出来的'键'就像树的'索引'一样,想根据'键'创建树,你就得想办法去匹配‘键’。
这意味着不是所有的类型都能为“K”工作:只有它们抽象成 Ord “typeclass类型类"才能匹配。
'类型类'有点像 java 的中的 interface(接口),除了它是惰性的特点外。Haskell 有很多'类型类':常见的有 “Eq”, “Ord”, “Show”, “Read”, “Num”, “Fractional” …
在类型签名时,'类型类'可用来限制参量类型;创建常用的函数时,只要其类型是正确的'类型类'实例,这些函数就可工作于所有类型。
我们知道所有像这样的函数 “myFunc :: String -> Int -> Int”,其返回值就只能是一个数字( 而不能写入磁盘,打开窗口,或窃取你的密码,并发邮件给我 )这是很了不起的。
即便函数能够这么做(指上面提到的)也是无用的。如果我想偷密码会发生什么呢?我的代码需要副作用(才能这么工作),但是副作用会修改结果以外的东西,因此这函数是不纯净的。:(
不管怎样,Haskell的类型系统非常强大。纯净的函数可以创建任何如你所想的类型的结果:为什么不创建一种类型,用它来描述副作用?
-- 副作用包括与其它计算机一起工作时的'写日志'
-- Monoid(独异点) typeclass 描述 类型可以是一个 combined/collected(组合/收集)
data Writer w a
tell :: Monoid w => w -> Writer w ()
runWriter :: Monoid w => Writer w a -> (w, a)
-- 副作用包括与其它计算机一起工作时'操纵一些状态'
data State s a
set :: s -> State s ()
get :: State s s
-- 副作用包括'顺序的解析一条String(字符串)'
data Parser a
runParser :: String -> Parser a -> a
pByte :: Parser Word8
pWord :: Parser Word16
pDWord :: Parser Word32
-- 副作用包括'修改一个游戏的状态'
data GameState a
players :: GameState [(Player,Position)]
move :: Player -> Position -> GameState ()
kill :: Player -> GameState ()
-- 副作用还包括(不管你喜不喜欢这么做)
-- file access(文件访问), network access(网络访问), direct memory access(目录内存访问), 等等
data IO a
-- 唯一能够操作的就是你的 operating system(操作系统):
-- "main" 类型, 也就是你程序的入口点,像这样
main :: IO Int
这些类型可以描述一些副作用,但仍然符合纯洁性,因为除了返回类型以外,它没有修改任何东西。实际计算运行时,我们需要一种方法来将多个函数序列化在一起。
想一想 “;” 分号在 C/Java/等等中的用途:它能够区分开两个函数,并且能够让一个函数运行结束时,开始另一个函数。
或者将它们结合成一个大的函数再跑起来。等等 …
一些行为被命名为 “Monad(单分体)” 的'类型类'来描述。所有是"Monad(单分体)” 实例的类型(Types),在计算时都会被链接在一起。
Hakell中的Monad(单分体)和一些其它的语法糖,计算时会产生副作用的,必需用一种格式来写!
-- 解析一 D2 报文(数据包)
data Packet = RightSkill Int Int | LeftSkill Int Int | ...
d2packet :: Parser (Maybe Packet)
d2packet = do
pid <- pByte
case pid of
0x05 -> do
x <- pWord
y <- pWord
return (Just (LeftSkill x y))
0x0c -> do
x <- pWord
y <- pWord
return (Just (RightSkill x y))
otherwise -> return Nothing
在此会产生什么?根本上这和在 C/Java/等其它语言里是一样的。放在函数序列里的函数一个接着一个运行,它们通过一些诸如 “if” “then” “case” 等控制流标识符来控制。
关键的区别在于,这些语言在 “IO” 内跑计算。任何副作用都有可能产生。 Haskell 可以在任何"单分体"内跑它们:编译器的类型签名会告诉你可能存在什么副作用。
IO 有点笨,因为它可做任何像它的事情。
大多数 monad(单分体)都能非常具体而准确的说明它们所包含的副作用:IO 体现了这一切。
有一些方法可以改进它。尽管这很琐碎,编写新的monad如:
data NetIO a
runNetIO :: NetIO a -> IO a
connect :: IP -> Port -> NetIO Socket
send :: Socket -> String -> NetIO Int
recv :: Socket -> Timeout -> NetIO String
-- etc...
你可以对结果指定你所想要的副作用,而拒绝其它的(副作用)。仅花一点心思通过签名告诉函数你想要做什么,更重要的是:告诉它们永远不要做什么。
后果看起来令人吃惊。我用其他语言编程时许多的BUG都是结果丢失的副作用造成的。理解不熟悉的代码;处理不可信赖的代码;
多线程;因此控制和找出副作用在很多地方都是非常重要的!
副作用可以被精确跟踪和控制这个观点,已经改变了我对其它所有编程语言的看法,以及如何设计/考虑软件。
当然还有一些骗局:CPU和内存不是无限的,所以使用他们时可能产生副作用。如果你所定义的函数没有考虑到所有的无效输入可能会引起崩溃,并且类型签名也不会有任何提示;
对于签名,还有一个很少使用,但有时又很有用的函数“unsafePerformIO :: IO a → a”.
简单说来,此函数保证不'作弊'是很强悍的,并且它能够改变你的编程方式。