我正在阅读关于延续的这个有趣的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)
除了聪明之外,它对简单记录有什么好处? 这是一种常见的模式吗?它有一个名字吗?
答案 0 :(得分:6)
嗯,这只是一个简单的同构。在ADT代数中:
IO()
String
×IO()
Int
≅IO()
String
+ Int
RHS的明显好处可能是它只包含IO()
一次 - DRY FTW。
答案 1 :(得分:4)
这是一个非常宽松的示例,但您可以将Arg
方法视为初始编码,将Processor
方法视为最终编码。正如其他人所指出的那样,当他们在许多灯光下观看时,他们是同等的力量;但是,存在一些差异。
初始编码使我们能够检查"命令"被执行。从某种意义上说,这意味着我们对操作进行了切片,以便输入和输出分开。这允许我们在给定相同输入的情况下选择许多不同的输出。
最终编码使我们能够更轻松地抽象实现。例如,如果我们有两个Processor
类型的值,那么即使两者具有不同的效果或通过不同的方式实现它们的效果,我们也可以相同地对待它们。这种抽象在OO语言中得到普及。
初始编码启用(在某种意义上)更容易添加新功能,因为我们只需要为Arg
类型添加新分支。如果我们有许多不同的方法来构建Processor
,那么我们必须更新这些机制。
老实说,我上面所描述的内容相当紧张。情况是Arg
和Processor
在某种程度上符合这些模式,但它们并没有以如此显着的方式这样做,以至于真正从这种区别中受益。如果您感兴趣,可能值得研究更多的例子 - 一个好的搜索词是"表达问题"它强调了上述第(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 ()
那有什么区别?没有多少:
combiner
应用于a -> b -> z
类型的内容,因为该a -> (b -> z)
解析而b -> z
与z
无法统一。如果您想将a -> b -> z
与c -> z
统一起来,那么您必须首先将该函数发送到(a, b) -> z
,这看起来有点像工作 - 当您使用该记录时,这不是问题版本fst split a
而不是combined $ Left a
。但这也意味着你不能轻易地做yz . combined
(等效于(yz . fst split, yz . snd split)
)这样的事情。如果您确实已经定义了Processor
记录,那么将其类型扩展到* -> *
并将其设为Functor
可能是值得的。withProcState p () [Read path1, Apply (map toUpper), Write path2]
,则很容易看到它为处理器提供了将path1大写为path2的命令。定义处理器的等价物看起来像procWrite p path2 $ procApply p (map toUpper) $ procRead p path1 ()
,它仍然非常清晰但不像前一种情况那样令人敬畏。