揭秘Scala中的Monad

In my previous post I mentioned how I decided to write about variance despite the fact that there are already several dozen articles about the topic. This is a similar situation. I am aware that this is a drop in the sea. I feel the sea is polluted with complex mathematical explanations involving category theory. Boring explanations which line up one definition after another without providing the gist of things.

在我以前的文章中,我提到了尽管有关于该主题的数十篇文章,但我还是决定如何撰写有关差异的文章。 这是类似的情况。 我知道这是沧海一粟。 我感到海洋被涉及范畴论的复杂数学解释所污染。 在没有提供要旨的情况下,无聊的解释将一个定义接一个定义。

I will not get technical, and by technical I mean mathematical. Stephen Hawking said that he was warned that every equation he put into “A brief history of time” (great book by the way) would halve the sales. I will go by the same principle here. But I will also try not to be too simplistic, and by simplistic I mean vague. We do need to get our hands dirty. Last, but not least, I will provide examples as a primary mean of explaining. Examples are more as a supplement to the main explanation.

我不会学技术,而技术是指数学。 斯蒂芬·霍金(Stephen Hawking)表示,他被警告说,他在“时间的简要历史”(顺便说一句好书)中所采用的每一个方程式都会使销量减半。 在这里,我将遵循相同的原则。 但是,我也将尽量不要过于简单,简单来说,我的意思是模糊的。 我们确实需要弄脏双手。 最后但并非最不重要的一点,我将提供示例作为解释的主要手段。 例子更多地是对主要解释的补充。

介绍 (Introduction)

So, what is a monad?

那么,什么是单子?

You can think of monads as wrappers. You just take an object and wrap it with a monad. It’s like wrapping a present; you take a scarf, a good book or a fancy watch, and wrap it with some shiny paper and a red ribbon. We wrap gifts with shiny paper because they look pretty. We wrap objects with monads because monads provide us with the following two operations:

您可以将monads视为包装器 。 您只需要拿一个对象并用monad包裹即可。 就像包装礼物。 您拿一条围巾,一本好书或一本精美的手表,然后用一些闪亮的纸和一条红丝带将其包裹起来。 我们用闪亮的纸包裹礼物,因为它们看起来很漂亮。 我们用monad包装对象,因为monad为我们提供以下两个操作:

  • identity (return in Haskell, unit in Scala)

    身份 (在Haskell中返回 ,在Scala中为单位 )

  • bind (>;>= in Haskell, flatMap in Scala)

    bind ( > ;> =在Has kell中,在Scala中为latMap)

Scala doesn’t come with a built-in monad type like Haskell so we will model the monad ourselves. If you take a look at some cool functional programming libraries like Scalaz you will find monads there. You will also find the rest of the category theory family (functors, applicatives, monoids and so on). In plain Scala, there’s no such thing out of the box.

Scala没有像Haskell这样的内置monad类型,因此我们将自己建模monad。 如果您看一些酷的功能编程库(例如Scalaz) ,就会在其中找到monad。 您还将找到类别理论家族的其余部分(功能部件,应用程序,monoid等)。 在普通的Scala中,没有开箱即用的东西。

We will model a monad with a generic trait that provides methods unit() and flatMap(). We can call it M instead of Monad simply to be more concise. Here it is:

我们将使用具有提供方法unit()和flatMap()的通用特征的monad进行建模。 为了简明起见,我们可以称其为M而不是Monad。 这里是:

trait M[A] {
  def flatMap[B](f: A => M[B]): M[B]
}
  
def unit[A](x: A): M[A]

See, it provides two aforementioned functions, unit and flatMap. Don’t worry about their signatures, what they do for now or why the unit() method is outside the trait body. We’ll get to that in a moment.

看,它提供了两个上述函数, unitflatMap 。 不必担心它们的签名,它们现在所做的事情,也不必担心unit()方法为何不在特征主体之内。 稍后我们将解决这个问题。

Now, note one very important thing here — there is a concept of monad (which we modeled in Scala half a minute ago). There are concrete monads of flesh and blood which implement those two functions and actually do something (e.g. IO monad). Sometimes people refer to one thing and sometimes the other, so be careful. We will encounter a couple of concrete monads later in this article. You can think of a generic monad as an interface and concrete monads as implementations.

现在,请注意这里非常重要的一件事-有一个monad概念 (我们半分钟前在Scala中建模)。 有具体的血肉monad实现这两个功能并实际执行某些操作(例如,IO monad)。 有时人们指的是一件事,有时指的是另一件事,所以要小心。 在本文的后面,我们将遇到几个具体的monad。 您可以将通用monad视为接口,将具体monad视为实现。

