用函数替换Haskell记录有什么好处

时间:2014-12-04 12:35:36

标签: haskell data-structures

我正在阅读关于延续的这个有趣的article,我发现了这个聪明的技巧。在我自然会使用记录的地方,作者使用一个以和类型作为第一个参数的函数。

例如,而不是这样做

data Processor = Processor { processString :: String -> IO ()
                           , processInt :: Int -> IO ()
                           }


processor = Processor (\s -> print $ "Hello "++ s)
                      (\x -> print $ "value" ++ (show x))

我们可以这样做:

data Arg = ArgString String | ArgInt Int
processor :: Arg -> IO ()
processor (ArgString s) = print "Hello" ++ s
processor (ArgInt x) = print "value" ++ (show x)

除了聪明之外,它对简单记录有什么好处? 这是一种常见的模式吗?它有一个名字吗?

3 个答案:

答案 0 :(得分:6)

嗯,这只是一个简单的同构。在ADT代数中:

IO() String ×IO() Int IO() String + Int

RHS的明显好处可能是它只包含IO()一次 - DRY FTW。

答案 1 :(得分:4)

这是一个非常宽松的示例,但您可以将Arg方法视为初始编码,将Processor方法视为最终编码。正如其他人所指出的那样,当他们在许多灯光下观看时,他们是同等的力量;但是,存在一些差异。

  1. 初始编码使我们能够检查"命令"被执行。从某种意义上说,这意味着我们对操作进行了切片,以便输入和输出分开。这允许我们在给定相同输入的情况下选择许多不同的输出。

  2. 最终编码使我们能够更轻松地抽象实现。例如,如果我们有两个Processor类型的值,那么即使两者具有不同的效果或通过不同的方式实现它们的效果,我们也可以相同地对待它们。这种抽象在OO语言中得到普及。

  3. 初始编码启用(在某种意义上)更容易添加新功能,因为我们只需要为Arg类型添加新分支。如果我们有许多不同的方法来构建Processor,那么我们必须更新这些机制。

  4. 老实说,我上面所描述的内容相当紧张。情况是ArgProcessor在某种程度上符合这些模式,但它们并没有以如此显着的方式这样做,以至于真正从这种区别中受益。如果您感兴趣,可能值得研究更多的例子 - 一个好的搜索词是"表达问题"它强调了上述第(2)和(3)点的区别。

答案 2 :(得分:2)

为了扩展leftroundabout的响应,有一种方法可以将函数写为输出输入,因为基数(有多少东西)。因此,例如,如果您考虑基数3的集{0, 1, 2}到基数2的集合{0, 1}的所有映射,您会看到0可以映射到0或1,独立于1映射到0或1,独立于2映射到0或1.当计算函数总数时,我们得到2 * 2 * 2或2 3

以同样的写作方式,求和类型用+编写,产品类型用*写成,有一种可爱的方法可以将其称为Out In1 + In2 = Out In1 * Out In2 ;我们可以将同构写为:

combiner :: (a -> z, b -> z) -> Either a b -> z
combiner (za, zb) e_ab = case e_ab of Left a -> za a; Right b -> zb b

splitter :: (Either a b -> z) -> (a -> z, b -> z)
splitter z_eab = (\a -> z_eab $ Left a, \b -> z_eab $ Right b)

我们可以在您的代码中通过以下方式对其进行修改:

type Processor = Either String Int -> IO ()

那有什么区别?没有多少:

  1. 组合形式要求两者都具有完全相同的尾端。您无法将combiner应用于a -> b -> z类型的内容,因为该a -> (b -> z)解析而b -> zz无法统一。如果您想将a -> b -> zc -> z统一起来,那么您必须首先将该函数发送到(a, b) -> z,这看起来有点像工作 - 当您使用该记录时,这不是问题版本
  2. 分割形式对于应用来说也更简洁;你只需要写fst split a而不是combined $ Left a。但这也意味着你不能轻易地做yz . combined(等效于(yz . fst split, yz . snd split))这样的事情。如果您确实已经定义了Processor记录,那么将其类型扩展到* -> *并将其设为Functor可能是值得的。
  3. 记录通常比sum-type-function更容易参与类型类。
  4. 总和类型看起来更有必要,因此它们可能更清晰易读。例如,如果我向您提供模式withProcState p () [Read path1, Apply (map toUpper), Write path2],则很容易看到它为处理器提供了将path1大写为path2的命令。定义处理器的等价物看起来像procWrite p path2 $ procApply p (map toUpper) $ procRead p path1 (),它仍然非常清晰但不像前一种情况那样令人敬畏。