简体   繁体   English

Haskell中无点函数的先决条件是什么?

[英]What are the prerequisites for a point-free function in Haskell

I always thought that the prerequisites for a pointfree function were to get the function arguments to the end of the definition. 我一直认为无点函数的先决条件是将函数参数放到定义的末尾。 Eg 例如

-- This can be made pointfree quite easily:    
let lengths x = map length x    
let lengths' = map length

-- However this cannot:
let lengthz x = length `map` x
-- let lengthz' = length `map` (parse error)

I originally came across this reading this question . 我最初看到这个问题 There we have this example: 我们有这个例子:

agreeLen :: (Eq a) => [a] -> [a] -> Int
agreeLen x y = length $ takeWhile id $ zipWith (==) x y
-- This may look like it can easily be made pointfree, however it cannot
-- agreeLen' :: (Eq a) => [a] -> [a] -> Int
-- agreeLen' = length $ takeWhile id $ zipWith (==) (zipWith is applied to too few arguments)

So why can my first example be made pointfree, but the other two cannot? 那么为什么我的第一个例子可以成为无点的,但另外两个不可以?

Your first one can be written in point-free style, but you have to adapt it to your use of map as an infix function. 您的第一个可以用无点样式编写,但您必须使其适应您使用map作为中缀函数。

let lengthz = (length `map`) -- write the partial application as a section

The problem with agreeLen is that zipWith is not a function of one argument, but of two. agreeLen的问题是zipWith不是一个参数的函数,而是两个参数的函数。 zipWith needs to be applied to both arguments before its result can be passed to takeWhile id . 在将结果传递给takeWhile id之前,需要将zipWith应用于两个参数。 The not-easy way to write it in point-free style is 以无点样式编写它的不容易的方法是

-- via http://pointfree.io
agreeLen = ((length . takeWhile id) .) . zipWith (==)

Put briefly, zipWith (==) is applied to the first argument x to agreeLen to produce a new function (one that takes a list and returns a zipped list). 简而言之, zipWith (==)应用于第一个参数xagreeLen以生成一个新函数(一个获取列表并返回压缩列表的函数)。 This new function is given as an argument to (length . takeWhile id) . 这个新函数作为参数给出(length . takeWhile id) . , which produces a new composed function that takes the second argument to agreeLen and produce the desired Int value. ,它产生一个新的组合函数,它将第二个参数agreeLen并产生所需的Int值。

(@duplode probably derives this more cleanly than what I was about to attempt.) (@duplode可能派生这超过了我正要尝试干净。)

A trick that quickly gets out of hand when the initial function takes more than 2 arguments is to explicitly uncurry it, do the composition, then re-curry the result. 当初始函数需要超过2个参数时,一个快速失控的技巧是明确地解决它,进行组合,然后重新调整结果。

agreeLen = curry $ length . takeWhile id . (uncurry $ zipWith (==))
 -- However this cannot: let lengthz x = length `map` x -- let lengthz' = length `map` (parse error) 

\\x -> length `map` x written point free is simply map length . \\x -> length `map` x自由写的点就是map length The infix backticks are just syntactical sugar. 中缀反引号只是语法糖。 (As chepner points out, if you really want it you can use a section, ie (length `map`) .) (正如chepner所指出的,如果你真的想要它,你可以使用一个部分,即(length `map`) 。)

 agreeLen :: (Eq a) => [a] -> [a] -> Int agreeLen xy = length $ takeWhile id $ zipWith (==) xy -- This may look like it can easily be made pointfree, however it cannot 

The key word here is "easily". 这里的关键词是“容易”。 Pretty much anything can be made point-free if you try hard enough. 如果你足够努力的话,几乎任何事情都可以毫无关键。 In this case, omitting the y parameter is easy if we write agreeLen in terms of (.) rather than ($) : 在这种情况下,如果我们用(.)而不是($)来表示agreeLen ,则省略y参数很容易:

agreeLen x y = (length . takeWhile id . zipWith (==) x) y
agreeLen x = length . takeWhile id . zipWith (==) x