There’s one more thing you should pay attention to — monads take a type parameter. We didn’t just write M; we wrote M[A]. The type parameter is like a label sticker on our gift wrap, saying what kind of object we have inside (in real life this would ruin the surprise, but in programming, we don’t like surprises). So if we want to wrap some object with our monad wrapper, we must parameterize the monad with the type of the underlying object, e.g. M[Int], M[String], M[MyClass] etc.

您还需要注意一件事-monad采用类型参数。 我们不仅写了M,还写了M。 我们写了M [A]。 类型参数就像礼物包装上的标签贴纸,说出我们里面有什么样的对象(在现实生活中,这会毁了惊喜,但在编程中,我们不喜欢惊喜)。 因此,如果要使用monad包装器包装某些对象,则必须使用基础对象的类型对monad进行参数化,例如M [Int],M [String],M [MyClass]等。

Now let’s take a closer look at those two functions.

现在,让我们仔细看看这两个功能。

Monad功能 (Monad functions)

In case you just joined us, let me repeat that monads come with two operations, unit (also known as identity or return) and flatMap (also known as bind). By the way, literature and online sources are rich with all kinds of naming. I merely mentioned the most common ones. Wait, I forgot one — unit is also often called pure. But don’t worry, it’s always pretty straightforward which function is being referred to, no matter which naming is used. Oh, they also use zero for unit sometimes. All right, let’s move on.

如果您刚刚加入我们,让我再说一遍monad带有两个操作,即unit (也称为identityreturn )和flatMap (也称为bind )。 顺便说一下,文献和在线资源都包含各种命名。 我只提到了最常见的那些。 等等,我忘了一个-单位也常称为pure 。 但是不用担心,无论使用哪种命名方式,引用哪个函数总是很简单的。 哦,有时它们也使用作为单位。 好吧,让我们继续前进。

Unit performs the wrapping part. So, in case of our gift wrap analogy, we can pass a book to unit(). It will return our book wrapped up in this super cool shiny colorful wrap paper, with a “book” label on it. And in case of our monad trait, if we give it a value of type A, it will return a value of type M[A]. It’s sort of a monad constructor, if you will.

单元执行包裹部分。 因此,以我们的礼品包装类比为例,我们可以将一本书传递给unit()。 它会把我们的书包装在这种超酷,有光泽的彩色包装纸中,并在上面贴上“书”标签。 对于我们的monad特征,如果我们给它一个类型A的值,它将返回一个类型M [A]的值。 如果可以的话,它有点像monad构造函数。

It should become clear at this point why we defined the method unit() outside the trait body. Because we don’t want to invoke it upon the existing monadic object (e.g. myMonad.unit(“myBook”)). That wouldn’t make much sense. We want it as a standalone static method (e.g. unit(“myBook”)). By passing our book to unit(), we get it back wrapped in a monad.

在这一点上应该清楚为什么我们在特征主体之外定义方法unit()。 因为我们不想在现有的monadic对象(例如,myMonad.unit(“ myBook”))上调用它。 那没有多大意义。 我们希望它作为一个独立的静态方法(例如unit(“ myBook”))。 通过将我们的书传递给unit(),我们将其包装回单子。

Now, about the flatMap. You may already be familiar with it; it’s the very same flatMap you can encounter in other places in Scala, such as collections. Here’s its signature:

现在,关于flatMap。 您可能已经熟悉了。 您在Scala的其他地方(例如集合)会遇到相同的flatMap。 这是它的签名:

def flatMap[B](f: (A) => U[B]): U[B]

Let’s say U is a List. It works for various other types, but we’ll use List for this example. Now, what flatMap does is that it takes a function with signature A List[B] and it uses that function to transform the underlying object of type A into a List[B]. This operation is known as map. Since we transformed our underlying A into a List[B], this leaves us with a List[List[B]]. But we did not use ordinary map() — we used flatMap(). This means that the job is not done yet; flatMap will now “flatten” our List[List[B]] into List[B].

假设U是一个列表。 它适用于其他各种类型,但在此示例中将使用List。 现在,flatMap要做的是,它使用带有签名A List [B]的函数,并使用该函数将类型A的基础对象转换为List [B]。 此操作称为map 。 由于我们将基础A转换为List [B],因此我们得到了List [List [B]]。 但是我们没有使用普通的map(),而是使用了flatMap()。 这意味着该工作尚未完成; flatMap现在将把我们的List [List [B]]“拉平”到List [B]。

