如何对 Haskell 中的类型执行 scatter/gather 操作?

How can I perform a scatter/gather operation on types in Haskell?

我有一棵包含不同类型节点的树。这些使用数据类型标记:

data Wrapping = A Int
              | B String 

我想写两个函数:

scatter :: Wrapping -> a
gather :: a -> Output

我的想法是我可以使用 (scatter.gather) :: Wrapping -> Output。 scatter 和 gather 函数当然会有几个不同的变体(每个 scatter 变体都有一个独特的 Wrappingn 数据类型,但中间类型的集合总是相同的),我希望能够干净地组合它们。

我遇到的问题是类型参数 a 并不是真正自由的,它是一组小的显式类型(这里是 {Int,String})。如果我尝试将到目前为止的内容编码为 Haskell 类型 classes,那么我会得到:

{-# LANGUAGE FlexibleInstances #-}
data Wrapping = A Int | B String

class Fanin a where
        gather :: a -> String
instance Fanin Int where
        gather x = show x
instance Fanin String where
        gather x = x

class Fanout a where
        scatter :: Fanout a => Wrapping -> a
instance Fanout Int where
        scatter (A n) = n
instance Fanout String where
        scatter (B x) = x

combined = gather.scatter

这两个 classes 类型检查很好,但显然最后一行会抛出错误,因为 ghc 知道类型参数在每种情况下都匹配,只有在我定义的两个情况下。我尝试了从另一个扩展一个 class 的各种组合:

class Fanin a => Fanout a where ...
class Fanout a => Fanin a where ...

最后我查看了 GADT 和存在类型来解决这个问题,但我在黑暗中跌跌撞撞。我找不到向 GHC 表达合法合格类型签名的方法,我在其中尝试了以下组合:

{-# LANGUAGE RankNTypes #-}
class (forall a. Fanout a) => Fanin a where
class (forall a. Fanin a) => Fanout a where

问题:如何向 GHC 表达我想将 a 限制为集合中的两种类型?

我觉得解决方案在于我看过的一种技术,但我太迷茫了,看不出它是什么...

如果我对你的理解正确,你需要如下内容:

module Main ( main ) where

-- Different kinds of wrapper data types
data WrapperA = A Int | B String
data WrapperB = C Int | D Float

-- A single intermediate data type (with phantom type)
data Intermediate a = E Int | F String

-- Generic scatter and gather functions
class Wrapped a where
  scatter :: Wrapped a => a -> Intermediate a
  gather :: Wrapped a => Intermediate a -> String

-- Specific scatter and gather implementations
instance Wrapped WrapperA where
  scatter (A i) = E i
  scatter (B s) = F s
  gather (E i) = show i
  gather (F s) = s

instance Wrapped WrapperB where
  scatter (C i) = E i
  scatter (D f) = F $ show f
  gather (E i) = show i
  gather (F s) = s ++ " was a float"

-- Beautiful composability
combined :: Wrapped a => a -> String
combined = gather . scatter

wrapperAexample1 = A 10
wrapperAexample2 = B "testing"
wrapperBexample1 = C 11
wrapperBexample2 = D 12.4

main :: IO ()
main = do print $ combined wrapperAexample1
          print $ combined wrapperAexample2
          print $ combined wrapperBexample1
          print $ combined wrapperBexample2

主要问题似乎是你有一个中间类型,它可以有不同种类的内容,但这对于不同的包装器来说是不变的。不过,根据包装器的种类,您希望 gather 函数的行为有所不同。

为此,我会定义 Intermediate 类型来指定可以在中间阶段保存的值的种类,并给它一个幻像类型参数(以记住它起源于哪种包装器从)。然后,您可以定义一个 class 来保存 scattergather 函数,并为不同类型的包装器定义不同的函数。

上面的代码对我来说编译没有错误,并给出以下输出:

"10"
"testing"
"11"
"12.4 was a float"

如您所见,WrapperB/D Float 输入的处理方式与 WrapperA/B String 不同(它被标记为浮点值,即使在它已被转换为字符串)。这是因为在 Intermediate 表示中,记住原点是 WrapperB:一个是 Intermediate WrapperA 类型,另一个是 Intermediate WrapperB.

另一方面,如果您实际上不希望 gather 函数对不同的包装器有不同的行为,您可以简单地将其从 class 中取出并取出 phantom 类型。让ghc知道中间阶段的类型可以是IntString的最简单方法在我看来仍然定义了类似Intermediate类型的东西,而不是只使用a.

The idea is that I can use (scatter.gather) :: Wrapping -> Output. There will of course be several different variations on both the scatter and the gather function (with each scatter variant having a unique Wrappingn datatype, but the set of intermediate types will always be the same) and I want to be able to cleanly compose them.

如果我没理解错的话,你想要不同的 Wrapping 类型,但中间 a 类型一直是 Either Int String。我们可以在 classes:

中反映这些信息
data Wrapping = A Int
              | B String

class Fanout wrap where
    scatter :: wrap -> Either Int String

instance Fanout Wrapping where
    scatter (A n) = Left n
    scatter (B str) = Right str

class Fanin output where
    gather :: Either Int String -> output

instance Fanin String where
    gather = either show id

combined :: Wrapping -> String
combined = gather . scatter    

此外,根据我从问题中收集到的信息,这个用例似乎并不特别适合输入 classes。特别是,我们可以去掉 Fanin,然后 combined = either show id . scatter 在我看来比之前的定义更好看。

仅当单个 Either Int String -> aa -> Either Int String 函数对每个 a 有意义并且您希望强制执行时,类型 class 解决方案才有意义这个。