monad_超越Monad:应用界面

monad

I’m in love with functional concepts in programming. And with good cause — functional programming is the future of software development, not just in JS but in systems as well (case in point, Rust, which Microsoft has now adopted). I’ve already described Monad as an interface. In this article I’d like to explain the Applicative type in the same way. Like I’ve said in the Monad Interface, in JS there aren’t actually interfaces like we have in C-Family languages. And there’s no compile-time checking of your adherence to your conceptual interfaces. But it’s really convenient to think in interfaces anyway, because they abstract away our problems into a small concept we can easily reason about. In JS we need to take some extra steps as the developer to ensure that we have upheld an interface’s contract, but it is well worth it to do so.

我爱上了编程中的功能概念。 并且有充分的理由-函数式编程是软件开发的未来,不仅在JS中,而且在系统中(例如, Microsoft现在采用的 Rust就是一个例子)。 我已经将Monad描述为接口 。 在本文中,我想以相同的方式解释Applicative类型。 就像我在Monad接口中所说的那样,在JS中实际上并没有像C语言那样的接口。 而且没有编译时检查您对概念性接口的遵守情况。 但是无论如何,在界面中进行思考确实很方便,因为它们将我们的问题抽象为一个我们可以轻松推理的小概念。 在JS中,作为开发人员,我们需要采取一些额外的步骤,以确保我们维持了接口的合同,但是这样做是值得的。

The Applicative Interface is very simple, even simpler than the Monad Interface, but I think it’s even more powerful because it’s actually more functional:

应用接口非常简单,甚至比Monad接口更简单,但我认为它甚至更强大,因为它实际上更实用

Applicative<A, B> {
ap(ft: Functor<A>) ⟶ Functor<B>
static of(f: Function<A> ⟶ B) ⟶ Applicative<A, B>
}

static in this case is my way of denoting that of should be a member related to the type itself, not an instance method. And just for posterity, I’ll throw in the Functor Interface:

static在这种情况下,我表示此方式of应该是相关类型本身,而不是一个实例方法成员。 只是为了后代,我将介绍Functor界面:

Functor<A> {
map(f: Function<A> ⟶ B) Functor<B>

By the definition of Functor above, we can actually also come to understand that a Monad is also a Functor — one that can unwrap its contents! That unwrapping is what gives us chain. But I digress. I’ll focus on Applicative today.

通过上面对Functor的定义,我们实际上还可以了解Monad也是Functor —可以解开其内容的函子! 这种解包才是给我们chain 。 但是我离题了。 我今天将重点介绍Applicative。

As I’ve shown, there are two major parts to the Applicative. I’ll first talk about of. of is actually a tenet of the Pointed Interface — all that means is given a value, of will produce a wrapper for that value, be it a Monad, Applicative, or Functor. It’s just the way that we lift a value into our boxed context. Its not a smart constructor though — it will lift whatever you give it into the context even if it doesn’t necessarily make sense. In the case of the Applicative, we expect that the value lifted into the Applicative context is a function. It could be a function with multiple parameters, but for simplicity we’ll start with just a single parameter function (although, you’ll find the power lies in combining the Applicative with curried functions, more on that later…).

正如我所展示的,该应用程序有两个主要部分。 我先说说ofof实际上是Pointed Interface的宗旨-所有这意味着给定一个值, of将为该值生成包装器,无论是Monad,Applicative还是Functor。 这就是我们将价值带入框内的方式。 但是,它不是一个聪明的构造函数-即使您输入的内容不一定有意义,它也会将您提供的内容提升到上下文中。 对于Applicative,我们期望提升到Applicative上下文中的值是一个函数。 它可能是一个具有多个参数的函数,但为简单起见,我们将仅从一个参数函数开始(尽管您会发现,强大的功能在于将Applicative与咖喱函数相结合,稍后再说……)。

Let’s start by hand-rolling some types that adhere to the Applicative Interface. You’ll recognize them from the Monad Interface because they’re also Monads!

让我们开始手动滚动一些遵循应用接口的类型。 您会从Monad界面中识别它们,因为它们也是Monad!

//first we'll need some boilerplate symbols...const VALUE = Symbol.for('value')const TAG   = Symbol.for('tag')//and a block-scoped throw helperconst err = x ⟹ { throw new Error(e) }const Right = x ⟹ ({
[VALUE]: x,
[TAG]: 'Right',
map(f) { return Right(f(this[VALUE])) },
ap(ft) { return ft.map(this[VALUE]) },
unwrap() { return this[VALUE] },
chain(f) { return this.map(f).unwrap() }
})const Left = x ⟹ ({
[VALUE]: x,
[TAG]: 'Left',
map(f) { return this },
ap(ft) { return ft },
unwrap() { return this[VALUE] },
chain(f) { return this }
})const Either = {
of: x ⟹ Right(x),
left: x ⟹ Left(x),
isRight: x ⟹ x[TAG] && x[TAG] ≡ 'Right',
isLeft: x ⟹ x[TAG] && x[TAG] ≡ 'Left',
either: (lfn, rfn, x) ⟹
Either.isLeft(x) ? lfn(x[VALUE])
: Either.isRight(x) ? rfn(x[VALUE])
: /*else*/ err('instance was not of Type Either.')
}

