【问题标题】:Understanding `ap` in a point-free function in Haskell在 Haskell 的无点函数中理解 `ap`
【发布时间】:2016-01-31 23:32:03
【问题描述】:

我能够理解 Haskell 中无点函数的基础知识:

addOne x = 1 + x

当我们在等式两边看到 x 时,我们将其简化:

addOne = (+ 1)

令人难以置信的是,在不同部分使用相同参数两次的函数可以无点编写!

让我以average 函数作为基本示例,写为:

average xs = realToFrac (sum xs) / genericLength xs

简化xs 似乎不可能,但http://pointfree.io/ 得出:

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

这行得通。

据我了解,这表明average 与在两个函数上调用ap 相同,(/) . realToFrac . sumgenericLength 的组合

不幸的是,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

但写作:

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

不起作用,(给出一个很长的类型错误消息,问我会包括它),所以我不明白文档想说什么。


表达式ap ((/) . realToFrac . sum) genericLength 是如何工作的?你能用比文档更简单的术语解释ap吗?

【问题讨论】:

  • let average = liftM2 ((/) . realToFrac) sum genericLength 工作。
  • @ØrjanJohansen 很有趣,你能在答案中解释为什么吗?
  • 查看函数的 Monad 实例的 ap 实现
  • 作为一个有趣的练习,ap 可以定义为(. ((. (return .)) . (>>=))) . (>>=)。 :-)

标签: haskell functional-programming pointfree


【解决方案1】:

当单子m(->) a 时,如您的情况,您可以定义ap 如下:

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

我们可以看到这在您的无点示例中确实“有效”。

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

我们也可以从一般规律推导出ap

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

也就是说,对do-notation 进行脱糖

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

如果我们替换 monad 方法的定义

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

我们得到了ap 的先前定义(对于我们特定的monad (-&gt;) a)。确实:

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)

【讨论】:

  • 所以定义ap' f g = \x -&gt; f x (g x) 会给它一个普通ap 拥有的权力的子集?
  • @Caridorc: 是的,正常的ap 适用于所有单子,您的ap' 仅适用于函数
  • ap' 是否类似于.. 由两个函数组成,两个函数都接受一个参数,ap' 组成两个函数,一个接受两个参数,一个接受一个参数。
  • @Caridorc 只是含糊其辞。它在 lambda 演算和组合逻辑中被称为“组合子 S”。en.wikipedia.org/wiki/Combinatory_logic#Examples_of_combinators
  • @chi 确实! Applicative 可以被认为是 SKI 演算的推广:(&lt;*&gt;) 是 S,pure 是 K,并且 I = S K K。
【解决方案2】:

任何 lambda 术语都可以重写为仅使用一组合适的 combinators 而没有 lambda 抽象的等效术语。此过程称为abstraciton elimination。在此过程中,您希望从内到外移除 lambda 抽象。因此,在一个步骤中,您拥有λx.M,其中M 已经没有lambda 抽象,并且您想摆脱x

  • 如果Mx,则将λx.x 替换为idid 在组合逻辑中通常用I 表示)。
  • 如果M 不包含x,则将该术语替换为const Mconst 在组合逻辑中通常用K 表示)。
  • 如果MPQ,即术语是λx.PQ,您想在函数应用程序的两个部分中“推送”x,以便您可以递归处理这两个部分。这是通过使用定义为λfgx.(fx)(gx)S 组合子来完成的,也就是说,它接受两个函数并将x 传递给它们,并将结果一起应用。您可以轻松验证λx.PQ 是否等同于S(λx.P)(λx.Q),我们可以递归处理这两个子项。

    如其他答案中所述,S 组合器在 Haskell 中可用作 ap(或 &lt;*&gt;),专门用于阅读器 monad。

reader monad 的出现并非偶然:在解决用等效函数替换λx.M 的任务时,基本上是将M :: a 提升到 reader monad r -&gt; a (实际上 reader Applicative 部分就足够了),其中rx 的类型。如果我们修改上面的过程:

  • 真正与阅读器monad相关的唯一情况是Mx。然后我们将x“提升”到id,以摆脱变量。下面的其他情况只是将表达式提升到应用函子的机械应用:
  • 另一种情况λx.M,其中M 不包含x,它只是将M 提升到读者应用程序,即pure M。事实上,对于(-&gt;) rpure 等价于const
  • 在最后一种情况下,&lt;*&gt; :: f (a -&gt; b) -&gt; f a -&gt; f b 是提升到 monad/applicative 的函数应用程序。这正是我们所做的:我们将PQ 两个部分都提升到读者应用程序,然后使用&lt;*&gt; 将它们绑定在一起。

可以通过添加更多组合子来进一步改进该过程,从而使结果项更短。大多数情况下,combinators B and C are used,在 Haskell 中对应于函数 (.)flip。同样,(.) 只是 fmap/&lt;$&gt; 供读者申请。 (我不知道有这样一个用于表达flip 的内置函数,但它会被视为f (a -&gt; b) -&gt; a -&gt; f b 的特化,供读者应用。)

前段时间我写了一篇关于这个的短文:The Monad Reader Issue 17, The Reader Monad and Abstraction Elimination。

【讨论】:

  • 它不是“内置”的,而是lensflip 泛化为(??)。我觉得Data.Functor有点坑。
