Typeclass语法糖 2012-05-10
你想让你的代码具备更高的复用性,比如说你写了一个牛逼的排序函数 sort ,你不希望它只能对整数或是字符串排序,你希望它能对所有类型排序,也就是:
但是你的排序函数并不是真的能对所有类型排序,被排序的类型至少要支持比较操作,于是我们改成这样:
data Ordering = GT | EQ| LT
sort :: (a -> a -> Ordering) -> [a] -> [a]
通过让使用者主动提供比较函数,我们的函数可以支持尽可能多的类型。
又假设你想写一个网络服务程序,你又不想依赖特定的传输协议,我们也可以用类似的做法,只不过刚才是一个函数,这次变成一组:
data Connection = Connection
{ recv :: IO ByteString
, send :: ByteString -> IO Int
, close :: IO ()
}
service :: Connection -> ... -> IO ()
而具体传输协议的实现大概就是这样:
tcpConn :: Socket -> Connection
tcpConn sock = Connection
{ recv = Sock.recv sock
, send = Sock.send sock
, close = Sock.close sock
}
再举一个例子,你想写一个hash map,其中key需要满足两个条件,一个是可以被hash,一个是可以比较,按照上面的做法,我们可能会这么写:
data IsKey key = IsKey key
{ hash :: key -> Int
, compare :: key -> key -> Ordering
}
lookup :: IsKey k -> k -> HashMap k v -> Maybe v
insert :: IsKey k -> k -> v -> HashMap k v -> HashMap k v
-- 此处省略若干操作
这里就存在一个问题,没有人阻止我们对同一个map,传入不同的函数实现,比如不同的哈希算法,不同的比较实现,这样我们的数据结构就悲剧了。
Haskell的typeclass语法糖本质上就是隐式传入的一组函数,只不过通过与类型系统的结合,可以保证同一个类型针对同一个接口只有一个实现,从而避免了上面这个问题。
比如hash map的例子,用typeclass写就是这样的:
class IsKey a where
hash :: a -> Int
compare :: a -> a -> Ordering
lookup :: IsKey k => k -> HashMap k v -> Maybe v
insert :: IsKey k => k -> v -> HashMap k v -> HashMap k v
为了让typeclass更好复用,实际上是这样的:
class Hashable a where
hash :: a -> Int
class Ord a where
compare :: a -> a -> Ordering
lookup :: (Hashable k, Ord k) => k -> HashMap k v -> Maybe v
insert :: (Hashable k, Ord k) => k -> v -> HashMap k v -> HashMap k v
不过,语法糖也并不总是比原始语法更好用,语法糖用得别扭的时候考虑一下更原始的方案,也许会有新思路。
利用类型系统表达约束 2012-05-04
看邮件列表有一个回答很精彩,是一个很典型的利用类型系统表达约束的案例,故翻译过来。
原讨论:[http://article.gmane.org/gmane.comp.lang.haskell.cafe/98103]
问题:
我有一个这样的程序:
data B = B Int
data A = Safe Int | Unsafe Int
createB :: A -> B
createB (Safe i) = B i
createB (Unsafe i) = error "禁止出现"
问题是,使用 Unsafe 调用 createB 的情况只能在运行时才能检查,而如果去掉第二条分支,又变成了模式匹配不完备的错误了 :-(
有没有办法把它变成编译错误?
以下答复使用文学化Haskell(literate Haskell)写就。
{-# LANGUAGE DataKinds, KindSignatures, GADTs #-}
要让类型系统介入进来,关键在于把信息放在类型系统能看到的地方,也就是类型签名里面。
所以我们要把 A 类型改成这样,让 Safe/Unsafe 的信息出现在里面:
data A safety = A Int
这就是所谓的“phantom类型”了,因为 safety 类型变量只出现的类型定义的左边。B的类型可以保持不变:
data B = B Int
现在,我们需要表达 "Safe" 和 "Unsafe":
data Safe
data Unsafe
通常数据类型定义会有一个或多个数据构造器。而这两个类型没有数据构造器,因为我们只打算把他们当作phantom类型参数用,不需要用到他们的值。我们需要两个独立的类型,是因为我们想在编译时区分两种情况。如果只定义一个类型带两个构造器的话,就没办法在编译时获得足够的约束能力了。
现在我们再定义两个函数把值标记成 Safe 或者 Unsafe:
unsafe :: A safety -> A Unsafe
unsafe (A x) = (A x)
safe :: A safety -> A Safe
safe (A x) = (A x)
然后我们把 createB 改成只接受 Safe 参数:
createB :: A Safe -> B
createB (A x) = B x
这样,我们就只能传给它 Safe 的参数:
b :: B
b = createB (safe (A 1))
而不能传 Unsafe 的参数:
{-
b2 :: B
b2 = createB (unsafe (A 1))
编译错误:
Couldn't match expected type `Safe' with actual type `Unsafe'
Expected type: A Safe
Actual type: A Unsafe
-}
可惜,我们还是可以给 createB 传没标记过的值:
b3 :: B
b3 = createB (A 1)
有时候这是个好事,不过针对楼主的问题,应该是不想这种情况发生。有一个方案是不要导出 A 这个构造器,同时导出这样两个函数:
unsafeA :: Int -> A Unsafe
unsafeA x = (A x)
safeA :: Int -> A Safe
safeA x = (A x)
如果只能通过这两个函数创建类型 A 的值的话,那就不会存在没标记过的值了。
目前这个方案可以让我们把值标记成 Safe 或 Unsafe,并在编译时阻止某些函数的调用。
然而,要想写一个函数同时对两种情况进行处理却很麻烦,需要建个type class(译注:可以作为练习)。
不如还是把 A 改回成两个构造器的版本:
] data A' safety = SafeA' Int | UnsafeA' Int
现在,我们需要解决一个棘手的问题,就是如何保证 SafeA' 构造出来的值会带上phantom类型 Safe ,而 UnsafeA' 构造出来的值带phantom类型 Unsafe ?
要解决这个问题就要用 GADTs 类型扩展了,我们可以这么写:
data A' safety where
UnsafeInt :: Int -> A' Unsafe
SafeInt :: Int -> A' Safe
这个定义和常规的数据类型定义很类似:
] data A' safety
] = UnsafeInt Int
] | SafeInt Int
但在 GADT 版本里面,我们可以指定当使用 UnsafeInt 的时候,phantom类型变量一定是 Unsafe ,而用 SafeInt 的时候一定是 Safe 。
这样就把上面说的两个问题都解决了,我们既可以对safe和unsafe两个构造器进行模式匹配,也可以保证 A' 类型一定会被标记成"Safe"或"Unsafe"。如果我们确实想要不标记的值,我们可以加一个构造器:
UnknownInt :: Int -> A' safety
现在我们可以把 createB 改成这样了:
createB' :: A' Safe -> B
createB' (SafeInt i) = B i
这里, createB' 的定义是完备的,因为编译器知道它的参数不可能是 UnsafeInt 。如果你非要加上:
] createB' (UnsafeInt i) = B i
会得到编译错误:
Couldn't match type `Safe' with `Unsafe'
Inaccessible code in
a pattern with constructor
UnsafeInt :: Int -> A' Unsafe,
到现在, A and A' 两个版本都还存在的一个问题是,phantom类型变量可以是任何类型。比如我们可以这么写:
nonsense :: A' Char
nonsense = UnknownInt 1
我们只希望支持Safe和Unsafe,但 A' Char 也是一个合法——但是不合理的类型。
GHC 7.4 里面我们可以使用数据类型提升来约束phantom类型参数能接受的类型。
我们先定义一个普通的数据类型:
data Safety = IsSafe | IsUnsafe
只要启用了 DataKind 扩展,我们就可以把这个类型用作phantom类型参数的签名。这样,类型 Safety 会自动变成kind Safety ,而数据构造器 IsSafe 和 IsUnsafe 自动变成类型构造器。现在我们就可以这么写:
data Alpha (safetype :: Safety) where
SafeThing :: Int -> Alpha IsSafe
UnsafeThing :: Int -> Alpha IsUnsafe
UnknownThing :: Int -> Alpha safetype
然后,我们可以这么写:
foo :: Alpha IsUnsafe
foo = UnknownThing 1
但是,如果我们尝试这么写的话:
] foo' :: Alpha Char
] foo' = UnknownThing 1
就会得到一个编译错误:
Kind mis-match
The first argument of `Alpha' should have kind `Safety',
but `Char' has kind `*'
In the type signature for foo': foo' :: Alpha Char
希望这些能帮到你!
warp静态文件服务器评测 2011-11-12
安装
安装GHC和Cabal,参考: http://book.realworldhaskell.org/read/installing-ghc-and-haskell-libraries.html
使用Cabal安装 Warp 服务器和 wai-app-static ,Cabal将自动下载安装依赖的一些库:
cabal install warp wai-app-static
编写我们的静态文件服务器:
import Control.Applicative ( (<$>) )
import Data.Maybe (fromMaybe, listToMaybe)
import System.Environment (getArgs)
import Network.Wai.Handler.Warp (run)
import Network.Wai.Application.Static ( staticApp
, defaultFileServerSettings)
main :: IO ()
main = do
port <- read . fromMaybe "3000" . listToMaybe <$> getArgs
run port $ staticApp defaultFileServerSettings
特性
一个完善的静态文件服务器:
- 能以高阶函数的方式定制文件查找逻辑
- 根据文件扩展名产生mimetype
- 304响应,支持If-Modified-Since,支持If-None-Match匹配文件哈希值
- 目录末尾自动添加'/' ,自动查找可配置的index文件等等。
性能
- 环境:Thinkpad X61, T8100 双核,Linux 2.6.38,GHC 7.2.1
- 编译选项: ghc -O3 -threaded Main.hs
- 执行选项: ./Main +RTS -N1 -qa
- 对照nginx:worker 1, sendfile on
因为我这个破本只有两个核,一个用来运行 ab ,一个可以用来运行web server,所以上面都只配置一个worker进程。
测试命令: ab -c 100 -n 100000 -r http://localhost:3000/test.html
测试6次,平均每秒请求数分别为:
1 7174.23
2 6946.33
3 6120.31
4 6819.33
5 7373.51
6 6776.65
对比nginx:
1 13543
2 13601.69
3 13512.38
4 13654.39
5 13680.97
6 13630.64
warp pong test
顺便再测下warp,把静态文件app去掉,换上一个最简单的app:
{-# LANGUAGE OverloadedStrings #-}
import Control.Applicative ( (<$>) )
import Data.Maybe (fromMaybe, listToMaybe)
import System.Environment (getArgs)
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp (run)
import Blaze.ByteString.Builder (fromByteString)
pong :: Application
pong req = return $ ResponseBuilder
statusOK
[("Content-Type", "text/plain")]
(fromByteString "pong")
main :: IO ()
main = do
port <- read . fromMaybe "3000" . listToMaybe <$> getArgs
run port pong
相同条件下测试,平均每秒请求数为:
1 22184.93
2 22232.89
3 22150.83
4 22189.02
5 22267.33
6 22125.08
nginx的话,好像没有办法构造一个等价测试案例,我配置了一个最简单的server block:
server {
server_name localhost;
location = / {
}
}
然后测试 http://localhost/ 这个 404 的响应,结果跟上面nginx返回静态文件的结果类似。
结论
warp pong 测试结果很惊人,看来 static app 还有不小的优化空间的。nginx主要是用来做个对照,没看过nginx代码,不一定公平。期待大家在不同环境下去测试测试,看结果如何。
无题 2011-11-03
写 parser 的时候需要写这样的代码:
其中 pred 是个判别函数,签名为: Char -> Bool 。 takeTill 的作用就是一直解析到 pred 返回 True 为止。
比如你要解析到下一个 > 或者 = ,你就写:
想解析到下一个空白字符为止,就写:
想解析各种空白字符呢,就在上面的基础上取个反:
这个例子是想提醒大家 isSpace 是个函数,所以这里需要进行函数组合,而不是直接调用 not 。
那我今天想说的是什么呢。现在我想解析到下一个 > 或 = 或空白字符为止,也就是说需要把前两个拼起来,直接写起来是这样的:
takeTill (\c -> inClass ">=" c || isSpace c)
也不麻烦,只不过对于患有 代码洁癖 的我来说,视觉上还不太给力。于是,我继续重构如下:
takeTill (inClass ">=" ||. isSpace)
多实现一个组合函数 ||. :
(||.) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
(||.) f g a = f a || g a
把两个判别函数组合成一个新的判别函数。
为了将它推广到其他的组合操作,我们进一步泛化一个通用函数,姑且取名叫 fn 吧 :
fn :: (b -> b -> b) -> (a -> b) -> (a -> b) -> a -> b
fn op f g a = f a `op` g a
前面的 ||. 就成为:
大功告成,正当我准备好好欣赏一下最终的产物 fn 的时候,却突然发现, fn 的作用不就是把 b 层面的二元函数提升到 (a -> b) 层面么,正如 || 和 ||. 都是或操作,只不过一个作用在 Bool 值层面,一个作用在 a -> Bool 判别函数层面。如此通用的概念,我意识到我很可能重造轮子了。
于是我请 lambdabot mm帮我诊断一下:
<huangyi> @pl fn op g k a = g a `op` k a
<lambdabot> fn = liftM2
果然,小mm告诉我, fn 其实就是 liftM2 。 liftM2 是专门用来把二元函数提升到 Monad 中去的,而 ((->) a) 正是 Monad 的实例。
instance Monad ((->) a) where
return = const
-- (>>=) :: (a -> b) -> (b -> a -> c) -> (a -> c)
f >>= g = g . f
liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
liftM2 op ma mb = do
a <- ma
b <- mb
return (a `op` b)
其实只需要提升一下抽象层次的话,还不需动用 Monad 这样的大杀器, Applicative 也可以搞定。
instance Applicative ((->) a) where
-- <$> :: (b -> c) -> (a -> b) -> (a -> c)
f <$> g = f . g
-- (<*>) :: (a -> b -> c) -> (a -> b) -> (a -> c)
f <*> g = \a -> (f a) (g a)
用 Applicative 的话,前面的 fn 就等价于 liftA2 了。
liftA2 :: Application f => (a -> b -> c) -> f a -> f b -> f c
liftA2 op fa fb = op <$> fa <*> fb
绕了一圈,最后还是没逃出Haskell最基本的框框。