简体   繁体   English

在 Haskell 的无点函数中理解 `ap`

[英]Understanding `ap` in a point-free function in Haskell

I am able to understand the basics of point-free functions in Haskell:我能够理解 Haskell 中无点函数的基础知识:

addOne x = 1 + x

As we see x on both sides of the equation, we simplify it:当我们在等式两边看到 x 时,我们将其简化:

addOne = (+ 1)

Incredibly it turns out that functions where the same argument is used twice in different parts can be written point-free!令人难以置信的是,在不同部分使用相同参数两次的函数可以写成无点函数!

Let me take as a basic example the average function written as:让我举一个基本的例子, average函数写成:

average xs = realToFrac (sum xs) / genericLength xs

It may seem impossible to simplify xs , but http://pointfree.io/ comes out with:简化xs似乎是不可能的,但是http://pointfree.io/出现了:

average = ap ((/) . realToFrac . sum) genericLength

That works.那个有效。

As far as I understand this states that average is the same as calling ap on two functions, the composition of (/) . realToFrac . sum据我了解,这表明average与在两个函数上调用ap相同,即(/) . realToFrac . sum (/) . realToFrac . sum (/) . realToFrac . sum and genericLength (/) . realToFrac . sumgenericLength

Unfortunately the ap function makes no sense whatsoever to me, the docs http://hackage.haskell.org/package/base-4.8.1.0/docs/Control-Monad.html#v:ap state:不幸的是, ap函数对我来说毫无意义,文档http://hackage.haskell.org/package/base-4.8.1.0/docs/Control-Monad.html#v:ap状态:

ap :: Monad m => m (a -> b) -> m a -> m b

In many situations, the liftM operations can be replaced by uses of ap,
which promotes function application.

      return f `ap` x1 `ap` ... `ap` xn

is equivalent to

      liftMn f x1 x2 ... xn

But writing:但是写:

let average = liftM2 ((/) . realToFrac . sum) genericLength

does not work, (gives a very long type error message, ask and I'll include it), so I do not understand what the docs are trying to say.不工作,(给出一个很长的错误信息,问我会包括它),所以我不明白文档想说什么。


How does the expression ap ((/) . realToFrac . sum) genericLength work?表达式ap ((/) . realToFrac . sum) genericLength工作的? Could you explain ap in simpler terms than the docs?ap比文档更简单的术语来解释ap吗?

Any lambda term can be rewritten to an equivalent term that uses just a set of suitable combinators and no lambda abstractions. 任何lambda术语都可以重写为仅使用一组合适的组合子而不使用lambda抽象的等效术语。 This process is called abstraciton elimination . 此过程称为abstraciton消除 During the process you want to remove lambda abstractions from inside out. 在此过程中,您希望从内到外删除lambda抽象。 So at one step you have λx.M where M is already free of lambda abstractions, and you want to get rid of x . 所以在一步你有λx.M ,其中M已经没有lambda抽象,你想摆脱x

  • If M is x , you replace λx.x with id ( id is usually denoted by I in combinatory logic). 如果Mx ,则用id替换λx.xid通常用组合逻辑中的I表示)。
  • If M doesn't contain x , you replace the term with const M ( const is usually denoted by K in combinatory logic). 如果M不包含x ,则用const M替换该术语( const通常用组合逻辑中的K表示)。
  • If M is PQ , that is the term is λx.PQ , you want to "push" x inside both parts of the function application so that you can recursively process both parts. 如果MPQ ,那就是术语是λx.PQ ,你想在函数应用程序的两个部分内“推” x ,这样你就可以递归地处理这两个部分。 This is accomplished by using the S combinator defined as λfgx.(fx)(gx) , that is, it takes two functions and passes x to both of them, and applies the results together. 这是通过使用定义为λfgx.(fx)(gx)S组合子来实现的,也就是说,它采用两个函数并将x传递给它们两者,并将结果一起应用。 You can easily verify that that λx.PQ is equivalent to S(λx.P)(λx.Q) , and we can recursively process both subterms. 您可以轻松验证该λx.PQ等效于S(λx.P)(λx.Q) ,并且我们可以递归地处理这两个子项。

    As described in the other answers, the S combinator is available in Haskell as ap (or <*> ) specialized to the reader monad. 如其他答案中所述, S组合器在Haskell中可用作专用于读取器monad的ap (或<*> )。

