【问题标题】:Associate a function with a type in Haskell将函数与 Haskell 中的类型相关联
【发布时间】:2020-08-31 08:22:30
【问题描述】:

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

class SerDes a where
    ser :: a -> ByteString
    des :: ByteString -> a

事实证明,为每种类型 a 提供一个特殊的辅助函数至关重要,例如

compress :: ByteString -> ByteString     -- actually varies with the original type

我将compress 视为一个函数,我想将它与每个a 关联,即SerDes。 (“关联”这个词可能是一个糟糕的选择,这也是互联网搜索没有结果的原因。)

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

这样做的一种方法是一个“虚拟”类,一个单例:

data For a = For

那么这将起作用:

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: For a -> ByteString -> ByteString

compressa 将被实例化为

compress (For :: For MyType) input = ...

另一种有点不寻常的方法是将所有功能都粘贴到记录中。

data SerDes a = SerDes { ser      :: a -> ByteString
                       , des      :: ByteString -> a
                       , compress :: ByteString -> ByteString 
                       }

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

【问题讨论】:

    标签: haskell record typeclass


    【解决方案1】:

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

    import Data.Proxy
    
    class SerDes a where
        ser      :: a -> ByteString
        des      :: ByteString -> a
        compress :: Proxy a -> ByteString -> ByteString
    

    有时这被概括为一个通用的proxy 类型变量。

    class SerDes a where
        ser      :: a -> ByteString
        des      :: ByteString -> a
        compress :: proxy a -> ByteString -> ByteString
    

    还有另一个选项,类似于代理。可以使用Taggeda 添加到结果类型,而不是强制将a 添加到参数中:

    import Data.Tagged
    
    class SerDes a where
        ser      :: a -> ByteString
        des      :: ByteString -> a
        compress :: ByteString -> Tagged a ByteString
    

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


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

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

    class SerDes a where
        ser      :: a -> ByteString
        des      :: ByteString -> a
        compress :: ByteString -> ByteString
    

    在这种方法中,我们需要使用较短的compress @T someByteString,而不是调用compress (Proxy :: Proxy 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
    

    【讨论】:

    • “就我个人而言,我不喜欢代理和标签”——我也不喜欢,而且我真的不明白你为什么一开始就用它们来回答。我想现在大多数人都会同意TypeApplications 是在现代 Haskell 中做这类事情的方式,而代理/标记值仅具有历史意义。 (事实上​​,Tagged 可能只是过时了,它从来没有很受欢迎;我以前更喜欢它而不是代理,而 TypeApplications 不可用,但现在我再也不会使用它了。代理可以更好地与 TypeApplications 结合使用,简单通过(Proxy @a).)
    • @leftaroundabout 有趣。过去,我认为我的偏好是少数人,而且很多人仍然喜欢使用代理。如果情况发生了变化,我很高兴听到它:)。无论如何,由于 OP 用For a 重新发明了代理,我认为从它开始是合适的。我将编辑最后一部分,以强调这是现代解决方案。
    • 好吧,由于遗留库,代理仍然被大量使用,但我希望这在未来会消失。
    • 我相信仍有一些情况您必须使用Proxy 参数而不是类型应用程序,不是吗?后者绝对是更可取的,除非你真的遇到这种情况,或者除非你需要支持旧的编译器版本。
    • @chi 我的意思是实际上打电话给f。如果您尝试写f (\x -&gt; _),您将无法在_ 中命名a。老实说,我不记得在这种情况下找到 any 方法来引用 lambda 内的a。当使用任意类型的函数时,这变得高度烦人,例如通过singleton的去功能化方法:data HList (f :: a ~&gt; Type) (xs :: [a]) :: Type where { .. }; mapH :: (forall a. f @@ a -&gt; g @@ a) -&gt; HList f xs -&gt; HList g xs。虽然在这种情况下,我们可以设置f, g = TyCon1 Blah 而不是Proxy#,但这有点不合时宜...
    猜你喜欢
    • 1970-01-01
    • 2010-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-10
    • 2018-05-11
    • 2021-07-05
    • 2022-07-15
    相关资源
    最近更新 更多