使用折叠实现“任何”的无点实现:为什么有些策略有效而其他策略无效?

问题描述

tl;博士:

这是“Haskell Programming from First Principles”中的一个练习,它要求读者使用折叠重新实现 any 函数myAny (a -> Bool) -> [a] -> Bool,并且以无点风格(如果可能) .

我被困在这个问题上并打算问一个问题,但在输入我原来的问题的过程中,我想出了以下解决方案:

myAny = curry $ foldr (||) False . uncurry map

我正在努力理解为什么这个有效,以及为什么我的其他尝试失败了背后的更大概念。

我想说明的是,我确实理解 Haskell 中的所有函数都是柯里化的;我可能会松散地使用诸如“函数 x 已应用其所有参数”之类的短语。

具体来说,我的问题是:

问题 1:

为什么

myAny = foldr (||) False . map

不能(就为什么 Haskell/GHC/computation-in-general 不支持这一点而言)以某种方式被告知“延迟组合直到 map 完全应用所有参数”而不使用咖喱/非咖喱模式。

问题 2:

如果咖喱/非咖喱模式有名称。看起来这可能与“组合器”有关,但我对它们只是模糊地熟悉。在我继续完成 HPFFP 之旅时,其他资源会有所帮助!

问题 3:

我采取的第二个策略是将“any”函数 f :: [a] -> Bool 放在“fold”函数中。我得到的最远的是

myAny f = foldr ((||) . f) False

我不确定我是否可以通过此策略进一步实现无点;我认为没有任何方法可以“传递”论点,以便我可以做类似

-- This does not work; it is not syntactically valid
myAny = foldr ((||) . ) False

确实有效。我已经尝试了 foldr 函数((.) (||)) 的咖喱/非咖喱的几种不同组合,但我似乎无法完全匹配类型。

我想了一会儿,如果我有办法“咖喱”foldr 本身的类型签名,所以它变成了

foldr` :: Foldable t => ((a,b) -> b) -> b -> t a -> b
foldr` f acc l = foldr (curry f) acc l

可能有一条前进的道路,但我没有成功。似乎试图告诉 foldr “我传递给您的函数在准备好之前需要更多参数”时会出现挂断。

有没有办法让这个特定的策略奏效,还是我在某处遇到了基本的限制?

问题 4

似乎这两个问题都可以归结为

没有办法表明在“应用了它的所有参数”之前不应将函数传递给它的调用者,

请注意,在 Haskell 和 lambda 演算中,所有函数都接受一个参数。在某些情况下,柯里化/去柯里化似乎可以缓解这种情况,但在其他情况下则不然。特别是,第一个策略之所以奏效,是因为它

而第二个策略试图将部分应用的高阶函数传递给一个期望高阶函数(尽管顺序和类型不同)的函数,因此无法区分功能是否“准备就绪”。

这种直觉是否正确?是否有任何工具(无论是概念上的还是 Haskell 本身的)可以用来帮助我(快速)了解我何时会遇到这种斗争?如果我的第二个策略确实不会成功,那么我就没有在策略中看到一种模式,这些模式给我一些先验迹象表明我正在尝试做的事情是不可能的。


长版

关于我自己的一些背景:我对函数式编程很陌生,尤其是 Haskell。我在数学方面有不错的背景,在 python 方面有一些命令式/面向对象的编程经验,以及少量的一般计算机科学知识。我正在学习一些类别理论,并且对基础知识感到相当满意。

我正在研究“从第一原则开始的 Haskell 编程”。到目前为止(我所拥有的版本中的第 10 章),它已经涵盖了 lambda 演算、类型、类型类、语法、递归、列表方法和现在折叠的基础知识。我对这些理论和实践(通过练习)都感到相当满意。

在折叠一章中,有一组练习可以通过折叠重写标准函数,并在可能的情况下以无点的方式进行。这本书展示了许多中间版本,这些版本应该有助于使用折叠收敛到最终的无点版本。给出的例子是“and”函数

myAnd :: [Bool] -> Bool

-- VERSION 1: 
-- direct recursion,not using (&&)
myAnd [] = True
myAnd (x:xs) =
  if x == False
  then False
  else myAnd xs

-- VERSION 2
-- direct recursion,using (&&)
myAnd [] = True
myAnd (x:xs) = x && myAnd xs

-- VERSION 3:
-- fold,not point-free
-- in the folding function
myAnd = foldr
        (\a b ->
          if a == False
          then False
          else b) True