As for x , we can handle it by treating the use of (.) to compose zipWith (==) x with the other functions as just another case of a value being modified with a function: 对于x ,我们可以通过处理(.)的使用来处理它,将zipWith (==) x与其他函数组合起来,只是用函数修改的值的另一种情况:

agreeLen x = (.) (length . takeWhile id) (zipWith (==) x)
agreeLen x = ((length . takeWhile id) .) (zipWith (==) x) -- cosmetical change
agreeLen x = (((length . takeWhile id) .) . zipWith (==)) x
agreeLen = ((length . takeWhile id) .) . zipWith (==)

It is not something you'd actually want to do in practice, but it is certainly possible. 这不是你在实践中真正想做的事情,但它肯定是可能的。

You can always convert a typed lambda expression into a typed combinator expression, using only the polymorphic S ( <*> ) and K ( const ) combinators. 您始终可以使用多态S( <*> )和K( const )组合器将类型化的lambda表达式转换为类型化的组合子表达式。 A Note on Typed Combinators and Typed Lambda Terms shows a proof, which is also an abstraction algorithm to convert lambda terms into (point-free) combinator terms: 关于Typed Combinators和Typed Lambda Terms的注释显示了一个证明,它也是一个将lambda项转换为(无点)组合子项的抽象算法

  1. |x α | | | x α = I α → α = Iα→α
  2. |x α | | | X α = K β → (α → β) X β if x α ∉ X β Xα= Kβ→(α→β)Xβ如果xα∉Xβ
  3. |x α | | | X α → β = X α → β if x α ∉ X α → β Xα→β= Xα→β如果xα∉Xα→β
  4. |x α | | | X β → γ Y β = S α → (β → γ) → α → β → (α → γ) (|x α | X β → γ )(|x α | Y β ) otherwise Xβ→γŶβ= Sα→(β→γ)→α→β→(α→γ)(| Xα| Xβ→γ)(| Xα|ÿβ)否则

This can be converted into Haskell notation easily: 这可以很容易地转换为Haskell表示法:

(\(x :: a) -> (x :: a)) = id

-- If X does not mention x
(\(x :: a) -> (X :: a)) = const X

-- If X does not mention x
(\(x :: a) -> (X :: a -> b)) = X

(\(x :: a) -> (X :: b -> c) (Y :: b))
  = (\(x :: a) -> X) <*> (\(x :: a) -> Y)

Therefore you can always write a point-free definition, but it might be much larger and uglier without higher-level combinators, and you may need to introduce newtype wrapping & unwrapping or RankNTypes to deal with impredicative polymorphism arising from passing polymorphic functions around. 因此,您总是可以编写一个无点定义,但是如果没有更高级别的组合器,它可能会更大,更丑陋,您可能需要引入newtype包装和展开或RankNTypes来处理因传递多态函数而产生的不可预测的多态性。

However, in real Haskell code you mainly want to use point-free style when you can “get the arguments to the end of the definition”, that is, when you can eta-reduce fx = gx into f = g . 但是,在真正的Haskell代码中,当你可以“将参数定义到定义的结尾”时,主要是想使用无点样式,也就是说,当你可以将fx = gx减少为f = g This works best if you have a simple “pipeline” of functions where you pass one intermediate value along without duplicating or dropping it. 如果你有一个简单的“管道”函数,你可以传递一个中间值而不复制或删除它,这种方法效果最好。 For example, here's a histogram function: 例如,这是一个直方图函数:

-- import Control.Arrow ((&&&))
(f &&& g) x = (f x, g x)

-- import Control.Category ((>>>))
(f >>> g) = g . f

histogram :: String -> [(Char, Int)]
histogram
  = sort                             -- [Char]
  >>> group                          -- [[Char]]
  >>> map (head &&& length)          -- [(Char, Int)]
  >>> sortBy (flip (comparing snd))

If we call this form of a function definition point-free 如果我们称这种形式的函数定义无