【解决方案3】:

简单的一点:修复 liftM2

原始示例中的问题是apliftM 函数的工作方式略有不同。 ap 接受一个包裹在 monad 中的函数,并将其应用于包裹在 monad 中的参数。但是liftMn 函数采用“普通”函数( 包裹在 monad 中的函数)并将其应用于 包裹的参数单子。

我将在下面详细解释这意味着什么,但结果是,如果您想使用liftM2,那么您必须将(/) 拉出并在开始时将其作为单独的参数。 (所以在这种情况下,(/) 是“正常”函数。)

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

如原始问题中所述,调用liftM2 应该涉及三个agruments:liftM2 f x1 x2。这里f(/)x1(realToFrac . sum)x2genericLength

问题中发布的版本(不起作用的版本)试图仅使用两个参数调用 liftM2

解释

我将分几个阶段进行构建。我将从一些特定的值开始,并构建一个可以采用任何值集的函数。跳转到 TL:DR 的最后一部分

在此示例中,假设数字列表为 [1,2,3,4]。这些数字之和为 10,列表长度为 4。平均值为 10/42.5

为了把它变成ap 的正确形式,我们将把它分解成一个函数、一个输入和一个结果。

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

三种功能应用

aplistM 都涉及单子。在解释的这一点上,您可以将 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)

(注意这是伪代码。monad(4) 不是实际上是有效的 Haskell)。

(请注意,liftM 与之前使用的 liftM2 是不同的函数。liftM 接受一个函数并且只有一个参数,这更适合我所描述的模式。)

在上面定义的average 函数中,monad 是函数,但是“functions-as-monads”可能很难说,所以我将从更简单的例子开始。

那么什么是单子?

对 monad 更好的描述是“包含一个值,或者产生一个值,或者你可以以某种方式从中提取一个值的东西,但它也有更复杂的事情发生。”

这是一个真的模糊的描述,但它必须是,因为“更复杂的东西”可以是很多不同的东西。

Monad 可能会令人困惑,但它们的意义在于,当您使用 monad 操作(如 apliftM)时,它们会为您处理“更复杂的事情”,因此您可以专注于值。

这可能还不是很清楚,所以让我们做一些例子:

Maybe 单子

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

最简单的单子之一是'Maybe'。该值是 Just 中包含的任何值。因此,如果我们调用ap 并给它(Just ourFunction)(Just ourInput),那么我们会返回(Just ourResult)

“更复杂的东西”是指那里可能根本没有值,您必须考虑到 Nothing 的情况。

如前所述,使用像ap 这样的函数的意义在于它为我们处理了这些额外的复杂问题。对于 Maybe monad,ap 通过返回 Nothing 来处理这个问题,如果 Maybe 函数或 Maybe 输入是 Nothing

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

列表单子

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

对于列表 Monad,值是列表中的任何内容。所以ap [ourfunction] [ourInput] 返回[ourResult]

“更复杂的东西”是列表中可能有不止一件东西(或者完全是一件东西,或者什么都没有)。

对于列表,这意味着ap 采用零个或多个函数的列表以及零个或多个输入的列表。它通过返回一个包含零个或多个结果的列表来处理这个问题:每个可能的函数和输入组合都有一个结果。

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

作为 Monad 的函数

genericLength 这样的函数被认为是 Monad,因为它有一个值(函数的输出),并且它有一个“更复杂的东西”(事实上你必须提供一个输入才能获得值) .

这有点令人困惑,因为我们要处理多个函数、多个输入和多个结果。这一切都很好定义,很难描述,所以我们必须小心我们的术语。

让我们从列表[1,2,3,4] 开始,并将其称为我们的“原始输入”。这就是我们试图找到平均值的列表。它是原始 average 函数中的 xs 参数。

如果我们将原始输入 ([1,2,3,4]) 提供给 genericLength,那么我们得到的值为“4”。

我们的另一个函数是((/) . realToFrac . sum)。它获取我们的列表 [1,2,3,4] 并找到总和 (10),将其转换为小数值,然后将其作为第一个参数提供给 (/)。结果是等待另一个参数的不完整除法函数。即它将[1,2,3,4] 作为输入,并产生(10/) 作为其输出。

这一切都符合ap 为函数定义的方式。对于函数,ap 需要做两件事。第一个是读取原始输入并生成新函数的函数。第二个是读取原始输入并产生新输入的函数。最终结果是一个函数,它接受原始输入,并返回与将新函数应用于新输入时相同的结果。

您可能需要多读几遍才能理解它。或者,这里是伪代码:

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 "2.5"  )

如果您将其与上面更简单的示例进行比较,您会发现它仍然具有我们的函数(10/)、我们的输入4 和我们的结果2.5。他们每个人都再次被包裹在“更复杂的东西”中。在这种情况下,“更复杂的东西”是“function that takes [1,2,3,4] and returns...”。

当然,由于它们是函数,它们必须[1,2,3,4] 作为输入。如果他们采用不同的整数列表(例如[1,2,3,4,5]),那么我们将得到不同的结果(例如,新函数:(15/)、新输入 5 和新值 3)。

其他示例

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

这些也都可以使用liftM2 来定义。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-18
    • 2016-03-20
    • 2014-05-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多