You’ll probably recognize the Either Monad. Either is representative of the logical separation of two paths. You can have either a Left or a Right. Right represents the ‘good’ path — as long as you have a Right you can keep operating on it. Left, however, is the bad path. If you get a Left, it will always have its initial value, regardless of how you operate on it. Left is a lot like the Const Monad, but has a binary relationship with the operable Right (which is alot like Identity!) You’ll notice in FP, we tend toward a right bias — most operations save the data portion for the rightmost position in function calls, and right tends to be the ‘good’ path in branching operations.

您可能会认识“单子”。 任一个代表两条路径的逻辑分离。 你可以一个左或右。 权利代表着“好的”道路-只要您拥有权利,您就可以继续努力。 但是,左路是错误的道路。 如果您得到一个Left,则无论您对其进行何种操作,它都将始终具有其初始值。 Left非常类似于Const Monad,但是与可操作的Right(与Identity相似!)具有二进制关系。您会注意到,在FP中,我们倾向于向右偏斜-大多数操作会将数据部分保存在最右边的位置在函数调用中,正确通常是分支操作中的“好”路径。

Now let’s also review the Option Monad (aka Maybe):

现在,我们还回顾一下Option Monad(又名Maybe):

const Some = x ⟹ ({
[TAG]: 'Some',
[VALUE]: x,
map(f) { return Option.of(f(this[VALUE])) },
ap(ft) { return ft.map(this[VALUE]) },
chain(f) { return this.map(f).unwrap() },
unwrap() { return this[VALUE] }
})const None = () ⟹ ({
[TAG]: 'None',
[VALUE]: null,
map(_) { return this },
ap(ft) { return ft },
chain(_) { return this },
unwrap() { return this }
})const Option = {
of: x ⟹
Option.isNone(x) ? None()
: /*else*/ Some(x),
isNone: x ⟹
x ≡ null
|| x ≡ undefined
|| (x[TAG] && x[TAG] ≡ 'None'),
isSome: x ⟹ x[TAG] && x[TAG] ≡ 'Some',
expect: (e, x) ⟹
Option.isNone(x) ? err(e)
: Option.isSome(x) ? x.unwrap()
: /*else*/ err('x in expect was not Type Option.')
}

In my implementation, I namespace our related functions within the Either and Option objects. It allows us to destructure out what we need later, and gives of more meaning by allowing us to call it from the namespace — we can’t be having ambiguous ofs for every type that implements the Pointed interface.

在我的实现中,我在Either和Option对象中为我们相关的函数命名空间。 它让我们解构了什么,我们以后需要,并给出of 通过允许我们从命名空间中调用它更多的意义-我们不能有模棱两可of每一个类型实现了尖接口秒。

Additional to our typical Monadic implementation of Option and Either which include map, unwrap, and chain operators, you’ll notice the ap method. If the value residing in this box is a function and this box can ap, we can use it as an Applicative. Here’s a simple example:

除了Option和Either的典型Monadic实现(包括mapunwrapchain运算符)之外,您还会注意到ap方法。 如果此框中的值是一个函数并且此框可以ap ,则可以将其用作应用程序。 这是一个简单的例子:

const mul = x ⟹ y ⟹ x * yconst some3 = Some(3) const some4 = Some(4)//we've mapped a function that returns a function!const applicative = some3.map(mul) //⟹ Some (y ⟹ x * y)//now if we want to 'apply' the function, we must use the ap method:applicative.ap(some4) //⟹ Some(12)

By using ap we can take a box that houses a function, and call that function on another boxed value. Pretty cool! Notice that it’s the act of maping a partially applied (or curried) function that gets us into this ‘boxed-function’ situation. Typically, that’s how we’ll encounter wild Applicatives. This revelation that mapping a curried function results in an Applicative Functor can lead us to one of the weirdest-named functions you’ll encounter in FP — liftA2 and family:

通过使用ap我们可以选择一个包含函数的框,然后在另一个带框的value上调用该函数。 太酷了! 注意, map到部分应用(或curried )函数的动作使我们陷入这种“盒装函数”情况。 通常,这就是我们遇到狂野的Applicatives的方式。 揭示了映射咖喱函数会产生Applicative Functor的启示,可以使我们找到在FP中会遇到的最奇怪的命名函数之一liftA2和family:

