将函数与 Haskell 中的类型关联

mcm*_*yer 15 haskell record typeclass

假设您有一个序列化器/反序列化器类型类

class SerDes a where
    ser :: a -> ByteString
    des :: ByteString -> a
Run Code Online (Sandbox Code Playgroud)

事实证明,对于每种类型都有一个特殊的辅助函数是至关重要的a,例如

compress :: ByteString -> ByteString     -- actually varies with the original type
Run Code Online (Sandbox Code Playgroud)

我认为compress作为一个功能,我想每个关联a这是一个SerDes。(“associate”这个词可能是一个糟糕的选择,这也是互联网搜索一无所获的原因。)

该示例并不像看起来那样做作,例如何时decompress是串行器/解串器的可选功能。(是的,可以通过增加ser控制压缩的开关来避免使用助手 ser:: a -> Bool -> ByteString,或者更好地使用Config记录。但让我们坚持这个例子。)

一种方法是一个“虚拟”类,一个单例:

data For a = For
Run Code Online (Sandbox Code Playgroud)

然后这将起作用:

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: For a -> ByteString -> ByteString
Run Code Online (Sandbox Code Playgroud)

并且compressfora将被实例化为

compress (For :: For MyType) input = ...
Run Code Online (Sandbox Code Playgroud)

另一种有点不寻常的方法是将所有功能都保存在一个记录中。

data SerDes a = SerDes { ser      :: a -> ByteString
                       , des      :: ByteString -> a
                       , compress :: ByteString -> ByteString 
                       }
Run Code Online (Sandbox Code Playgroud)

还有其他方法可以将compress函数与类型“关联”a吗?

chi*_*chi 20

您的For a类型Proxy a在库中被称为。

import Data.Proxy

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: Proxy a -> ByteString -> ByteString
Run Code Online (Sandbox Code Playgroud)

有时这被推广到泛型proxy类型变量。

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: proxy a -> ByteString -> ByteString
Run Code Online (Sandbox Code Playgroud)

还有另一种选择,类似于代理。a可以a使用Tagged以下命令添加到结果类型,而不是强行添加到参数中:

import Data.Tagged

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: ByteString -> Tagged a ByteString
Run Code Online (Sandbox Code Playgroud)

这需要用来unTagged (compress someByteString :: Tagged T ByteString)告诉编译器我们想要compress函数T


就个人而言,我不喜欢代理和标签。过去 GHC 不允许使用其他更简单的解决方案时需要它们,但现在不应再使用它们。

现代方法是打开无害的扩展AllowAmbiguousTypesTypeApplications然后简单地编写你想要的类

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: ByteString -> ByteString
Run Code Online (Sandbox Code Playgroud)

在这种方法中,compress (Proxy :: Proxy T) someByteString我们需要compress @T someByteString在明确“传递a我们想要的类型”(T在这种情况下)的地方使用较短的而不是调用,以便选择想要的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
Run Code Online (Sandbox Code Playgroud)

  • @leftaroundabout 有趣。过去,我认为我的偏好是少数人,而且许多人仍然更喜欢使用代理。如果情况发生变化,我很高兴听到:)。不管怎样,既然OP用“For a”重新发明了代理,我认为从那开始是合适的。我将编辑最后一部分,以强调这是现代解决方案。 (2认同)