Let’s look at a concrete example. If A is Int, and our function f is

让我们看一个具体的例子。 如果A为Int,则我们的函数f

val f = (i: Int) => List(i - 1, i, i + 1)

then we can flatMap a list of integers with f as follows:

那么我们可以用FlatMap将带有f的整数列表如下:

val list = List(5, 6, 7)
println(list.flatMap(f))
// prints List(4, 5, 6, 5, 6, 7, 6, 7, 8)

Note that regular map() would take each number i and expand it to a mini-list (predecessor, original number, successor) which would give us the following list of mini-lists: List((4, 5, 6), (5, 6, 7), (6, 7, 8)). FlatMap goes one step further; it flattens that into one long list, resulting inList(4, 5, 6, 5, 6, 7, 6, 7, 8).

请注意,常规map()会将每个数字i扩展到一个迷你列表(前身,原始数字,后继) ,这将为我们提供以下迷你列表:List((4,5,6),( 5、6、7),(6、7、8))。 FlatMap更进一步; 它将其展平为一个长列表,从而得到List(4,5,6,5,6,6,7,6,7,8)。

Now let’s say that we have some class M with an underlying type A, written as M[A]. If we want to flatMap that sh*t (I didn’t invent this phrase; try Googling it) we need to provide a function A M[B]. FlatMap will then use this function to transform our underlying A into M[B], resulting in a M[M[B]], and then it will flatten the whole thing into M[B].

现在,我们有一些类M,其基础类型为A,写为M [A]。 如果我们想把flat * ssh * t (我没有发明这个短语;尝试使用谷歌搜索),我们需要提供一个函数A M [B]。 然后,FlatMap将使用此函数将我们的基础A转换为M [B],从而得到M [M [B]],然后将整个对象压平为M [B]。

map with A => M[B]                  flatten
M[A]  ------------------------->  M[M[B]]  -----------> M[B]

Notice how flatMap doesn’t require a function A M[A], but a more flexible one, A M[B]. So if M is a List and A is an Int, we can feed the flatMap with functions such as Int List[String], Int List[MyClass] and so on. It doesn’t have to be Int List[Int]. For example, we could have defined f as:

请注意,flatMap不需要功能A M [A],而是更灵活的功能A M [B]。 因此,如果M是一个List而A是一个Int,我们可以使用诸如Int List [String],Int List [MyClass]等函数为flatMap提供数据。 它不必是Int List [Int]。 例如,我们可以将f定义为:

val f = (i: Int) => List("pred=" + (i - 1), "succ=" + (i + 1))

and then we could flatMap a list of integers with it like this:

然后我们可以用flatMap将整数列表像这样:

val list = List(5, 6, 7)
println(list.flatMap(f))
// prints List(pred=4, succ=6, pred=5, succ=7, pred=6, succ=8)

FlatMap is way more powerful than map. It gives us the ability to chain operations together, as you’ll see in the examples section. Map functionality is just a subset of flatMap functionality. If you want to have map() available in your monad, you can express it using monad’s existing methods flatMap() and unit() like this (note that g is some function Int → Something, not Int → List[Something]):

FlatMap比地图更强大。 如示例部分所示,它使我们能够将操作链接在一起。 地图功能只是flatMap功能的一部分。 如果您想在monad中使用map(),则可以使用monad的现有方法flatMap()和unit()来表示它(请注意g是某个函数Int→Something ,而不是Int→List [Something] ):

m map g = flatMap(x => unit(g(x)))

If all we had was map and unit, we would not be able to define flatMap because neither one of them has a clue about flattening. Flatten (also known as join in Haskell) is a very important part of the process in our monad machine. If we had a map, but no ability to flatten (and therefore no flatMap), then we would end up with what is known in category theory as functor. Functors are pretty cool too, but monads are the star of our show so let’s not digress.

如果我们只有地图和单位,我们将无法定义flatMap,因为它们两个都没有关于展平的线索。 Flatten(在Haskell中也称为join )是我们monad机器过程中非常重要的一部分。 如果我们有一个地图,但是没有展平的能力(因此也没有flatMap),那么我们最终会得到类别理论中称为functor的东西 。 函子也很酷,但是monad是我们节目的主角,所以不要离题。

By the way, if we decompose flatMap into map (fmap) and flatten (join) we will create a completely equivalent and valid definition of the monad. That definition would then look something like this:

顺便说一下,如果我们将flatMap分解为map ( fmap )和flatten ( join ),我们将创建一个完全等效且有效的monad定义。 该定义将如下所示:

  • def unit: A → F[A]

    定义单位:A→F [A]
  • def map: F[A] → (A → B) → F[B]

    定义图:F [A]→(A→B)→F [B]
  • def flatten: F[F[A]] → F[A]

    def展平:F [F [A]]→F [A]

Note that I wrote map’s signature as a fully standalone, “neutral” function here. Meaning that it’s defined without any additional context instead of as a method on F[A].

请注意,我在此处将地图的签名写为完全独立的“中立”功能。 意味着它是在没有任何其他上下文的情况下定义的,而不是作为F [A]上的方法来定义的。

Such map first takes an instance of F[A] first (say, List(42)), then a function A → B (such as a → a + 1) and then gives us back an instance of F[B] (in our case List(43)). I wrote it that way only to be able to write the corresponding flatten signature as well. If I had written map simply as A → B, assuming that it’s defined as a method on F[A], then I would have had a hard time writing down the signature for flatten. It would have to be a function Unit → F[A], defined only on instances of F[F[A]], which makes things clunky. By writing down the “neutral” signatures things should be clear.

这样的映射首先获取F [A]的实例(例如List(42)),然后获取函数A→B(例如a→a + 1),然后将F [B]的实例返回给我们(在我们的案例清单(43))。 我以这种方式编写它只是为了能够编写相应的扁平化签名。 如果我简单地将地图写为A→B,并假设它是在F [A]上定义的方法,那么我将很难写下签名进行展平。 它必须是一个仅在F [F [A]]实例上定义的功能Unit→F [A],这会使事情变得笨拙。 通过写下“中性”签名,事情应该很清楚。

Anyways, we will stick to the flatMap version, and we will continue to view map’s signature as A → B instead of F[A] → A → B, meaning that F[A] is already known at the time of invocation (it’s the instance upon which we’re invoking the method map()).