//liftA2 :: (a ⟶ b ⟶ c) ⟶ F a ⟶ F (b ⟶ c) ⟶ F c
const liftA2
= (fn, ftA, ftB) ⟹ ftA.map(fn).ap(ftB)

Yeah, let’s discuss that. I personally find the Haskell-style type definitions to be incredibly helpful when properly parenthesized (even though I’ve never written Haskell codes!), but not everyone does. First off, liftA2 stands for ‘lift a curried function into Applicative with two parameters’. When you know that, liftA2 makes a lot more sense in name.

是的,让我们讨论一下。 我个人认为,正确地加上括号后,Haskell风格的类型定义会非常有用(即使我从未编写过Haskell代码!),但并非所有人都如此。 首先, liftA2代表“将具有两个参数的咖喱函数提升为Applicative”。 当您知道这一点时, liftA2名称就会有意义得多。

Now let’s explain it play-by-play style. As we now know the name suggests, liftA2 takes a curried function that takes a total of two arguments and returns some morphism of those arguments a ⟶ b ⟶ c as its first parameter. Then it takes some Functor of whatever type that holds a value of type a and that can behave as an Applicative F a. This Functor is the target of our map. The map in this case lifts our curried function into our Functor context, so then we have the leftover resultant function hanging out in our Functor F (b ⟶ c). That Functor is now an Applicative Functor. When we finally call ap, it transforms the contents into the result of the fully applied function, resulting in a Functor holding a value F c. Once you understand how liftA2 works, you can understand how to apply curried functions with more than two curried parameters:

现在,让我们解释一下播放方式。 正如我们现在所知道的,顾名思义, liftA2采用了一个咖喱函数,该函数总共接受两个参数,并返回这些参数a ⟶ b ⟶ c某种形态作为其第一个参数。 然后,它需要保存类型的值无论何种类型的一些函子a ,并且可以表现为一个应用型F a 。 该仿函数是我们map的目标。 在这种情况下, map我们的咖喱函数提升到Functor上下文中,因此剩下的剩余结果函数挂在Functor F (b ⟶ c) 。 该函子现在是应用函子。 当我们最终调用ap ,它将内容转换为完全应用的函数的结果,从而使Functor保持值F c 。 一旦了解了liftA2工作原理,就可以了解如何应用具有两个以上liftA2函数:

const liftA3 = (fn, ftA, ftB, ftC) ⟹ ftA.map(fn).ap(ftB).ap(ftC)const liftA4 = (fn, ftA, ftB, ftC, ftD) ⟹ ...

Now we can accept curried functions with any number of parameters and use them in a Point-Free fashion using our lift functions. We can now unleash awesome Applicative powers! Let’s imagine an application where we must pick some Objects that represent persons along with those persons’ interests from a set of data. We then need to figure out whether any given person has interests in common with another, so that we can predict if they might make good buddies for one another:

现在,我们可以接受具有任意数量参数的咖喱函数,并通过我们的lift函数以无点方式使用它们。 现在,我们可以释放出色的应用能力! 让我们想象一个应用程序,在该应用程序中,我们必须从一组数据中选择一些代表人的对象以及这些人的兴趣。 然后,我们需要弄清某个给定的人是否与另一个人有共同的利益,以便我们可以预测他们是否可以成为彼此的好伙伴:

const DATA = [
{ id: 0,
name: 'Ross',
interests: ['tech', 'programming', 'guitar']
},
{ id: 1,
name: 'John',
interests: ['tech', 'sports', 'stock']
},
{ id: 2,
name: 'Bill',
interests: ['guitar', 'stock', 'football']
}
]

Now we need some tools to pick out a person and get their interests:

现在,我们需要一些工具来挑选一个人并获得他们的兴趣:

//prop :: String ⟶ Object ⟶ Option aconst prop = key ⟹ obj ⟹ Option.of(obj[key])//getByName :: (Number ⟶ Object) ⟶ Option Objectconst getById = (id, data) ⟹ Option.of(data.find(d ⟹ d.id ≡ id))//safeInterests :: Object ⟶ Option [a]const safeInterests = prop('interests')

prop gives us a curried function to get either Some property if it exists on an object, or None. getById looks through an array and finds either Some object or None. safeInterests partially applies prop with the string ‘interests’.

prop为我们提供了一个经过咖喱的函数,以获取对象上是否存在某些属性,或者获取无属性。 getById通过数组查找并找到Some对象或None。 safeInterests将prop与字符串“ interests”部分地应用。

We’ll also need to determine whether two arrays include any items in common:

我们还需要确定两个数组是否包含任何共同点:

//includes :: [a] ⟶ [a] ⟶ Boolean const includes = listA ⟹ listB ⟹ 
listA.some(listB.includes.bind(listB))

I want to make our resulting ‘buddies’ into a Pair object. So let’s implement a Pair Functor quickly as well:

我想将生成的“伙伴”制作为Pair对象。 因此,让我们也快速实现对函子:

const Pair = (a, b) ⟹ ({
[VALUE]: [a, b],
[TAG]: 'Pair',
fst() { return this[VALUE][0] },
snd() { return this[VALUE][1] },
map(f) {
return Pair(this.fst(), f(this.snd()))
},
bimap(fa, fb) {
return Pair(fa(this.fst()), fb(this.snd()))
},
unwrap() { return this[VALUE] }
})//toPair :: Object ⟶ Object ⟶ Pair Object Objectconst toPair = personA ⟹ personB ⟹ Pair(personA, personB)

Pair could be a Monad if I had bothered to implement chain but we won’t need it for this example.

如果我不愿意实现chain那么Pair可能是Monad,但在此示例中我们不需要它。

Now let’s finally use liftA2 to do some cool things!

现在,让我们最后使用liftA2做一些很酷的事情!

//eitherInterests :: (Option Object ⟶ Option Object) ⟶ 
// Boolean ⟶
// Either (Pair Object Object) Err
const eitherInterests = (optPersonA, optPersonB) ⟹ bool ⟹
bool ? liftA2(toPair, optPersonA, optPersonB).chain(Either.of)
: /*else*/ Left(`They did not have similar interests!`)

Here we use liftA2 to make our person A and person B into a Pair if bool is true, otherwise we return a Left for our error. We also chain our Pair into a Right using Either.of. That’s because without chaining it, we end up with Some Pair Object Object instead of Right Pair Object Object. Since we’re working with Either, we want both result types to be of type Either.

在这里,如果bool为true,则使用liftA2将我们的人A和人B配对,否则,将为错误返回Left。 我们还使用Either.of配对配对为权利。 那是因为如果没有链接它,我们最终会得到Some Pair Object Object而不是Right Pair Object Object 。 由于我们正在使用Either,因此我们希望两种结果类型均为Either类型。

//getMatchup :: (Option Object ⟶ Option Object) ⟶ 
// Either (Pair Object Object) Err
const getMatchup = (optPersonA, optPersonB) ⟹
liftA2(includes,
optPersonA.chain(safeInterests),
optPersonB.chain(safeInterests))
.chain(eitherInterests(optPersonA, optPersonB))

In getMatchup we use liftA2 to find the intersection between two persons’ interests with includes. We also have to chain our persons into their interests properties so that includes gets the right parameters. We chain the result of our liftA2, which is an Option Boolean, into an Either (Pair Object Object) Err using our other lifting function, eitherInterests.

getMatchup我们使用liftA2查找includes两个人的兴趣之间的交集。 我们还必须chain我们的人到他们的interests属性,使includes得到正确的参数。 我们使用另一个提升功能( eitherInterests Either (Pair Object Object) Err将作为Option Boolean liftA2的结果liftA2到一个Either (Pair Object Object) Err

Finally we have something we can work with! Now we will have either a pair of buddies, or a message that the two persons weren’t a good match:

最后,我们有一些可以合作的东西! 现在,我们将有一对伙伴,或者一条消息,说明这两个人并不匹配:

//Option Object -- our peoplesconst Ross  = getById(0, DATA)const John  = getById(1, DATA)const Bill  = getById(2, DATA)//Either (Pair Object Object) Errconst matchupA = getMatchup(Ross, John)const matchupB = getMatchup(Bill, John)const matchupC = getMatchup(Ross, Bill)//logBuddies :: Pair Object Object ⟶ IOconst logBuddies = x ⟹ 
console.log('Buddies: (' + x.bimap(toName, toName).unwrap() +')')//finally we can use the data!
either(console.warn, logBuddies, matchupA)
either(console.warn, logBuddies, matchupB)
either(console.warn, logBuddies, matchupC)

Here’s a pen to show all of that working together (there’s some other stuff tossed in there to make the log and HTML display a little prettier):

这是一支笔,用于显示所有这些内容的协同工作(其中还有其他一些东西,使日志和HTML显示得更漂亮):

Hope you enjoyed another very functional adventure, FP on readers!

希望您喜欢另一本非常实用的冒险游戏,FP对读者!

翻译自: https://medium.com/swlh/beyond-monad-the-applicative-interface-9685d233c1b3

monad

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值