我是Haskell的新手,正在做一些我发现的初学者练习。我有一个函数,它接受函数列表和值。我需要通过列表中的每个函数发送该值,从头开始,然后返回该值。我考虑了递归和折叠...但我认为某种递归方法将是要走的路。
我尝试了一种类似于下面的递归方法,但它得到了基本情况,返回,我无法正确组合
func xs y =
if length xs == 1 then
(head xs) y
else
(head xs) y : func (drop 1 xs) y
我无法弄明白! 任何帮助都会很棒,谢谢!
答案 0 :(得分:2)
所以,首先,我建议添加一个类型签名,这可以帮助您,因为编译器会为您提供更好的错误消息:
func :: [a -> b] -> a -> [b]
让我们编译它:
test.hs:4:9:
Couldn't match type ‘b’ with ‘[b]’
啊,所以在第4行(原来第3行,因为我添加了类型签名),我们有类型b
,但我们需要一个列表!有道理,不是吗?那条线应该是:
[(head xs) y] -- originally just `(head xs) y`
问题1已修复,让我们再次编译:
test.hs:6:9:
Couldn't match type ‘b’ with ‘a -> b’
‘b’ is a rigid type variable bound by
the type signature for func :: [a -> b] -> a -> [b] at test.hs:1:9
啊,所以我们希望有b
类型,但我们实际上找到了a -> b
这是一个函数。说得通!您忘记将该值实际应用于该函数。那条线应该是
(head xs) y : func (drop 1 xs) y -- instead of `(head xs) : func (drop 1 xs) y`
现在让我们再试一次:
Ok, modules loaded: Main.
固定!
现在,让我们考虑如何在Haskell中更加惯用地编写这个:
我想,一个选项是模式匹配,它们比使用不安全的head
函数更好:
func' :: [a -> b] -> a -> [b]
func' fs x =
case fs of
f:[] -> [f x]
f:fs' -> f x : func' fs' x
但实际上我们可能会意识到,我们只想在每个值上映射一些东西(在本例中是一个函数),所以这应该真的有效:
func' :: [a -> b] -> a -> [b]
func' xs y = map SOMETHING xs
并且SOMETHING
是给定函数应该将该函数应用于y
的东西。嗯,这很简单:\f -> f y
将给定一个函数f
,并将其应用于y
。所以
func' :: [a -> b] -> a -> [b]
func' xs y = map (\f -> f y) xs
这项工作做得很好。或点免费风格:
func' :: [a -> b] -> a -> [b]
func' xs y = map (flip ($) y) xs
($)
函数有签名(a -> b) -> a -> b
,我们需要这样,但前两个参数被翻转(所以a -> (a -> b) -> b
),这可以通过flip ($)
实现。
我希望这是有道理的,否则只需添加评论。
我遗憾地误解了这个问题,让我们再试一次:截至评论时,类型签名应为func :: [a -> a] -> a -> a
。让我们编译一下:
test2.hs:7:3:
Couldn't match expected type ‘a’ with actual type ‘[a]’
啊,公平地说,在(head xs) y : func (drop 1 xs) y
行中我们返回一个列表,但实际上我们只想要一个值。因此我们不希望使用原始y
调用第二个函数但使用first_function y
来解决这个问题。所以我们将其改为
func (drop 1 xs) ((head xs) y)
然后它已经有效了:)。
让我们尝试使其更加惯用:如果我们找到一个调用func [f1, f2, f3, f4] y
,我们实际想要执行的是(f4 (f3 (f2 (f1 y))))
。整个事情看起来确实像是一个折叠。它就是!
import Data.List (foldl')
func' :: [a -> a] -> a -> a
func' xs y = foldl' (\x f -> f x) y xs
或者再次
func' xs y = foldl' (flip ($)) y xs
如果您想知道我为何使用foldl'
而不是foldl
,请在此处阅读why foldl
is broken and shouldn't be used。
答案 1 :(得分:0)
如果我已正确理解,您希望将功能列表和点作为输入,并将所有这些功能应用于该点。例如,给出我们想要的三个函数的列表
func [f,g,h] x = f (g (h x))
上面的例子可以使用函数合成运算符重写为
func [f,g,h] x = (f . g . h) x
,由于eta-contraction,可以简化为
func [f,g,h] = f . g . h
它澄清了问题所在:给定一个函数列表,返回它们的组成。
现在,考虑一个不同的问题:如果我们想解决(概括)
anotherFunc [a,b,c] = a + b + c
我们会使用递归
anotherfunc [] = 0
anotherfunc (x:xs) = x + anotherfunc xs
或折叠
anotherFunc xs = foldr (+) 0 xs
请注意anotherFunc
利用二进制操作(+)
及其中性元素0
(即x + 0 = 0 + x = x
的元素)。
对于原始问题(组成函数列表),我们可以这样做:
func xs = foldr (.) id xs
确实,身份函数id
是构成的中性元素。
我们甚至可以将上述内容合同到
func = foldr (.) id
或者,如果愿意,可以使用显式递归
func [] y = y
func (x:xs) y = x (func xs y)
-- or equivalently (x . func xs) y
答案 2 :(得分:0)
另一种看待这种情况的方式:
在意识到您想要的类型应该是[a -> a] -> a -> a
之后。添加多余的括号以改变我们得到的重点[a -> a] -> (a -> a)
;另一种查看函数的方法是它需要一个函数列表并将它们“压缩”为单个函数。
听起来非常类似于功能组合,它是!你基本上指定了一个函数列表的组合,而不是组成其中两个函数的.
运算符;我们只想“继续编写所有功能,直到只留下一个”。现在听起来非常像折叠:
func' :: [a -> a] -> (a -> a)
func' = foldr (.) _
但我们填补空白的是什么? GHC实际上告诉我们(至少对于GHC> = 7.8)!
foo.hs:3:19:
Found hole ‘_’ with type: a -> a
Where: ‘a’ is a rigid type variable bound by
the type signature for func' :: [a -> a] -> a -> a at foo.hs:2:10
Relevant bindings include
func' :: [a -> a] -> a -> a (bound at foo.hs:3:1)
In the second argument of ‘foldr’, namely ‘_’
In the expression: foldr (.) _
In an equation for ‘func'’: func' = foldr (.) _
专注于Found hole ‘_’ with type: a -> a
位。
如果你已经这样做了一段时间,你就会知道a -> a
a
id
类型的唯一合理函数是id
,所以这似乎是一个很好的候选者。从您的规范中思考它也会导致id
;折叠功能组合的“起始值”必须是与所有其他组合的功能;我们不希望它做任何事情,所以func [] x
是有道理的。如果我们的函数列表为空(例如id
) - “不对函数应用函数”,它也是将应用于该值的函数听起来应该保持不变,所以再次{{1}符合条件。
所以 1 :
func' :: [a -> a] -> (a -> a)
func' = foldr (.) id
为什么要写func'
?因为这不是你的func
;您指定该值应首先通过列表中的第一个函数,然后是第二个函数,最后是最后一个函数。这意味着从f1 (f2 (f3 (f4 x)))
中获取func
效果的方法是写func [f4, f3, f2, f1] x
;当您写出完整的应用程序时,函数的从左到右顺序与列表中函数的从左到右顺序相反。 .
组成顺序相反,构建我们的列表也是通过使用.
func' [f1, f2, f3, f4] x = f1 (f2 (f3 (f4 x)))
进行折叠来构建的。
但这很容易解决;我们可以通过reverse
管理功能列表,然后再将其输入折叠!哪个给了我们:
func :: [a -> a] -> (a -> a)
func = foldr (.) id . reverse
示例:
λ func [("f1 . " ++), ("f2 . " ++), ("f3 . " ++)] "id $ x"
"f3 . f2 . f1 . id $ x"
1 “但是等等!”我听到你哭了,“作为身份元素,id
作为一个幺半群是不是形成一个幺半群?”。它在数学上是的,所以实际上这个函数“可以”只是:func' = mconcat
。但是这个Monoid
实例实际上并不存在于Haskell中,因为它需要扩展来编写,并且会与前奏中存在的另一个Monoid
实例重叠。在Data.Monoid
中有一个newtype包装器Endo a
(包装a -> a
),它有实例。
所以你可以写func' = appEndo . mconcat . map Endo
,但是在那一点上,通过利用幺半群结构我们比折叠更“简单”是值得商榷的。