无论如何,我们将继续使用flatMap版本,我们将继续以A→B而不是F [A]→A→B 形式查看地图的签名,这意味着在调用时已经知道F [A](即我们在其上调用方法map()的实例。

暴露单子 (Exposing the monad)

As I said earlier, there is no such thing as monad type class in Scala. But that doesn’t mean there are no monads in Scala. Monad is not a class or a trait; monad is a concept. Every “wrapper” that provides us with our two beloved operations, unit, and flatMap, is essentially a monad (well, it’s not really enough to just provide methods with those names, they, of course, have to follow certain laws, but we’ll get to that).

正如我之前所说的,Scala中没有诸如monad类型的类。 但这并不意味着Scala中没有monad 。 Monad不是阶级也不是特质。 monad是一个概念。 每个为我们提供两个受欢迎的操作unitflatMap的 “包装器”本质上都是monad(嗯,仅提供具有这些名称的方法还不够,它们当然必须遵循某些定律,但是我们”。

I think we can now finally put the two and two together and realize that List is a monad! Let that sink in. What a plot twist, huh? It’s like in that movie when you realize that it was *him* all along. But wait — there are others! Set? Monad. Option? Monad. Future? Monad!(*)

我认为我们现在终于可以将两个和两个放在一起,并意识到List是单子 ! 让它沉入。有什么阴谋扭曲,是吗? 就像那部电影中,当您意识到自己一直都是他。 但是,等等-还有其他人! 组? 单子。 选项? 单子。 未来? 单子!(*)

OK, it’s a mass conspiracy movie. Monads are everywhere!

好吧,这是一部大规模的阴谋电影。 单子无处不在!

(*): There is a slight controversy whether Future really is or isn’t a monad. Since this is a beginners-friendly text, I will just say that it is a monad and call it a day.

(*):Future真的是单子还是不是单子,都存在一点争议。 由于这是一本适合初学者使用的文字,我只想说这是一个monad,并称之为一天。

In case someone from the Future-is-not-a-true-monad camp is reading this, I’m sure you’ll agree that this is not the place for that debate. Perhaps I’ll even write an article on that too someday.

如果未来不是真正的单子阵营的某人正在阅读此书,我相信您会同意,这不是该辩论的地方。 也许有一天我甚至会写一篇文章。

I agree that their breaking of some fundamental functional programming principles such as referential transparency should not go unnoticed. But let’s leave that for some other time.

我同意他们对某些基本功能性编程原则(例如引用透明性)的破坏不应引起人们的注意。 但是,让我们再待一段时间。

When you look at them up close, you can see that each one indeed has a flatMap method. What about unit? Well, remember that unit is an operation that creates a monad M[A] from an object of type A. This means that a simple apply() serves as a perfectly good unit. So if we have an object called x, here’s what unit operation looks like in various monads (recall that there’s syntax sugar that allows us to invoke e.g. List.apply(3) as List(3)):

当您近距离查看它们时,您会发现每个对象确实都有一个flatMap方法。 那单位呢? 好吧,请记住,单位是从类型为A的对象创建monad M [A]的操作。这意味着简单的apply()是一个非常好的单位。 因此,如果我们有一个名为x的对象,则这是在各种monad中的单位运算的样子(回想一下,有语法糖可以让我们以List(3)的形式调用List.apply(3)):

List:          Set:          Option:                 Future:
------------------------------------------------------------------
List(x)        Set(x)        Some(x) or Option(x)    Future(x)

Also, as I said earlier, there is no actual monad type class in plain Scala. Constructs such as List, Option, Future etc. don’t extend any special Monad trait (it doesn’t exist). This means that they are not obligated to provide us with methods called unit and flatMap. It’s by observing them and seeing that they have methods unit() and flatMap() with correct signatures and behavior that we can deduct that they are, in fact, monads.

而且,正如我之前说的,纯Scala中没有实际的monad类型类。 诸如List,Option,Future等的构造不会扩展任何特殊的Monad特性(它不存在)。 这意味着他们没有义务向我们提供称为unitflatMap的方法。 通过观察它们并看到它们具有正确签名和行为的unit()和flatMap()方法,我们可以推断出它们实际上是monad。

I can hear you say “but if there’s no actual unit() method in Scala (since we said that apply() in monad’s companion object serves as unit), what did you mean by saying “return in Haskell, unit in Scala? What, unit is a name for a function that doesn’t exist?”

我可以听到您说“但是,如果Scala中没有实际的unit()方法(因为我们说过monad的同伴对象中的apply()用作单位),那么您说的意思是”在Haskell中返回 ,即Scala中的unit ? 什么是单位 ,是不存在的功能的名称?”

That’s a good observation, and yes, you are quite right. “Unit” is merely a convention for referencing monad’s identity operation in Scala. You can create a perfectly good custom monad and call its methods galvanize and dropTheBass instead of unit and flatMap if you wish. As long as they have proper signatures and do what they’re supposed to, it will conceptually be a monad. But convention is a nice thing, and Scala community embraced the terms flatMap (as seen in List, Option, Future) for the bind operation and unit (by convention implemented as apply()) for the identity operation.

这是一个很好的观察,是的,您是正确的。 “单元”仅是引用Scala中monad身份操作的约定。 您可以创建一个完美的自定义monad,并根据需要调用其方法galvanizedropTheBass而不是unitflatMap 。 只要它们具有适当的签名并按预期方式执行,从概念上讲它将是单子。 但是约定是一件好事,Scala社区接受了针对绑定操作的术语flatMap (在List,Option,Future中看到)和对于标识操作的单位 (通过约定实现为apply())。

I said, “as long as they have proper signatures and do what they’re supposed to”. OK, we covered the signatures part, but we never really specified what it is exactly that these methods have to do. I mean, we discussed how they should behave, but this is not really enough to define their requirements in more concrete terms. This is where monad laws come into play. These laws must be obeyed by unit and flatMap if our monad is to indeed be a true, proper monad.

我说,“只要他们有适当的签名并按照他们的意愿去做”。 好的,我们涵盖了签名部分,但是我们从未真正指定这些方法必须执行的操作。 我的意思是,我们讨论了他们应该如何表现,但这还不足以更具体地定义他们的要求。 这是单子法生效的地方。 如果我们的monad确实是一个真实,适当的monad,则unitflatMap必须遵守这些法律。

Since this is a beginner-friendly text, I will not go into too much details about the theory behind the laws or even demonstrating their correctness. For now it’s important that you know that they exist. It’s OK if you postpone working through them until you get some practice.

由于这是一本适合初学者使用的文章,因此,我不会过多地讨论法律背后的理论,甚至不会证明其正确性。 现在,重要的是要知道它们的存在。 如果您推迟通过他们的工作直到您进行一些练习就可以了。

So, if we have some basic value x, a monad instance m (holding some value) and functions f and g of type Int → M[Int], we can write the laws as follows:

因此,如果我们有一些基本值x ,一个monad实例m (持有一些值),并且函数Int→M [Int]的函数fg可以写成如下定律:

  • left-identity law:

    左恒定律

    left-identity law: unit(x).flatMap(f) == f(x)

    左恒律unit(x).flatMap(f)== f(x)

  • right-identity law:

    权利认同法

    right-identity law: m.flatMap(unit) == m

    右恒律m.flatMap(unit)== m

  • associativity law:

    结合律

    associativity law:m.flatMap(f).flatMap(g) == m.flatMap(x ⇒ f(x).flatMap(g))

    关联律m.flatMap(f).flatMap(g)== m.flatMap(x⇒f(x).flatMap(g))

Alright. So far we have achieved two out of three goals that I aimed for in this article. We explained the concept of a monad and we drew a parallel to some real-life monads in Scala. By the way, in the beginning I only mentioned IO-monad as an example of a concrete monad. I wanted to postpone mentioning the others until you had an idea about the general concept.

好的。 到目前为止,我们已经实现了本文所针对的三个目标中的两个。 我们解释了monad的概念,并绘制了与Scala中一些现实生活中的monad类似的东西。 顺便说一句,在一开始,我仅以IO-monad为例来举例说明。 我想推迟提及其他内容,直到您对一般概念有所了解为止。

In case you’re wondering what the hell is an IO-monad anyway, it’s a quite complex little thingy used for IO operations in purely functional languages such as Haskell. This is not the time or place to dive deeper into that one.

万一您想知道到底什么是IO-monad,在纯函数式语言(例如Haskell)中进行IO操作时,这是一件相当复杂的事情。 现在不是深入研究该问题的时间或地点。

Time for the third goal — why are monads useful?

第三个目标的时间-为什么Monad有用?

实践中的Monad:选项 (Monads in practice: Option)

In this section, I will show two monads, Option, and Future.

在本节中,我将显示两个单子,即Option和Future。

We’re starting with Option. As you probably know, Option is a construct that allows us to avoid null pointers in Scala (in Haskell it’s called Maybe). We use it for things that may or may not have a defined value. If a value is defined, option equals Some(value), and if it’s not defined, it equals None.

我们从Option开始。 您可能知道,Option是一种结构,使我们可以避免Scala中的空指针(在Haskell中称为Maybe)。 我们将其用于可能具有或未具有定义值的事物。 如果定义了一个值,则选项等于Some(value),如果未定义,则等于无。

Let’s say we have a bunch of users stored in some database. We also have a service that can load a user from that database with a method loadUser(). It takes a name and provides us with an Option[User] because user with that name may or may not exist.

假设我们有一堆用户存储在某个数据库中。 我们还有一项服务,可以使用loadUser()方法从该数据库加载用户。 它使用一个名称并为我们提供Option [User],因为使用该名称的用户可能存在也可能不存在。

Each user may or may not have a child (for the sake of the example let’s say there’s a law enforced in the state which allows a maximum of one child). Note that the child is also of type User, so it can have a child too.

每个用户可能有也可能没有孩子(就本示例而言,假设在该州实施了一项法律,该法律最多允许一个孩子)。 请注意,子项也属于User类型,因此它也可以有一个子项。

Last, but not least — we have a simple function getChild which returns the child for a given user.

最后但并非最不重要的一点-我们有一个简单的getChild函数,该函数返回给定用户的孩子。

trait User {
  val child: Option[User]
}

object UserService {
  def loadUser(name: String): Option[User] = { /** get user **/ }
}

val getChild = (user: User) => user.child

Now let’s say we want to load a user from the database and if they exist we want to see if they have a grandchild. We need to invoke these three functions:

现在,我们要从数据库中加载用户,如果他们存在,我们想看看他们是否有孙子。 我们需要调用以下三个功能:

String Option[User] // load from dbUser Option[User] // get childUser Option[User] // get child’s child

字符串选项[用户] //从dbUser加载选项[用户] //获取childUser 选项[用户] //获取子项

And here’s the code.

这是代码。

val result = UserService.loadUser("mike")
  .flatMap(getChild)
  .flatMap(getChild)

If you didn’t know how to flatMap a monad, you would probably wind up writing a couple of nested if-then-else branches, checking if option is defined. Nothing wrong with that, but this is far more elegant, concise and in the spirit of functional programming.

如果您不知道如何对一个monad进行flatMap,则可能会写一些嵌套的if-then-else分支,检查是否定义了option。 没什么错,但是这更加优雅,简洁并且符合函数式编程的精神。

OK, let’s take a closer look at our “optional user” monad. Remember what we learned earlier. Here’s the analogy.

好的,让我们仔细看看我们的“可选用户” monad。 记住我们之前学到的东西。 这是个比喻。

generic monad:
--------------
unit:     A => M[A]           
flatMap: (A => M[B]) => M[B]

our monad:
-------------- 
unit:     User => Option[User]
flatMap: (User => Option[User]) => Option[User]

By the way, you can also write those functions as in-place lambda functions instead of defining them a priori. Then the code becomes this:

顺便说一句,您也可以将这些函数编写为就地lambda函数,而不用先验地定义它们。 然后代码变为:

val result = UserService.loadUser("mike")
  .flatMap(user => user.child)
  .flatMap(user => user.child)

or even more concise:

甚至更简洁:

val result = UserService.loadUser("mike")
  .flatMap(_.child)
  .flatMap(_.child)

You can also use a for-comprehension which is basically syntax sugar for mapping, flatMapping, and filtering. I don’t want to digress too much so I won’t explain it here, you can look it up; I’ll just show you the code.

您还可以使用for-compression,这基本上是用于映射,flatMapping和过滤的语法糖。 我不想偏离太多,所以我在这里不做解释,你可以查一下。 我只向您显示代码。

val result = for {
  user             <- UserService.loadUser("mike)
  usersChild       <- user.child
  usersGrandChild  <- usersChild.child
} yield usersGrandChild

If you find all this a bit confusing, fiddling around with your own code helps a lot. You can create some dummy users, add a basic implementation to UserService.loadUser() so that it return one of them. Make them raise a ton of children and grandchildren and flatMap the living daylights out of them.

如果您发现所有这些都令人困惑,那么摆弄自己的代码会很有帮助。 您可以创建一些虚拟用户,向UserService.loadUser()添加一个基本实现,以便它返回其中一个。 让他们养育大量的子孙后代,并绘制出生活中的日光。

实践中的Monad:未来 (Monads in practice: Future)

Future is a wrapper over some asynchronous operation. Once the future has been completed you can do whatever it is you need to do with its result.

Future是某些异步操作的包装。 未来完成后,您可以根据结果做任何事情。

There are two main ways to use a future:

使用未来有两种主要方法:

  • use future.onComplete() to define a callback that will work with the result of the future (not so cool)

    使用future.onComplete()定义将与将来的结果一起使用的回调(不是很酷)
  • use future.flatMap() to simply say which operations should be performed on the result once future is complete (cleaner and more powerful since you can return the result of the last operation)

    使用future.flatMap()简单地说出将来完成后应该对结果执行哪些操作(更干净,更强大,因为您可以返回上一个操作的结果)

On to our example. We have an online store and customers who have placed thousands of orders. For each customer, we must now get his/her order, check which item the order is for, get the corresponding item from the database, make the actual purchase and write the result of purchase operation to log. Let’s see that in code.

继续我们的例子。 我们有一个在线商店,客户已下达数千个订单。 现在,对于每个客户,我们必须获取他/她的订单,检查该订单用于哪个项目,从数据库中获取相应的项目,进行实际购买并将购买操作的结果写入日志。 让我们在代码中看看。

// needed for Futures to work
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

trait Order
trait Item
trait PurchaseResult
trait LogResult
object OrderService {  
  def loadOrder(username: String): Future[Order] 
}

object ItemService {  
  def loadItem(order: Order): Future[Item] 
}

object PurchasingService { 
  def purchaseItem(item: Item): Future[PurchaseResult]
  def logPurchase(purchaseResult: PurchaseResult): Future[LogResult] 
}

By the way, don’t mind stuff like referencing global objects from within functions. I know it’s not best practice. But that is completely beside the point here. Also, note that code above doesn’t compile because I left out the implementations of service methods for clarity. Again, if you want to play with the example yourself (and I recommend it), you can create mock implementations yourself. e.g. def loadItem(order: Order) = Future(new Item{}).

顺便说一句,不要介意从函数内部引用全局对象之类的东西。 我知道这不是最佳做法。 但这完全与本文无关。 另外,请注意,上面的代码无法编译,因为为了清楚起见,我省略了服务方法的实现。 同样,如果您想自己玩这个示例(我推荐这样做),则可以自己创建模拟实现。 例如def loadItem(order:Order)= Future(new Item {})。

Now, similarly to the Option example, there are a couple of functions that we will use. They’re pretty trivial as each one simply invokes a method from the corresponding service.

现在,类似于Option示例,我们将使用几个函数。 它们非常简单,因为每个人只需从相应的服务中调用一个方法即可。

val loadItem: Order => Future[Item] = {
  order => ItemService.loadItem(order)
}

val purchaseItem: Item => Future[PurchaseResult] = {
  item => PurchasingService.purchaseItem(item)
}

val logPurchase: PurchaseResult => Future[LogResult] = {
  purchaseResult => PurchasingService.logPurchase(purchaseResult)
}

We need to load the order for a given customer, get the item in question, make the purchase of that item and log the result. It’s as easy as:

我们需要为给定的客户加载订单,获取有问题的商品,购买该商品并记录结果。 就像这样简单:

val result = 
  OrderService.loadOrder("customerUsername")
  .flatMap(loadItem)
  .flatMap(purchaseItem)
  .flatMap(logPurchase)

Here’s the equally nice for-comprehension alternative, using direct service method invocations instead of the functions that were used above:

这是同样不错的理解方法,使用直接服务方法调用而不是上面使用的功能:

val result =
  for {
    loadedOrder    <- orderService.loadOrder(“customerUsername”)
    loadedItem     <- itemService.loadItem(loadedOrder)
    purchaseResult <- purchasingService.purchaseItem(loadedItem)
    logResult      <- purchasingService.logPurchase(purchaseResult)
  } yield logResult

That’s it. I hope I managed to shed some light on the mystery of monads.

而已。 我希望我能阐明单子的奥秘。

结论 (Conclusion)

Monad with his two weapons, unit and flatMap, is a pretty powerful guy. Of course, they are not the solution to all your problems. But thinking in this way (chaining operations and manipulating data using map, flatMap, filter etc., accompanied by other functional programming constructs such as pattern matching) really improves your reasoning about the code and lowers the number of bugs in it.

Monad拥有两个武器,即unit和flatMap,是一个非常强大的家伙。 当然,它们并不是您所有问题的解决方案。 但是以这种方式思考(使用map,flatMap,filter等链接操作和处理数据,以及其他功能性编程构造(例如模式匹配)),确实可以改善您对代码的推理,并减少其中的错误数量。

And given the fact that code is much more often read than written, readability and clearness of such code is a big plus. For example, here’s an excerpt from the code I wrote at work today (I changed the names):

考虑到代码的可读性比书面性高,因此其可读性和清晰度很高。 例如,以下是我今天在工作中编写的代码的摘录(我更改了名称):

itemService.loadItems(order).flatMap {

  case Success(items) =>
    val updateResults = items.map { item =>
      itemService.purchase(item, order.owner)
    }
    Future.sequence(updateResults)
      .map(toPurchaseResults(_))
      .map(mergeResults(_))
      
  case RepositoryFailure(failure) =>
    Future(Failure(Json.obj(Failed -> FailureLoadingItems)))
    
}

Forget entangled if-branches, nested loops with their off-by-one errors and callback hell. This code is simpler, prettier and, with a little help from the Scala compiler, works the first time you run it.

忘记纠结的if分支,嵌套循环以及它们的错误代码和回调hell 。 该代码更简单,更漂亮,并且在Scala编译器的一点帮助下,在您首次运行它时就可以使用

You can catch a glimpse of some other category theory constructs here or go for a more detailed reading here.

你可以捕捉一些其他类别的理论结构的一瞥这里或者去一个更详细的阅读这里

Regarding functional programming in general, if you like Scala you could start from here.

一般而言,关于函数式编程,如果您喜欢Scala,则可以从此处开始。

There are also some nice libraries that provide functional programming constructs, such as Scalaz (which was already mentioned earlier) and Cats (younger kid on the block) so try playing around with them; you can find some tutorials here.

还有一些不错的库提供功能性的编程结构,例如Scalaz (前面已经提到过)和Cats (年轻的孩子),因此请尝试使用它们。 您可以在此处找到一些教程。

And if you want to get seriously involved with functional programming, you will have to learn Haskell sooner or later (in case you don’t already know it).

而且,如果您想认真地参与函数式编程,则必须早晚学习Haskell (以防万一您不了解它)。

Here’s my email: sinisalouc[at]gmail[dot]com. If you find any mistakes, think that some specific part needs improvement or simply want to get in touch, feel free to contact me.

这是我的电子邮件:sinisalouc [at] gmail [dot] com。 如果发现任何错误,认为某些特定的部分需要改进或只是想取得联系,请随时与我联系。

翻译自: https://www.freecodecamp.org/news/demystifying-the-monad-in-scala-cc716bb6f534/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值