The appearance of the reader monad isn't accidental: When solving the task of replacing λx.M with an equivalent function is basically lifting M :: a to the reader monad r -> a (actually the reader Applicative part is enough), where r is the type of x . 读者monad的外观并非偶然:当用等效函数解决替换λx.M的任务时,基本上将M :: a提升到读者monad r -> a (实际上读者应用部分就足够了),其中rx的类型。 If we revise the process above: 如果我们修改上述流程:

  • The only case that is actually connected with the reader monad is when M is x . 实际上与读取器monad连接的唯一情况是当Mx Then we "lift" x to id , to get rid of the variable. 然后我们将x “提升”为id ,以摆脱变量。 The other cases below are just mechanical applications of lifting an expression to an applicative functor: 下面的其他案例只是将表达式提升到应用程序仿函数的机械应用程序:
  • The other case λx.M where M doesn't contain x , it's just lifting M to the reader applicative, which is pure M . 另一种情况是λx.M ,其中M不包含x ,它只是将M提升到读者应用程序,这是pure M Indeed, for (->) r , pure is equivalent to const . 实际上,对于(->) rpure等价于const
  • In the last case, <*> :: f (a -> b) -> fa -> fb is function application lifted to a monad/applicative. 在最后一种情况下, <*> :: f (a -> b) -> fa -> fb是将函数应用程序解除为monad / applicative。 And this is exactly what we do: We lift both parts P and Q to the reader applicative and then use <*> to bind them together. 这正是我们所做的:我们将PQ部分提升到阅读器应用程序,然后使用<*>将它们绑定在一起。

The process can be further improved by adding more combinators, which allows the resulting term to be shorter. 通过添加更多的组合器可以进一步改善该过程,这使得所得的术语更短。 Most often, combinators B and C are used , which in Haskell correspond to functions (.) and flip . 大多数情况下, 使用组合器BC ,它们在Haskell中对应于函数(.)flip And again, (.) is just fmap / <$> for the reader applicative. 同样, (.)只是fmap / <$>的读者应用程序。 (I'm not aware of such a built-in function for expressing flip , but it'd be viewed as a specialization of f (a -> b) -> a -> fb for the reader applicative.) (我不知道这种用于表达flip的内置函数,但它被视为f (a -> b) -> a -> fb的专门化,适用于读者。)

Some time ago I wrote a short article about this: The Monad Reader Issue 17 , The Reader Monad and Abstraction Elimination. 前段时间我写了一篇关于此的短篇文章: Monad读者第17期 ,读者Monad和抽象消除。

When the monad m is (->) a , as in your case, you can define ap as follows: 当monad m(->) a ,就像你的情况一样,你可以定义ap如下:

ap f g = \x -> f x (g x)

We can see that this indeed "works" in your pointfree example. 我们可以看到这确实在您的无点示例中“有效”。

average = ap ((/) . realToFrac . sum) genericLength
average = \x -> ((/) . realToFrac . sum) x (genericLength x)
average = \x -> (/) (realToFrac (sum x)) (genericLength x)
average = \x -> realToFrac (sum x) / genericLength x

We can also derive ap from the general law 我们也可以从一般法中得出ap

ap f g = do ff <- f ; gg <- g ; return (ff gg)

that is, desugaring the do -notation 也就是说,脱糖的do -notation

ap f g = f >>= \ff -> g >>= \gg -> return (ff gg)

If we substitute the definitions of the monad methods 如果我们替换monad方法的定义

m >>= f = \x -> f (m x) x
return x = \_ -> x

we get the previous definition of ap back (for our specific monad (->) a ). 我们得到了ap back的先前定义(对于我们特定的monad (->) a )。 Indeed: 确实:

app f g 
= f >>= \ff -> g >>= \gg -> return (ff gg)
= f >>= \ff -> g >>= \gg -> \_ -> ff gg
= f >>= \ff -> g >>= \gg _ -> ff gg
= f >>= \ff -> \x -> (\gg _ -> ff gg) (g x) x
= f >>= \ff -> \x -> (\_ -> ff (g x)) x
= f >>= \ff -> \x -> ff (g x)
= f >>= \ff x -> ff (g x)
= \y -> (\ff x -> ff (g x)) (f y) y
= \y -> (\x -> f y (g x)) y
= \y -> f y (g y)

The Simple Bit: fixing liftM2简单位:修复liftM2

The problem in the original example is that ap works a bit differently from the liftM functions.原始示例中的问题是ap工作方式与liftM函数略有不同。 ap takes a function wrapped up in a monad, and applies it to an argument wrapped up in a monad. ap接受一个包裹在 monad 中的函数,并将其应用于包裹在 monad 中的参数。 But the liftMn functions take a "normal" function (one which is not wrapped up in a monad) and apply it to argument(s) that are wrapped up in monads.liftMn函数采用一个“正常”的功能(一个在单子包裹起来),它适用于单子包裹起来的参数(一个或多个)。

I'll explain more about what that means below, but the upshot is that if you want to use liftM2 , then you have to pull (/) out and make it a separate argument at the beginning.我将在下面详细解释这意味着什么,但结果是,如果您想使用liftM2 ,那么您必须在开始时将(/)拉出并使其成为一个单独的参数。 (So in this case (/) is the "normal" function.) (所以在这种情况下(/)是“正常”函数。)

