Haskell中的Eta减少

Posted

技术标签:

【中文标题】Haskell中的Eta减少【英文标题】:Eta reduction in haskell 【发布时间】:2018-04-13 01:18:56 【问题描述】:

我在haskell中尝试了减半这个功能,我想表达例如:

mySum x y = x + y
mySum x y = (+) x y
mySum x = (+) x
mySum = (+) -- it's Messi's goal! 

我的函数有点复杂,但我真的做不到,我到处看看,我知道有一些技巧,比如修改右侧,并使用flip。我试过了,我卡在这里:

zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith' f x y  = map  (uncurry f) (zip x y) 

步骤:

zipWith' f x y  = map  (uncurry f) (zip x y) 
zipWith' f x y  = flip  map  (zip x y) (uncurry f)
zipWith' f x y  = flip  map  (zip x y) $ uncurry f

然后我不知道如何继续......

我正在寻找一个可以逐步解释如何实现“梅西的目标”的答案,我知道有很多问题要问,所以我会尽快添加赏金以感谢努力

【问题讨论】:

您不能在第一个参数上进行 eta 合同,只能在最后一个参数上进行合同,就像您为 mySum 所做的那样。 pointfree.io 说zipWith' = (. zip) . (.) . map . uncurry,顺便说一句。还不错,但不完全可读。 @chi 感谢您的评论,我更新了答案,因为如果我想用正确的答案再次学习,我将无法自己再次学习。不过谢谢! 这就是为什么定义另一个更好的方法,手动实现zipWith,然后简单地执行zip = zipWith (,)... This answer 演示了当你想这样做时要遵循的一些机械规则 【参考方案1】:
zipWith' f x y = map (uncurry f) (zip x y)

将应用程序重写为组合和 eta-reduce:

-- \y -> let g = map (uncurry f); h = zip x in (g . h) y
-- let g = map (uncurry f); h = zip x in g . h

zipWith' f x = map (uncurry f) . zip x

将中缀改写为前缀:

-- g . h = (.) g h

zipWith' f x = (.) (map (uncurry f)) (zip x)

将应用程序重写为组合和 eta-reduce:

-- \x -> let g = (.) (map (uncurry f)); h = zip in (g . h) x
-- let g = (.) (map (uncurry f)); h = zip in g . h

zipWith' f = (.) (map (uncurry f)) . zip

将中缀改写为前缀:

-- g . h = (.) g h

zipWith' f = (.) ((.) (map (uncurry f))) zip

使用flipf 移动到右侧:

-- flip f x y = f y x

zipWith' f = flip (.) zip ((.) (map (uncurry f)))

将应用程序重写为组合:

-- g (h (i x)) = (g . h . i) x

zipWith' f = flip (.) zip (((.) . map . uncurry) f)

将应用程序重写为组合和 eta-reduce:

-- \f -> let g = flip (.) zip; h = (.) . map . uncurry in (g . h) f
-- let g = flip (.) zip; h = (.) . map . uncurry in g . h

zipWith' = (flip (.) zip) . ((.) . map . uncurry)

删除多余的括号:

zipWith' = flip (.) zip . (.) . map . uncurry

如果你愿意,可以简化为中缀:

zipWith' = (. zip) . (.) . map . uncurry

不过,这个结果不太可读。


通常在编写完全无点代码时,您希望利用Control.Arrow 中的-> 应用程序和箭头组合器。与其尝试编写像 \ f x y -> ... 这样的函数,不如先将参数分组到元组中,以使它们更容易重新排列和传递。在这种情况下,我将使用\ (f, (x, y)) -> ...

\ (f, (x, y)) -> map (uncurry f) (zip x y)

我们可以通过将uncurry应用于zip来消除(x, y)的解包:

\ (f, (x, y)) -> map (uncurry f) (uncurry zip (x, y))
\ (f, xy) -> map (uncurry f) (uncurry zip xy)

现在我们有一个简单的案例:将两个函数(uncurryuncurry zip)应用于两个参数(fxy),然后将结果组合(与 map)。为此,我们可以使用来自Control.Arrow*** 组合子,类型为:

(***) :: Arrow a => a b c -> a b' c' -> a (b, b') (c, c')

专门用于功能,即:

(***) @(->) :: (b -> c) -> (b' -> c') -> (b, b') -> (c, c')

这只是让我们将一个函数应用于一对中的每个元素。完美!

uncurry *** uncurry zip
  :: (a -> b -> c, ([x], [y])) -> ((a, b) -> c, [(x, y)])

您可以将uncurry f 视为使用函数f 组合一对元素。所以这里我们可以使用uncurry map合并结果:

uncurry map . (uncurry *** uncurry zip)
  :: (a -> b -> c, ([a], [b])) -> [c]

您可以将curry 视为将元组上的函数转换为多参数函数。这里我们有两层元组,外层(f, xy) 和内层(x, y)。我们可以用curry解开外面的那个:

curry $ uncurry map . (uncurry *** uncurry zip)
  :: (a -> b -> c) -> ([a], [b]) -> [c]

现在,您可以将 -> 应用程序中的 fmap f 视为“跳过”第一个参数:

fmap @((->) _) :: (a -> b) -> (t -> a) -> t -> b

所以我们可以使用fmap curry解包第二个元组:

fmap curry $ curry $ uncurry map . (uncurry *** uncurry zip)
  :: (a -> b -> c) -> [a] -> [b] -> [c]

我们完成了!或者不完全。在编写无点代码时,将事物分解为许多名称更清晰的可重用小函数是值得的,例如:

zipWith' = untuple2 $ combineWith map apply zipped
  where
    untuple2 = fmap curry . curry
    combineWith f g h = uncurry f . (g *** h)
    apply = uncurry
    zipped = uncurry zip

然而,虽然知道这些技术很有用,但所有这些都只是徒劳的伎俩,很容易迷失方向。大多数时候,你应该只在 Haskell 中使用无点风格,当它明显赢得可读性时,并且两者都没有其中的结果比简单的原始版本更清晰:

zipWith' f x y = map (uncurry f) (zip x y)

部分无积分版本:

zipWith' f = map (uncurry f) .: zip
  where (.:) = (.) . (.)

【讨论】:

反问:你是否必须通过所有非生产性的东西才能意识到什么是重要的或简单的? @AndreiBozantan:如果你这样做一次,你可能会更好地了解如何以及何时避免再次这样做:)

以上是关于Haskell中的Eta减少的主要内容,如果未能解决你的问题,请参考以下文章

查看 Haskell 中的缩减步骤

Haskell 在 Scala 中的新类型

在 Haskell 中即时减少列表

Haskell,分析导入库的内存使用情况

Scala中的Haskell“forall”翻译

Haskell 中的模块、包和库有啥区别?