f = e

where f is just a function name and e is an expression, we can make at least those functions point-free that 其中f只是一个函数名, e是一个表达式,我们至少可以使那些函数无点

  1. have a right hand side that is just a function application. 有一个右侧只是一个功能应用程序。
  2. don't require pattern matching on their arguments and 不需要对其参数进行模式匹配
  3. the arguments are not mentioned in "complex" subexpressions, such as list comprehensions, case, let or lambda abstractions. 在“复杂”子表达式中没有提到参数,例如列表推导,case,let或lambda抽象。

That is, if the arguments are just simple names, we can make it point-free if the other two conditions are also met. 也就是说,如果参数只是简单的名称,如果还满足其他两个条件,我们可以使其无点。

For arguments that need some pattern matching, if we can eliminate the constructor with some function (like maybe , either , fst , snd ) we can often rewrite the function so that we get one that can be written point-free: 对于需要一些模式匹配,如果我们可以消除一些函数构造函数的参数(如maybeeitherfstsnd ),我们经常可以重写功能,使我们得到一个可以写入自由点:

 f (a,b) = (b,a)
 f' ab   = (snd ab, fst ab)
 f'' = (pure (,) <*> snd) <*> fst

Also, an if expressions on the right hand side can be rewritten as application of bool 此外,右侧的if表达式可以重写为bool应用

Here is an algorithm that eliminates some variable v from an expression e such that the resulting expression x doesn't contain v and, when applied to v is equivalent to e 这是一个算法,它从表达式e中消除一些变量v ,使得结果表达式x不包含v,并且当应用于v时等效于e

  • If e doesn't contain v , the result is pure e 如果e不包含v ,则结果为pure e
  • If e is v , the result is id 如果ev ,则结果为id
  • If e is some expression f applied to v and f does not mention v the result is f 如果e是某个表达式f应用于vf没有提到v结果是f
  • Otherwise, e must be an application gh where at least one of g and h mentions v . 否则, e必须是应用程序gh ,其中gh中至少有一个提到v The result would be g' <*> h' where g' is the expression that results when eliminating v from g and h' is the expression that results when eliminating v from h 结果将是g'* * h' ,其中g'是从g中消除v时产生的表达式, h'是从h中消除v时产生的表达式

And here is how to make a function definition point-free that fulfills the requirements outlined above 以下是如何使函数定义无点,以满足上述要求

 f v1 v2 v3 ... vn = e
  • let e' be the elimination of vn from e e'e中消除vn
  • Write down the new function 记下新功能

    f' v1 v2 v3 ... v(n-1) = e' f'v1 v2 v3 ... v(n-1)= e'

  • If no arguments are left, you're done 如果没有参数,你就完成了

  • Otherwise, make f' point-free 否则,使f'无

Note, there are different ways to eliminate a variable form an expression using functions like (.) flip etc. that often leads to shorter code, however, the above is the most basic method since it only needs the S, K and I combinators (which are written <*> pure and id in Haskell). 注意,有不同的方法使用像(.) flip等函数消除变量形式的表达式,这通常会导致更短的代码,但是,上面是最基本的方法,因为它只需要S,K和I组合器(在Haskell中编写了<*> pureid )。

I always thought that the prerequisites for a pointfree function were to get the function arguments to the end of the definition 我一直认为无点函数的先决条件是将函数参数放到定义的末尾

Well, as other answers have shown, this isn't a necessary condition; 好吧,正如其他答案所示,这不是一个必要条件; almost anything can be made point free. 几乎任何东西都可以免费提供。 So it's not a prerequisite as such, rather just one simple rule for rewriting a function definition. 因此,这不是一个先决条件,而只是重写函数定义的一个简单规则。 This particular rule even has a name: eta reduction (as in the Greek letter η, spelled out in English as "eta"). 这个特殊的规则甚至有一个名字: eta减少 (如希腊字母η,英文拼写为“eta”)。 But it's not the only such rule. 但这并不是唯一的规则。