let average = liftM2 ((/) . realToFrac . sum) genericLength -- does not work
let average = liftM2 (/)   (realToFrac . sum) genericLength -- works

As posted in the original question, calling liftM2 should involve three agruments: liftM2 f x1 x2 .正如原始问题中发布的那样,调用liftM2应该涉及三个参数: liftM2 f x1 x2 Here the f is (/) , x1 is (realToFrac . sum) and x2 is genericLength .这里f(/)x1(realToFrac . sum)并且x2genericLength

The version posted in the question (the one which doesn't work) was trying to call liftM2 with only two arguments.问题中发布的版本(不起作用的版本)试图仅用两个参数调用liftM2

The explanation说明

I'll build this up in a few stages.我将分几个阶段来构建它。 I'll start with some specific values, and build up to a function that can take any set of values.我将从一些特定的值开始,然后构建一个可以接受任何值集的函数。 Jump to the last section for the TL:DR跳转到 TL:DR 的最后一部分

In this example, lets assume the list of numbers is [1,2,3,4].在这个例子中,让我们假设数字列表是 [1,2,3,4]。 The sum of these numbers is 10, and the length of the list is 4. The average is 10/4 or 2.5 .这些数字的总和为 10,列表的长度为 4。平均值为10/42.5

To shoe-horn this into the right form for ap , we're going to break this into a function, an input, and a result.为了将其apap的正确形式,我们将把它分解为一个函数、一个输入和一个结果。

ourFunction =  (10/)    -- "divide 10 by"
ourInput    =    4
ourResult   =    2.5 

Three kinds of Function Application三种功能应用

ap and listM both involve monads. aplistM都涉及 monad。 At this point in the explanation, you can think of a monad as something that a value can be "wrapped up in".在解释的这一点上,您可以将 monad 视为可以“包装”一个值的东西。 I'll give a better definition below.下面我会给出一个更好的定义。

Normal function application applies a normal function to a normal input.普通函数应用程序将普通函数应用于普通输入。 liftM applies a normal function to an input wrapped in a monad, and ap applies a function wrapped in a monad to an input wrapped in a monad. liftM将普通函数应用于包裹在 monad 中的输入,而ap将包裹在 monad 中的函数应用于包裹在 monad 中的输入。

        (10/) 4          -- returns 2.5
liftM   (10/) monad(4)   -- returns monad(2.5)
ap monad(10/) monad(4)   -- returns monad(2.5)

(Note that this is pseudocode. monad(4) is not actually valid Haskell). (请注意,这是伪代码monad(4)实际上不是有效的 Haskell)。

(Note that liftM is a different function from liftM2 , which was used earlier. liftM takes a function and only one argument, which is a better fit for the pattern i'm describing.) (请注意, liftM是从一个不同的功能liftM2 ,这是以前使用。 liftM取一个函数和只有一个参数,其是用于我所描述的图案的更好的拟合。)

In the average function defined above, the monads were functions, but "functions-as-monads" can be hard to talk about, so I'll start with simpler examples.在上面定义的average函数中,monads 是函数,但是“functions-as-monads”可能很难谈论,所以我将从更简单的例子开始。

So what's a monad?那么什么是单子呢?

A better description of a monad is "something which contains a value, or produces a value, or which you can somehow extract a value from, but which also has something more complicated going on."对 monad 的一个更好的描述是“包含一个值或产生一个值的东西,或者你可以以某种方式从中提取一个值,但它也有一些更复杂的事情发生。”

That's a really vague description, but it kind of has to be, because the "something more complicated" can be a lot of different things.这是一个非常模糊的描述,但它必须如此,因为“更复杂的东西”可能有很多不同的东西。

Monads can be confusing, but the point of them is that when you use monad operations (like ap and liftM ) they will take care of the "something more complicated" for you, so you can just concentrate on the values. Monad 可能会令人困惑,但它们的重点是当您使用 monad 操作(如apliftM )时,它们会为您处理“更复杂的事情”,因此您可以专注于值。

That's probably still not very clear, so let's do some examples:这可能还不是很清楚,让我们举几个例子:

The Maybe monad也许单子

ap (Just (10/)) (Just 4)   -- result is (Just 2.5)

One of the simplest monads is 'Maybe'. 'Maybe' 是最简单的单子之一。 The value is whatever is contained inside a Just .该值是Just包含的任何内容。 So if we call ap and give it (Just ourFunction) and (Just ourInput) then we get back (Just ourResult) .所以如果我们调用ap并给它(Just ourFunction)(Just ourInput)然后我们返回(Just ourResult)

The "something more complicated" is the fact that there might not be a value there at all, and you have to allow for the Nothing case. “更复杂的事情”是这样一个事实,即那里可能根本没有值,并且您必须考虑Nothing情况。

As mentioned, the point of using a function like ap is that it takes care of these extra complications for us.如前所述,使用像ap这样的函数的重点是它为我们处理了这些额外的复杂问题。 With the Maybe monad, ap handles this by returning Nothing if either the Maybe-function or the Maybe-input were Nothing .使用Maybe monad,如果 Maybe-function 或 Maybe-input 是Nothingap通过返回Nothing处理这个问题。

ap (Just (10/)) Nothing    -- result is Nothing
ap Nothing (Just 4)        -- result is Nothing

The List Monad列表单子

ap [(10/)] [4]    -- result is [2.5]

With the list Monad, the value is whatever is inside the list.对于列表 Monad,值是列表中的任何内容。 So ap [ourfunction] [ourInput] returns [ourResult] .所以ap [ourfunction] [ourInput]返回[ourResult]

The "something more complicated" is that there may be more than one thing inside the list (or exactly one thing, or nothing at all). “更复杂的事情”是列表中可能有不止一件事(或者完全是一件事,或者根本没有)。

With lists, that means ap takes a list of zero or more functions, and a list of zero or more inputs.对于列表,这意味着ap接受零个或多个函数的列表,以及零个或多个输入的列表。 It handles that by returning a list of zero or more results: one result for every possible combination of function and input.它通过返回零个或多个结果的列表来处理这个问题:函数和输入的每个可能组合都有一个结果。

ap [(10/), (100/)] [5,4,2]  -- result is [2.0, 2.5, 5.0, 20.0, 25.0, 50.0]

Functions as Monads作为 Monad 的函数

A function like genericLength is considered a Monad because it has a value (the function's output), and it has a "something more complicated" (the fact that you have to supply an input before you can get the value).genericLength这样的genericLength被认为是 Monad,因为它有一个值(函数的输出),并且它有一个“更复杂的东西”(你必须在获得值之前提供输入的事实)。

This is where it gets a little confusing, because we're dealing with multiple functions, multiple inputs, and multiple results.这就是有点令人困惑的地方,因为我们正在处理多个函数、多个输入和多个结果。 It is all well defined, it's just hard to describe, so we have to be careful with our terminology.这一切都很好定义,只是很难描述,所以我们必须小心我们的术语。

Lets start with the list [1,2,3,4] , and call that our "original input".让我们从列表[1,2,3,4] ,并将其称为我们的“原始输入”。 That's the list we're trying to find the average of.这就是我们试图找到平均值的列表。 It's the xs argument in the original average function.这是原始average函数中的xs参数。

If we give our original input ( [1,2,3,4] ) to genericLength then we get a value of '4'.如果我们将原始输入 ( [1,2,3,4] ) 提供给genericLength则我们得到的值为 '4'。

Our other function is ((/) . realToFrac . sum) .我们的另一个函数是((/) . realToFrac . sum) It takes our list [1,2,3,4] and finds the sum ( 10 ), turns that into a fractional value, and then feeds it as the first argument to (/).它接受我们的列表[1,2,3,4]并找到总和 ( 10 ),将其转换为一个小数值,然后将其作为第一个参数提供给 (/)。 The result is an incomplete division function that is waiting for another argument.结果是等待另一个参数的不完整除法函数。 ie it takes [1,2,3,4] as an input, and produces (10/) as its output.即它以[1,2,3,4]作为输入,并产生(10/)作为其输出。

This all fits with the way ap is defined for functions.这一切都符合ap为函数定义的方式。 With functions, ap takes two things.对于函数, ap需要做两件事。 The first is a function that reads the original input and produces a new function.第一个是读取原始输入并生成新函数的函数。 The second is a function that reads the original input and produces a new input.第二个是读取原始输入并生成新输入的函数。 The final result is a function that takes the original input, and returns the same thing you would get if you applied the new function to the new input.最终结果是一个函数,它接受原始输入,并返回与将新函数应用于新输入时得到的相同的结果。

You might have to read that a few times to make sense of it.你可能需要多读几遍才能理解它。 Alternatively, here it is in pseudocode:或者,这里是伪代码:

average = 
  ap
    (functionThatTakes [1,2,3,4] and returns "(10/)" )
    (functionThatTakes [1,2,3,4] and returns "  4  " )

-- which means:

average = 
    (functionThatTakes [1,2,3,4] and returns "(10/) 4"  )

Other examples其他例子

minPlusMax = ap ((+) . minimum) maximum
-- a function that adds the minimum element of a list, to the maximum element


upperAndLower = ap ((,) . toUpper) toLower
-- a function that takes a Char and returns a tuple, with the upper case and lower case versions of a character

These could all also be defined using liftM2 .这些也都可以使用liftM2来定义。

average       = liftM2 (/) sum genericLength
minPlusMax    = liftM2 (+) minimum maximum
upperAndLower = liftM2 (,) toUpper toLower

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

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM