Haskell函数参数的顺序是否有意义?

时间:2015-07-31 00:54:44

标签: haskell

我一直在学习Haskell,我注意到许多内置函数接受的命令计数器中的参数直观地符合我的预期。例如:

replicate :: Int -> a -> [a]

如果我想复制7次,我会写replicate 2 7。但是当用英语大声朗读时,函数调用就像是在说“复制2,7次”。如果我自己编写函数,我会交换第一个和第二个参数,以便replicate 7 2读取“复制7次,2次”。

当我通过99 Haskell Problems时出现了一些其他例子。我不得不写一个函数:

dropEvery :: [a] -> Int -> [a]`

它的第一个参数是一个列表,第二个参数是Int。直观地说,我会将标题写为dropEvery :: Int -> [a] -> [a],以便dropEvery 3 [1..100]读作:“删除列表[1..100]中的每三个元素。但在问题的示例中,它看起来像: dropEvery [1..100] 3

我也看到过这个我现在找不到的其他功能。由于实际原因以这种方式编写函数是否常见,或者这一切都在我脑海中?

4 个答案:

答案 0 :(得分:14)

以这种方式编写函数的原因之一是因为他们的curried表单变得有用。

例如,考虑函数mapfilter

map :: (a -> b) -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]

如果我想将偶数保留在列表中然后除以2,我可以写:

myfunc :: [Int] -> [Int]
myfunc as = map (`div` 2) (filter even as)

也可以这样写:

myfunc = map (`div` 2) . filter even
         \___ 2 ____/    \___ 1 ___/

将此视为从右到左的管道:

  • 首先我们保持偶数(步骤1)
  • 然后我们将每个数字除以2(步骤2)

.运算符作为将管道段连接在一起的方式 - 非常类似于|运算符在Unix shell中的工作方式。

这是可能的,因为mapfilter的list参数是这些函数的最后一个参数。

如果您使用此签名撰写dropEvery

dropEvery :: Int -> [a] -> [a]

然后我们可以将它包含在其中一个管道中,例如:

myfunc2 = dropEvery 3 . map (`div` 2) . filter even

答案 1 :(得分:11)

在Haskell中通常的做法是对函数参数进行排序,以便首先“配置”一个操作的参数,以及“最后操作的主要事物”。这通常是来自其他语言的反直觉,因为它往往意味着您最终会首先传递“最不重要”的信息。来自OO的特别震撼,其中“main”参数通常是调用该方法的对象,在调用的早期发生它完全超出参数列表!

但是有一种方法让我们疯狂。我们这样做的原因是部分应用(通过currying)非常容易并且在Haskell中广泛使用。假设我有foo :: Some -> Config -> Parameters -> DataStrucutre -> DataStructurebar :: Differnt -> Config -> DataStructure -> DataStructure等功能。如果您不习惯高阶思维,那么您只需将这些视为转换数据结构的内容。但是你也可以 使用它们中的任何一个作为“DataStructure变换器”的工厂:DataStructure -> DataStructure类型的函数。

很可能其他操作由 配置<{1}}个函数;至少有DataStructure -> DataStructure用于将DataStructures的变换器转换为DataStructures(列表,Maybes,IO等)仿函数的变换器。

我们有时也会更进一步。再次考虑fmap。如果我希望foo :: Some -> Config -> Parameters -> DataStructure -> DataStructure的来电者经常使用相同的fooSome多次调用它,但会改变Config,那么甚至更多部分的应用程序也会变得有用。

当然,即使我的部分应用程序的参数处于“错误”的顺序,我仍然可以使用Parameters之类的组合器和/或创建包装函数/ lambdas来完成它。但这会在我的代码中产生很多“噪音”,这意味着读者必须能够弄清楚正在做什么的“重要”事情以及只是调整界面的内容。

因此,基本理论是函数编写者试图预测函数的使用模式,并按照从“最稳定”到“最不稳定”的顺序列出其参数。当然,这不是唯一的考虑因素,并且通常存在冲突模式并且没有明确的“最佳”订单。

但是“参数将在描述函数调用的英语句子中列出的顺序”不是我在设计函数时会给予很多重视(而不是在其他语言中)。 Haskell代码看起来不像英语(在大多数其他编程语言中也没有代码),并且在少数情况下试图使它更接近并没有真正帮助。

针对您的具体示例:

  1. 对于flip,在我看来replicate参数是“主”参数,所以我会把它放在最后,就像标准库那样。尽管如此,它并不是很多;似乎非常首先选择复制数量并且具有a函数比首先选择复制元素并具有a -> [a]更有用功能

  2. Int -> [a]确实似乎以一种不稳定的顺序接受它的论点,但不是因为我们用英语说“删除列表中的每个第N个元素”。采用数据结构并返回“相同结构的修改版本”的函数几乎总是将数据结构作为其最后一个参数,其中配置“修改”的参数首先出现。

答案 2 :(得分:2)

为了增加其他答案,通常也有动力使最后一个论点成为其结构可能最复杂和/或成为lambda抽象的论证。这样就可以写

f some little bits $
  big honking calculation
  over several lines

而不是用括号括起来的大计算和最后的一些小参数。

答案 3 :(得分:0)

如果您想翻转参数,只需使用Prelude

中的flip函数
replicate' = flip replicate

> :t replicate'
replicate' :: a -> Int -> [a]