-- VERSION 4
-- fold,both myAnd and the folding
-- function are point-free Now
myAnd = foldr (&&) True

我遇到的问题是对“any”函数做同样的事情。我已经下载了前两个版本:

-- VERSION 1:
-- direct recursion
myAny :: (a -> Bool) -> [a] -> Bool
myAny f [] = False
myAny f (x:xs) =
  if f x == True
  then True
  else myAny f xs

-- Version 2:
-- direct recursion with (||)
myAny f [] = False
myAny f (x:xs) = f x || myAny f xs

但我坚持使用无点版本。我看到了至少两种策略。

首先是在 fold 函数中应用“myAny”的 a -> Bool 函数我有三个工作版本,每个版本都更接近于免费:

myAny1 :: (a -> Bool) -> [a] -> Bool

-- All of these appear to work
myAny1 f l = foldr (\x y -> f x || y) False l
myAny1 f = foldr (\x y -> f x || y) False
myAny1 f = foldr ((||) . f) False
myAny1 = \f -> foldr ((||) . f) False

但我正在努力从上一个版本的 lambda 中消除 f 参数。

由于柯里化等原因,我还没有说服自己这是否可能。我的直觉告诉我它不是;我不能简单地将部分应用的函数(例如 g = (.) (||))传递给 foldr,因为参数不会从 g 中“脱落”。即,

(foldr ((.) (||)) False) f l

并不是f应该首先应用于“部分组合”作为((.) (||) f);上面的表达式实际上甚至在语法上都无效,因为 foldr 已经部分应用于两个参数并且只需要一个列表。因此,我认为这个策略需要一个 lambda 术语来“传入参数”并定义折叠函数,我没有看到避免这种情况的方法

在对类型系统和柯里化/反柯里化进行试验后,我有一种预感,即 foldr 的类型签名本身是柯里化的。即,它是

foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b 

而不是

foldr` :: Foldable t => ((a,b) -> b) -> b -> t a -> b
foldr` f acc l = foldr (curry f) acc l

但我相当肯定,即使使用 foldr' 而不是 foldr 仍然阻止我成功地使这一点免费 - 我仍然无法让 foldr “知道”我传递给它的函数只是“部分应用”,我觉得再多的柯里化或取消柯里化都无法纠正这一点。


我的第二个策略是将函数 f 映射到列表上,然后应用 or,或者本着练习的精神,通过折叠重新实现 or

我的第一次尝试是:

-- First map `f` over `l`,then apply the fold
-- This works; makes sense so far
myAny2 f l = foldr (||) False $ map f l

我的第二次尝试是:

-- Try to eliminate the argument
-- Does not work; makes sense why
myAny2 f = foldr (||) False $ map f

不起作用,因为 map f 被当作 foldr 的最后一个参数。但是 map f 不是一个列表:它是一个函数,它接受一个列表并返回一个相同类型的列表。解决方法是将 $ 更改为 .

-- Works:
myAny2 f = foldr (||) False . map f

现在,我希望能够去掉上面定义两边的 f,如

myAny2 = foldr (||) False . map

基本原理是 map 消耗我们的两个参数(f 和列表);不传入它们意味着不评估函数组合运算符 .,因为函数应用程序的优先级高于组合。因此,当将两个参数传递给上述版本的 myAny2 时,它们首先应用于 map,进行评估,然后送入 foldr

但是这种理解是错误的,因为GHC抛出了如下错误

Ch10ex.hs:28:10: error:
    • Couldn't match type ‘Bool’ with ‘[a] -> Bool’
      Expected type: (a -> Bool) -> [a] -> Bool
        Actual type: (a -> Bool) -> Bool
    • Possible cause: ‘(.)’ is applied to too many arguments
      In the expression: foldr (||) False . map
      In an equation for ‘myAny2’: myAny2 = foldr (||) False . map
    • Relevant bindings include
        myAny2 :: (a -> Bool) -> [a] -> Bool (bound at Ch10ex.hs:28:1)
   |
28 | myAny2 = foldr (||) False . map 
   |          ^^^^^^^^^^^^^^^^^^^^^^

Ch10ex.hs:28:29: error:
    • Couldn't match type ‘[Bool]’ with ‘Bool’
      Expected type: (a -> Bool) -> [a] -> Bool
        Actual type: (a -> Bool) -> [a] -> [Bool]
    • In the second argument of ‘(.)’,namely ‘map’
      In the expression: foldr (||) False . map
      In an equation for ‘myAny2’: myAny2 = foldr (||) False . map
   |
28 | myAny2 = foldr (||) False . map 
   |                             ^^^

看看类型和脱糖,我想我明白为什么这不起作用。我们有:

map :: (a -> b) -> [a] -> [b]
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
foldr (||) False :: Foldable t => t Bool -> Bool
(.) :: (b -> c) -> (a -> b) -> a -> c

所以上面实际上变成了

(.) (folder (||) False) map

这意味着,在类型签名 (.) :: (b-> c) -> (a -> b) -> a -> c 中,我们必须有 b :: t Boolc :: Boola ::……好吧,a 没有不行。它是咖喱,所以它不适合签名。我认为这就是出现第一个错误的原因; “地图”上的柯里化意味着只应用一个参数,然后它尝试用部分应用的折叠函数组合函数,显然不能。我认为第二个错误来自尝试将这种类型检查传播到 map 函数;类型检查器意识到如果 map 具有类型签名 map :: (a -> Bool) -> [a] -> Bool,那么一切都可以工作(也许?)。

尽管如此,如果我们uncurry map,那么我们可以自己进行表达式类型检查:

foldr (||) False . uncurry map :: (a -> Bool,[a]) -> Bool

但这并不是 myAny 所期望的类型签名。所以我们再次咖喱,一切正常:

-- Final product,working:
myAny2 = curry $ foldr (||) False . uncurry map

一切顺利。

这种咖喱/非咖喱模式有名字吗?在实际层面上,似乎“uncurry”完成的是将“2-参数”函数 map 转换为一个以单个 2-tuple 作为参数的函数(以便它可以与一个需要“单个参数”的函数),然后重新柯里化以使给定的类型签名适合。

解决方法

您的第一个示例不起作用的原因是它最终试图将 map f 作为参数传递给 foldr

myAny = foldr (||) False . map
=
myAny f = (foldr (||) False . map) f
=
myAny f = foldr (||) False (map f)

您需要另一个级别的组合来“映射”额外的参数。我更喜欢用 fmap 写这个来唤起那个助记符,但它等价于 (.)。 (这是使用 instance Functor ((->) x)。)

myAny = fmap (foldr (||) False) . map
=
myAny f = (fmap (foldr (||) False) . map) f
=
myAny f = fmap (foldr (||) False) (map f)
=
myAny f = foldr (||) False . map f
=
myAny f x = (foldr (||) False . map f) x
=
myAny f x = foldr (||) False (map f x)

curry/uncurry 模式并没有我真正知道的名字,但总体策略与使用 Arrow 进行编程的原则有关。在这种情况下,不是使用 fmap 来“浸入”一个参数,而是使用 uncurry 将两个参数组合为一个,这样您就可以使用 (.) 创建一个线性管道,然后 curry 再次取消分组:

                                  map  ::  (a -> b) ->    [a]  -> [b]
                          uncurry map  :: ( a -> b,[a]) -> [b]
       foldr (||) False                ::                         [Bool] -> Bool
       foldr (||) False . uncurry map  :: ( a -> Bool,[a])           -> Bool
curry (foldr (||) False . uncurry map) ::  (a -> Bool) -> [a]            -> Bool

您可以消除此代码中的 f

myAny f = foldr ((||) . f) False

通过重写将 f 放在可以 eta-reduce 的位置:

myAny f = foldr ((||) . f) False
=
myAny f = flip foldr False ((||) . f)
=
myAny f = flip foldr False ((.) (||) f)
=
myAny f = flip foldr False (fmap (||) f)
=
myAny f = (flip foldr False . fmap (||)) f
=
myAny = flip foldr False . fmap (||)

我没有在策略中看到一种模式,让我先验地表明我正在尝试做的事情是不可能的。

总是可以以无点形式编写表达式。 pointfree.io 会给你任何东西的糟糕 pointfree 版本。

如果你想要一些在公共场合不会尴尬的东西,那么你必须考虑数据流结构,使用像 Control.Arrow 中的那些更高级别的组合器,理想情况下使用大量的中间函数有明确的名字。口头禅是“名称代码,而不是数据”。 pointfree 代码 (ha) 的重点是让代码更容易理解,通过强制您修复变量允许您编写的“意大利面条式数据流”(通过让您将任何东西连接到其他任何东西) .如果直接扔掉变量名而不改进结构,结果会更难阅读。