The conditions for applying eta reduction are also not quite just "get the arguments to the end". 应用eta减少的条件也不仅仅是“让争论到底”。 That's a loose description of what it "looks like" when you can apply eta reduction, but the actual condition isn't one you can just apply blindly to the source code text without considering the structure of the expression. 当你可以应用eta缩减时,这是一个松散的描述“看起来像”,但实际情况不是你可以盲目地应用于源代码文本而不考虑表达式的结构。 1 1

The real rule is to look for something like: 真正的规则是寻找类似的东西:

func x = expr x func x = expr x

By that I do not mean "some text (that I'm calling expr ) followed by x ", but rather "some well-formed expression (that I'm calling expr ) applied to x ". 并不是说“一些文本(我正在调用expr )后跟x ”,而是“一些格式正确的表达式(我称之为expr )” 应用于 x “。 The distinction is that I'm talking about the structure of the expression, not the order of characters on the source code representation of that expression. 区别在于我在谈论表达式的结构,而不是该表达式的源代码表示上的字符顺序。 This is crucial to understanding the differences between the examples that are confusing you. 这对于理解令您困惑的示例之间的差异至关重要。

You always need the "top level expression" of the function body to be exactly something directly applied to the last argument of the function. 你总是需要的函数体的“顶级表达式”是准确的东西直接应用到该函数的最后一个参数。 You also need for that argument not to be referenced anywhere else, but it otherwise doesn't matter what the "something" is; 你还需要在其他地方不要引用那个参数,但是否则“什么”是什么并不重要; it could be a complicated expression with many sub parts. 它可能是一个复杂的表达,有许多子部分。

Now let's consider your examples in that light. 现在让我们从这个角度考虑你的例子。

-- This can be made pointfree quite easily:
let lengths x = map length x

Okay, x is textually at the end. 好的, x在文本结尾处。 But is it the argument of a top level application? 但它是顶级应用程序的论点吗? We've got three parts to this expression, not just two, so our rule doesn't necessarily apply immediately. 我们在这个表达式中有三个部分,而不仅仅是两个部分,因此我们的规则不一定立即适用。 But remembering that function application is left associative ( fxy means f is applied to x, and the result of that fx is applied to y ), this is map length applied to x . 但是记住函数应用程序是左关联的( fxy意味着f应用于x,并且该fx的结果应用于y ),这是应用于x map length So we can apply eta reduction to eliminate x , leaving just the expression that was applied to it: map length . 因此我们可以应用eta减少来消除x ,只留下应用于它的表达式: map length

-- However this cannot:
let lengthz x = length `map` x

Again x is textually last, but we clearly can't just cut it out textually, since that doesn't even result in well-formed code. 再次x在文本上是最后的,但我们显然不能只是在文本上删除它,因为这甚至不会导致格式良好的代码。 An infix operator needs a left and a right argument (a name written in backticks like `map` turns it into an infix operator). 中缀运算符需要左`map`和右`map` (用`map`作为反引号的名称将其转换为中缀运算符)。

But remember what infix operator syntax means . 但请记住中缀运算符语法的含义 length `map` x is map applied to length , and the result of that map length applied to x . length `map` x是应用于length map ,并且该map length的结果应用于x So eta reduction does apply, and we can eliminate x . 因此,eta减少确实适用,我们可以消除x But not just by blinding deleting the source code text "x". 但不仅仅是通过盲目删除源代码文本“x”。 What is left should be the expression that was applied to x , which is map length again, not the meaningless source code text length `map` . 剩下的应该是应用于x的表达式,它再次是map length ,而不是无意义的源代码文本length `map`

