问题描述
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
。
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 Bool
、c :: Bool
和 a ::
……好吧,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) 的重点是让代码更容易理解,通过强制您修复变量允许您编写的“意大利面条式数据流”(通过让您将任何东西连接到其他任何东西) .如果直接扔掉变量名而不改进结构,结果会更难阅读。