将函数与Haskell中的类型相关联
假设您有一个序列化器/反序列化器类型类
Suppose you have a serializer/deserializer type class
class SerDes a where
ser :: a -> ByteString
des :: ByteString -> a
事实证明,对于每种类型a
,例如
and it turns out that it's crucial to have a special helper function for each type a
, e.g.
compress :: ByteString -> ByteString -- actually varies with the original type
我将compress
视为我想与作为SerDes
的每个a
关联的功能. (关联"一词可能是一个错误的选择,这也是互联网搜索无法产生任何结果的原因.)
I see compress
as a function that I would like to associate with each a
that is a SerDes
. (The word "associate" is probably a bad choice, and the reason why internet searches yield nothing.)
该示例并不像它看起来的那样虚构,例如,当decompress
是可选的时
串行器/解串器的功能. (是的,可以通过增加
ser
,带有一个控制压缩的开关,ser:: a -> Bool -> ByteString
,或者更好地使用Config
记录.但是,让我们坚持这个例子.)
The example is not as contrived as it looks, for example when decompress
is an optional
feature of the serializer/deserializer. (Yes, the helper could be avoided by augmenting
ser
with a switch that controls the compression, ser:: a -> Bool -> ByteString
, or better use a Config
record. But let's stick with the example.)
一种实现此目的的方法是虚拟"类,即单例:
One way to do this is a 'dummy' class, a singleton:
data For a = For
然后这将起作用:
class SerDes a where
ser :: a -> ByteString
des :: ByteString -> a
compress :: For a -> ByteString -> ByteString
和a
的compress
将被实例化为
compress (For :: For MyType) input = ...
另一种有点不寻常的方法是将所有功能都保留在记录中.
Another way, somewhat unusual, would be to stick all the functions in a record.
data SerDes a = SerDes { ser :: a -> ByteString
, des :: ByteString -> a
, compress :: ByteString -> ByteString
}
是否还有其他关联"方式? compress
函数的类型为a
?
Are there any other ways to "associate" the compress
function with the type a
?
您的For a
类型在库中称为Proxy a
.
import Data.Proxy
class SerDes a where
ser :: a -> ByteString
des :: ByteString -> a
compress :: Proxy a -> ByteString -> ByteString
有时,它通常被泛化为通用的proxy
类型变量.
Sometimes this is generalized to a generic proxy
type variable.
class SerDes a where
ser :: a -> ByteString
des :: ByteString -> a
compress :: proxy a -> ByteString -> ByteString
还有另一种选择,类似于代理.除了强制将a
添加到参数之外,还可以使用Tagged
将a
添加到结果类型:
There is another option, similar to proxies. Instead of forcibly adding a
to the arguments, one can add a
to the result type using Tagged
:
import Data.Tagged
class SerDes a where
ser :: a -> ByteString
des :: ByteString -> a
compress :: ByteString -> Tagged a ByteString
这需要用作unTagged (compress someByteString :: Tagged T ByteString)
来告诉编译器我们想要T
的compress
函数.
This needs to be used as unTagged (compress someByteString :: Tagged T ByteString)
to tell the compiler we want the compress
function for T
.
我个人不喜欢代理和标签.过去,GHC不允许使用其他更简单的解决方案时就需要使用它们,但现在不应该再使用它们.
Personally, I'm not a fan of proxies and tags. They were needed in the past when GHC did not allow another simpler solution, but right now they should no longer be used.
现代的方法是打开无害的扩展AllowAmbiguousTypes
和TypeApplications
并只需编写您想要的类
The modern approach is to turn on the harmless extensions AllowAmbiguousTypes
and TypeApplications
and simply write your wanted class
class SerDes a where
ser :: a -> ByteString
des :: ByteString -> a
compress :: ByteString -> ByteString
在这种方法中,代替调用compress (Proxy :: Proxy T) someByteString
,我们将需要使用较短的compress @T someByteString
,在此我们显式地传递所需的类型a
". (在这种情况下为T
),因此选择所需的compress
.
In this approach, instead of calling compress (Proxy :: Proxy T) someByteString
we will need to use the shorter compress @T someByteString
where we explicitly "pass the type a
we want" (T
in this case), so to select the wanted compress
.
完整示例:
{-# LANGUAGE AllowAmbiguousTypes, TypeApplications, OverloadedStrings #-}
import Data.ByteString as BS
class SerDes a where
ser :: a -> ByteString
des :: ByteString -> a
compress :: ByteString -> ByteString
-- bogus implementation to show everything type checks
instance SerDes Int where
ser _ = "int"
des _ = 42
compress bs = BS.tail bs
-- bogus implementation to show everything type checks
instance SerDes Bool where
ser _ = "bool"
des _ = True
compress bs = bs <> bs
main :: IO ()
main = BS.putStrLn (compress @Int "hello" <> compress @Bool "world")
-- output: elloworldworld