We do have another option with infix operators though. 我们确实为中缀运营商提供了另一种选择。 You can "leave out" an argument from an infix operator if you convert it to a section by wrapping the thing in parentheses. 可以从管道符“遗漏”的说法,如果你将其转换为一个部分由括号包裹的东西。 So we could write map applied to length and not apply the result to a second argument by writing (length `map`) , if you want to keep the result looking similar to what you started with. 因此,我们可以编写应用于length map ,而不是通过写(length `map`)将结果应用于第二个参数,如果你想保持结果看起来与你开始的相似。 This also gives another way of eliminating arguments to get to point free code: we can leave out the first argument of an infix operator just as easily as the second. 这也提供了另一种消除参数以获得自由代码的方法:我们可以省略中缀运算符的第一个参数,就像第二个一样容易。 For example if we had: 例如,如果我们有:

f x = x / 3

This is (/) applied to x , and then the result of that applied to 3 . 这是(/)应用于x ,然后应用于3的结果。 So the argument of the outermost application is 3 , not x , and we can't apply eta reduction. 因此,最外层应用程序的参数是3 ,而不是x ,我们不能应用eta减少。 But we can apply a very similar process because of Haskell's operator section syntax 2 , and eliminate x to leave (/ 3) . 但是我们可以应用一个非常相似的过程,因为Haskell的运算符部分语法2 ,并消除x离开(/ 3) Again, note that the important thing to consider is the structure of the expression x / 3 , not the order of the bits of source code text we use to write it down. 再次注意,要考虑的重要事项是表达式x / 3结构 ,而不是我们用来写下来的源代码文本的位的顺序。

Enough of that digression. 足够的题外话。 Your last example is a little more complicated: 你的最后一个例子有点复杂:

agreeLen :: (Eq a) => [a] -> [a] -> Int
agreeLen x y = length $ takeWhile id $ zipWith (==) x y

y (and then x if we could do this twice) is at the end of the source code text. y (如果我们可以执行两次,则为x )位于源代码文本的末尾。 But it is not the argument of the top-level function application in this expression. 但它不是此表达式中顶级函数应用程序的参数。

One quirk of Haskell's syntax is that if there are operators involved in an expression (unless they're inside a parenthesised sub-expression), one of them is always the top-level application. Haskell语法的一个怪癖是,如果表达式中涉及运算符(除非它们位于带括号的子表达式中),其中一个始终是顶级应用程序。 This is because normal function application is higher-precedence than any possible operator, and it's the lowest precedence thing that ends up being the top-level expression. 这是因为普通函数应用程序的优先级高于任何可能的运算符,并且它是最低优先级的东西,最终成为顶级表达式。 So just from the presence of the $ operators it's immediately obvious that one of them has to be the "top" function, and there's no way either of them could be applied to just y on its own. 因此,仅仅从$运算符的出现,很明显其中一个必须是“顶级”函数,并且它们中的任何一个都无法单独应用于y So eta reduction isn't going to apply immediately. 因此,eta减少不会立即适用。

In more detail, this is $ applied to length , and then to everything on the right. 更详细地说,这是$应用于length ,然后应用于右边的所有内容。 The top-level application is therefore ($) length being applied to takeWhile id $ zipWith (==) xy ; 因此,顶级应用程序是($) length应用于takeWhile id $ zipWith (==) xy ; y is deep inside the argument expression, we need the argument of the top-level application to just be simply y . y在参数表达式的深处,我们需要顶层应用程序的参数只是简单的y

But remember that f $ x is just another way of writing fx , usually used only so we don't have to put parentheses around f or x if those are complex expressions. 但请记住, f $ x只是编写fx另一种方式,通常只用于如果那些是复杂的表达式我们不必在fx周围加括号。 If we put the parentheses back we get closer to having y at the top level. 如果我们把括号放回去,我们就会接近顶级的y

agreeLen x y = length (takeWhile id (zipWith (==) x y))

We're still not there. 我们还没有。 But this is " length applied to ( takeWhile id applied to ( zipWith (==) x applied to y )). This pattern of "chaining" things applied to the result of other applications is exactly what function composition with the . operator is for. We can mentally reframe this as " length applied after takeWhile id applied after zipWith (==) x , and the whole of that chain applied to y ". Written in code as: 但这是“应用的lengthtakeWhile id应用于( zipWith (==) x应用于y ))。这种应用于其他应用程序结果的”链接“模式正是使用.运算符的函数组合 .我们可以在心理上将其重新定义为“在zipWith (==) x之后应用takeWhile id后应用的length ,并将整个链应用于y ”。在代码中写为:

agreeLen x y = (length . takeWhile id . zipWith (==) x) y

Now we've got <something> applied to y ! 现在我们已经将<something>应用于y So we can eta-reduce: 所以我们可以减少:

agreeLen x = length . takeWhile id . zipWith (==) x

Note that x , although now textually last, does not fit the same pattern as y did above. 请注意, x虽然现在是文本上最后的,但与上面的y 符合相同的模式。 We don't have parentheses around the whole "function-pipeline" before it's applied to x , rather x is part of the last stage in the pipeline. 在将它应用于x之前,我们在整个“函数管道”周围没有括号,而x是管道中最后一个阶段的一部分 We can shuffle things around to eventually get: 我们可以随意洗牌,最终得到:

agreeLen x = (((length . takeWhile id) .) . zipWith (==)) x

and then eta reduce to eliminate x , as shown in other answers. 然后eta减少以消除x ,如其他答案所示。 But the steps are more complicated; 但步骤更复杂; I don't even know what they are, I just copied that expression from @chepner's answer (who copied it from http://pointfree.io ). 我甚至不知道它们是什么,我只是从@chepner的答案中复制了那个表达式(谁从http://pointfree.io复制了它)。 This shows a key difference between the role of x and y in the original agreeLen , even though they both appeared at the end of source code in the right order. 这显示了xy在原始的agreeLen的作用之间的关键区别,即使它们都以正确的顺序出现在源代码的末尾。

Which just re-emphasises my point: to understand why you can sometimes eta reduce and sometimes not you need to not think of eta reduction as a simple textual rule; 这再次强调了我的观点:理解为什么你有时可以减少,有时候你不需要将eta减少视为一个简单的文本规则; you do actually have to read and understand the structure of the expressions you're trying to rewrite. 确实有阅读和理解你正在试图改写表达式的结构。


1 Actually in pure lambda calculus you can apply this almost as a blind textual rule; 1实际上,在纯粹的lambda演算中,你几乎可以将它作为盲文本规​​则来应用; Haskell adds more complicated syntax like infix operators with implicit parentheses via precedence/associative, constructs like case, if/then/else, etc. Haskell添加了更复杂的语法,如中缀运算符,通过优先级/关联隐式括号,类似case,if / then / else等构造。

2 Infix operator syntax doesn't exist in pure lambda calculus, let alone operator sections. 纯粹的lambda演算中不存在2个中缀运算符语法,更不用说运算符部分了。 So there's no fancy Greek-letter name for this one, but that's only because the theoreticians who named these rules originally weren't studying Haskell. 所以这个没有花哨的希腊字母名称,但这只是因为命名这些规则的理论家最初并没有研究Haskell。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 F#中具有对象/记录的无点样式 - Point-free style with objects/records in F# 为什么Haskell指向函数的自由版本导致模糊类型错误? - Why does Haskell point free version of function result in ambiguous type error? function 指针有什么意义? - What is the point of function pointers? 在Haskell中,(+)是一个函数,((+)2)是一个函数,((+)2 3)是5.那究竟是什么? - In Haskell, (+) is a function, ((+) 2) is a function, ((+) 2 3) is 5. What exactly is going on there? 此代码中预期的 Haskell 反向 function 是什么? - What is the Haskell reverse function expected in this code? 函数对象在什么时候具有属性? - At what point does a function object have properties? 在Swift中声明函数类型变量有什么意义? - What is the point in declaring a variable of function type in Swift? 从 function 返回 const 别名有什么意义 - What is the point of returning a const alias from a function 在 function 参数中声明变量有什么意义? - What is the point of having variable declarations in function parameters? 在此特定的javascript代码中返回函数的意义是什么? - What is the point of returning a function in this specific javascript code?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM