【问题标题】:Haskell - List of functions that operate on single valueHaskell - 对单个值进行操作的函数列表
【发布时间】:2015-07-20 14:45:25
【问题描述】:

我是 Haskell 的新手,正在做一些我发现的初学者练习。我有一个函数,它接收函数列表和一个值。我需要通过列表中的每个函数发送该值,从头部开始,然后返回该值。我考虑过递归和折叠......但我认为某种递归方法将是要走的路。

我尝试了类似下面的递归方法,但它到达了基本情况,返回,我无法正确组合它

func xs y =
 if length xs == 1 then
  (head xs) y
 else
  (head xs) y : func (drop 1 xs) y

我就是想不通! 任何帮助都会很棒,谢谢!!!

【问题讨论】:

标签: list function haskell recursion


【解决方案1】:

被误解的版本

所以,首先,我建议添加一个类型签名,这会对您有所帮助,因为编译器会为您提供更好的错误消息:

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 ($) 实现。

我希望这是有道理的,否则只需添加评论。

希望版本正确

不幸的是,我误解了这个问题,所以让我们再试一次:对于 cmets,类型签名应该是 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

【讨论】:

  • 对不起,我一定是解释的有点糟糕。在所有函数都对其进行操作之后,我只想返回一个值......更像这样: func :: ([(a->a)]) -> a -> a 我不确定这种递归方法是否将能够做我想做的事
  • 嗯,这很有趣。谢谢你。但是再次,请参阅我上面的评论,哈哈,对不起
  • 所以参数有:函数列表->值->修改值
  • 有点像地图的反面...有意义吗?
  • 哇,说真的,非常感谢您的帮助。现在这是有道理的。你写了很多,解释了很多,我非常感谢:)这东西很有趣!
【解决方案2】:

如果我理解正确,您想将函数列表和一个点​​作为输入,并将所有这些函数应用于该点。例如,给定我们想要的三个函数的列表

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 是组合的中性元素。 我们甚至可以将上述内容进行 eta-contract

func = foldr (.) id

或者,如果愿意,使用显式递归

func []     y = y
func (x:xs) y = x (func xs y)
  -- or equivalently (x . func xs) y

【讨论】:

  • 这将为足够大的输入构建一个巨大的 thunk。例如,尝试func (replicate 100000000 $ (+) 3) 2 并观察程序消耗的内存。我提出了func' xs y = foldl' (flip ($)) y xs,它在恒定内存中运行,不会遇到这个问题。
  • 对不起,这取决于列表中函数的严格程度。如果函数是惰性的,您的实现会更好。对于func (repeat $ (++) [2]) [2],你的会不断产生价值,而我的永远不会产生任何东西。
  • @JohannesWeiß 是的。但是请注意,这取决于功能。考虑例如func (replicate 1000000 $ (:) 3) 2。这里右折叠在常量内存中运行,而左折叠则没有。
  • @JohannesWeiß 是的,完全正确。您的情况(严格功能)可能更常见。
  • 是的,并且存在关联性差异:func [(+) 2, (*) 5] 7 与您的实现将返回37(+) 2 ((*) 5 7))但是据我了解,它应该评估(*) 5 ((+) 2 7),即45
【解决方案3】:

另一种看法:

在意识到你想要的类型应该是[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

func' :: [a -> a] -> (a -> a)
func' = foldr (.) id

我为什么写func'?因为这不是你的func;您指定该值应首先通过列表中的第一个函数,然后是第二个,最后是最后一个。这意味着从func 中获得f1 (f2 (f3 (f4 x))) 效果的方法是写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 中有一个新类型包装器Endo a(包装a -> a),它确实有实例。

所以你可以写func' = appEndo . mconcat . map Endo,但在这一点上,我们是否通过利用幺半群结构比折叠“更简单”是有争议的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-03
    • 2016-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-21
    • 1970-01-01
    相关资源
    最近更新 更多