有人可以用F#来阐明monads/计算表达式及其语法吗 [英] Can someone clarify monads / computation expressions and their syntax, in F#

查看:44
本文介绍了有人可以用F#来阐明monads/计算表达式及其语法吗的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

首先,我读到:

https://fsharpforfunandprofit.com/posts/elevated-world/

https://ericlippert.com/2013/02/21/monads-part-one/

我觉得我已经掌握了所有的部分,但没有将所有部分结合在一起的部分,所以我有几个可以一起回答的问题.

I feel like I have all the pieces, but not the part that joins it all together, so I have several questions that can probably be answered all together.

此外,F#是我第一次遇到monads/计算表达式.我来自C语言,对其他功能语言和这些概念没有任何经验.

Also, F# is the first time I'm confronted to monads / computation expressions. I come from a C background and have no experience with other functional languages and these concepts.

我想澄清一下术语:据我了解,monad是模型,而计算表达式是该模型的F#实现.正确吗?

I would like to clarify the terminology: as far as I understand, monads are the model and computation expressions are the F# implementation of this model. Is that correct?

为此,我似乎理解,当您声明一个表达式时,有一些底层功能(绑定,映射等)被调用,但是需要完全不同的语法(let !、 yield!等).)使用时.但是,您仍然可以根据需要使用原始术语(Option.map等).这似乎很令人困惑,所以我很好奇我是否正确,如果正确,为什么要对同一件事使用两种语法?

To that effect, I seem to understand that there are a few underlying functionalities (bind, map, etc) that are called that way when you declare an expression, but require a totally different syntax (let!, yield!, etc) when used. However, you can still use the original terms as wanted (Option.map, etc). This seems very confusing, so I'm curious if I got this right, and if so, why two syntax for the same thing?

就实际用途而言,在我看来如下:

As far as practical uses, it looks to me like the following:

  • 您描述了一个模型,在该模型中,您将数据包装在您设计的任何容器中并提供函数(例如绑定和映射),以便能够将容器链接到容器操作(例如Result< int,_>-> Result< int),_>)或非容器到容器的操作(例如int-> Result< int,_>)等,对吗?
  • 然后,您在该上下文中构建一个使用该模型的表达式以构建操作链.这是正确的假设,还是我错过了全局?

我通常使用Result,Option等,但是我试图对底层机制有一个很好的认识.

I am routinely using Result, Option, etc but I'm trying to get a good sense of the underlying mechanism.

作为实验,我从网络上获取了此信息:

As experiment, I took this from the web:

type ResultBuilder () =
    member this.Bind(x, f) =
        match x with
        | Ok x    -> f x
        | Error e -> Error e
    member this.Return     x = Ok x
    member this.ReturnFrom x = x

没有真正了解如何使用Return/ReturnFrom,并以这种方式成功使用它:

without truly understanding how Return / ReturnFrom are used, and successfully used it that way:

ResultBuilder() {
    let! r1 = checkForEmptyGrid gridManager
    let! r2 = checkValidation r1
    let! r3 = checkMargin instrument marginAllowed lastTrade r2
    return r3
}

绝对可以跳过本来需要的层次结果匹配链.

and it definitely allowed to skip the hierarchical result match chain I would have needed otherwise.

但是,昨天我发布了一个不相关的问题:尝试扩展结果类型..在F#

But, yesterday I posted a kind of unrelated question: trying to extend the result type.. unsuccesfully, in F#

和用户@Guran指出Result.map可以实现相同的目的.

and user @Guran pointed out that Result.map could achieve the same thing.

所以,我去了 https://blog.jonathanchannon.com/2020-06-28-understanding-fsharp-map-and-bind/,拿出代码并从其中制成了Jupyter笔记本,以便进行使用.

so, I went to https://blog.jonathanchannon.com/2020-06-28-understanding-fsharp-map-and-bind/, took the code and made a Jupyter notebook out of it in order to play with it.

我了解到Map将采用非包装(在Result内部)函数并将结果以包装/结果格式放置,而Bind将附加/绑定已经在Result模型中的函数.

I came to understand that Map will take a non wrapped (inside Result) function and put the result in the wrapped/Result format and Bind will attach/bind functions which are already inside the Result model.

但是以某种方式,尽管顶部的两个链接都深入探讨了该主题,但我似乎看不到全局,也无法直观地看到用于包装/解开包装的不同操作及其结果.自定义模型.

But somehow, despite the two links at the top going through the topic in depth, I don't seem to see the big picture, nor be able to visualize the different operations to wrap / unwrap operations, and their results in a custom model.

推荐答案

好,让我们再尝试一次.可能出什么问题了?:-)

Ok, let's try this one more time. What could go wrong? :-)

编程或多或少与捕获模式有关.好吧,至少它有趣的部分仍然如此.看一下GoF的设计模式"例如.是的,我知道,这是一个不好的例子:-/

Programming is more or less about capturing patterns. Well, at least the fun parts of it anyway. Look at the GoF "design patterns" for example. Yeah, I know, bad example :-/

Monad是为此一种特定模式提供的名称.这种模式变得非常有用,以至于单子获得了神圣的品质,现在每个人都对它们感到敬畏.但实际上,这只是一种模式.

Monad is a name given to this one particular pattern. This pattern became so incredibly useful that monads kind of gained a divine quality and everybody is in awe of them now. But really, it's just a pattern.

要查看模式,请以您的示例为例:

To see the pattern, let's take your example:

  • checkForEmptyGrid
  • checkValidation
  • checkMargin

首先,这些功能中的每一个都可能失败.为了表示我们使它们返回可以成功或失败的 Result< r,err> .到现在为止还挺好.现在,让我们尝试编写程序:

First, every one of those functions may fail. To express that we make them return a Result<r, err> that can be either success or failure. So far so good. Now let's try to write the program:

let checkStuff gridManager instrument marginAllowed lastTrade =
    let r1 = checkForEmptyGrid gridManager
    match r1 with
    | Error err -> Error err
    | Ok r -> 
        let r2 = checkValidation r
        match r2 with
        | Error err -> Error err
        | Ok r ->
            let r3 = checkMargin instrument marginAllowed lastTrade r
            match r3 with
            | Error err -> Error err
            | Ok r -> Ok r

看到图案了吗?看到那三个几乎相同的嵌套块吗?在每个步骤中,我们或多或少都执行相同的操作:我们正在查看上一个结果,如果有错误,请返回该错误,否则请调用下一个函数.

See the pattern yet? See those three nearly identical nested blocks in there? At every step we do more or less the same thing: we're looking at the previous result, if it's an error, return that, and if not, we call the next function.

因此,让我们尝试提取该模式以供重用.毕竟,这就是我们作为程序员所要做的,不是吗?

So let's try to extract that pattern for reuse. After all, that's what we do as programmers, isn't it?

let callNext result nextFunc =
    match result with
    | Error err -> Error err
    | Ok r -> nextFunc r

简单,对吧?现在,我们可以使用此新功能重写原始代码:

Simple, right? Now we can rewrite the original code using this new function:

let checkStuff gridManager instrument marginAllowed lastTrade =
    callNext (checkForEmptyGrid gridManager) (fun r1 ->
        callNext (checkValidation r1) (fun r2 ->
            callNext (checkMargin instrument marginAllowed lastTrade r2) (fun r3 ->
                Ok r3
            )
        )
    )

哦,太好了!那要短多少!之所以较短,是因为我们的代码现在从不处理 Error 情况.这项工作已外包给 callNext .

Oh, nice! How much shorter that is! The reason it's shorter is that our code now never deals with the Error case. That job was outsourced to callNext.

现在让它变得更漂亮.首先,如果翻转 callNext 的参数,则可以使用管道:

Now let's make it a bit prettier. First, if we flip callNext's parameters, we can use piping:

let callNext nextFunc result =
    ...

let checkStuff gridManager instrument marginAllowed lastTrade =
    checkForEmptyGrid gridManager |> callNext (fun r1 ->
        checkValidation r1 |> callNext (fun r2 ->
            checkMargin instrument marginAllowed lastTrade r2 |> callNext (fun r3 ->
                Ok r3
            )
        )
    )

parens少一些,但仍然有点丑陋.如果我们将 callNext 设置为运算符怎么办?让我们看看是否可以得到一些东西:

A bit fewer parens, but still a bit ugly. What if we made callNext an operator? Let's see if we can gain something:

let (>>=) result nextFunc =
    ...

let checkStuff gridManager instrument marginAllowed lastTrade =
    checkForEmptyGrid gridManager >>= fun r1 ->
        checkValidation r1 >>= fun r2 ->
            checkMargin instrument marginAllowed lastTrade r2 >>= fun r3 ->
                Ok r3

哦,太好了!现在,所有功能都不必放在自己的括号中,这是因为运算符语法允许这样做.

Oh nice! Now all the functions don't have to be in their very own parentheses - that's because operator syntax allows it.

但是,等等,我们可以做得更好!将所有压痕向左移动:

But wait, we can do even better! Shift all the indentation to the left:

let checkStuff gridManager instrument marginAllowed lastTrade =
    checkForEmptyGrid gridManager >>= fun r1 ->
    checkValidation r1 >>= fun r2 ->
    checkMargin instrument marginAllowed lastTrade r2 >>= fun r3 ->
    Ok r3

看:现在几乎看起来像我们在分配"每次调用变量"的结果,不是很好吗?

Look: now it almost looks like we're "assigning" result of every call to a "variable", isn't that nice?

然后您就去了.您现在就可以停下来,享受>> = 运算符(顺便说一句,它被称为"bind";-)

And there you go. You can just stop now and enjoy the >>= operator (which is called "bind" by the way ;-)

那是你的单子.

但是等等!我们是程序员,不是吗?概括所有事物!

But wait! We're programmers, aren't we? Generalize all the things!

上面的代码与 Result< __,_> 一起使用,但是实际上 Result 本身在代码中几乎看不见.它可能也可以与 Option 一起使用.看!

The code above works with Result<_,_>, but actually, Result itself is (almost) nowhere to be seen in the code. It might just as well be working with Option. Look!

let (>>=) opt f =
    match opt with
    | Some x -> f x
    | None -> None

let checkStuff gridManager instrument marginAllowed lastTrade =
    checkForEmptyGrid gridManager >>= fun r1 ->
    checkValidation r1 >>= fun r2 ->
    checkMargin instrument marginAllowed lastTrade r2 >>= fun r3 ->
    Some r3

您能在 checkStuff 中发现差异吗?区别只是最后的小 Some ,它代替了以前的 Ok .就是这样!

Can you spot the difference in checkStuff? The difference is just the little Some at the very end, which replaced the Ok that was there before. That's it!

但这还不是全部.除 Result Option 外,这还可以与其他功能一起使用.您知道JavaScript Promise 吗?那些也可以!

But that's not all. This could also work with other things, besides Result and Option. You know JavaScript Promises? Those work too!

let checkStuff gridManager instrument marginAllowed lastTrade =
    checkForEmptyGrid gridManager >>= fun r1 ->
    checkValidation r1 >>= fun r2 ->
    checkMargin instrument marginAllowed lastTrade r2 >>= fun r3 ->
    new Promise(r3)

看到区别了吗?又到了尽头.

See the difference? It's at the very end again.

因此,在凝视了一会儿之后,结果发现这种将下一个功能粘合到上一个结果"的模式就可以了.扩展到很多有用的东西.除了这一点不便之处:最后,我们必须使用不同的方法来构造最终返回值".- Ok 表示 Result Some 表示 Option ,以及任何黑魔法 Promise 实际使用,我不记得了.

So it turns out, after you stare at this for a while, that this pattern of "gluing next function to the previous result" extends to a lot of things that are useful. Except this one little inconvenience: at the very end we have to use different methods of constructing the "ultimate return value" - Ok for Result, Some for Option, and whatever black magic Promises actually use, I don't remember.

但是我们也可以概括一下!为什么?因为它也有一个模式:它是一个函数,该函数需要一个值并返回包装器".( Result Option Promise 或其他),其中包含该值:

But we can generalize that too! Why? Because it also has a pattern: it's a function that takes a value and returns the "wrapper" (Result, Option, Promise, or whatever) with that value inside:

let mkValue v = Ok v  // For Result
let mkValue v = Some v  // For Option
let mkValue v = new Promise(v)  // For Promise

实际上,为了使我们的功能链代码在不同的上下文中工作,我们要做的就是提供> = (通常称为"bind")的适当定义.)和 mkValue (通常称为"return",或更复杂的数学原因,在更现代的Haskell中称为"pure").

So really, in order to make our function-chaining code to work in different contexts, all we need to do is provide suitable definitions of >>= (usually called "bind") and mkValue (usually called "return", or in more modern Haskell - "pure", for complicated maths reasons).

这就是monad:这是针对特定上下文的这两件事的实现.为什么?为了以这种方便的形式写下链接计算,而不是在此答案的最顶部将其作为《厄运的阶梯》.

And that's what a monad is: it's an implementation of those two things for a specific context. Why? In order to write down chaining computations in this convenient form rather than as a Ladder of Doom at the very top of this answer.

但是等等,我们还没有完成!

But wait, we're not done yet!

如此有用的monad证明是功能语言决定为它们提供特殊的语法是非常好的.语法不是魔术,它只是使最后的 bind return 调用无效,但是它使程序看起来更好一点.

So useful monads turned out to be that functional languages decided it would be super nice to actually provide special syntax for them. The syntax is not magic, it just desugars to some bind and return calls in the end, but it makes the program look just a bit nicer.

最干净的工作(在我看来)是在Haskell(及其朋友PureScript)中完成的.这就是所谓的"do notation",这是上面的代码在其中的样子:

The cleanest (in my opinion) job of this is done in Haskell (and its friend PureScript). It's called the "do notation", and here's how the code above would look in it:

checkStuff gridManager instrument marginAllowed lastTrade = do
    r1 <- checkForEmptyGrid gridManager
    r2 <- checkValidation r1
    r3 <- checkMargin instrument marginAllowed lastTrade r2
    return r3

区别在于,对>> = 的调用被翻转",从右到左,并使用特殊关键字<-(是的,这是一个关键字,而不是运算符).看起来很干净,不是吗?

The difference is that calls to >>= are "flipped" from right to left and use the special keyword <- (yes, that's a keyword, not an operator). Looks clean, doesn't it?

但是F#不使用该样式,它有自己的样式.部分原因是由于缺少类型类(因此您每次都必须提供特定的计算生成器),部分原因是,我认为这只是在维护语言的总体美感.我不是F#设计人员,所以我无法确切地说出原因,但是无论它们是什么,等效的语法都是这样:

But F# doesn't use that style, it has its own. Partly this is due to the lack of type classes (so you have to provide a specific computation builder every time), and partly, I think, it's just trying to maintain the general aesthetic of the language. I'm not an F# designer, so I can't speak to the reasons exactly, but whatever they are, the equivalent syntax would be this:

let checkStuff gridManager instrument marginAllowed lastTrade = result {
    let! r1 = checkForEmptyGrid gridManager
    let! r2 = checkValidation r1
    let! r3 = checkMargin instrument marginAllowed lastTrade r2
    return r3
}

而且,除了将调用插入>> = 之外,还需要更多的时间来进行协商.取而代之的是,每个 let!都由对 result.Bind 的调用替换,而每个 return 均由 result.Return 调用..而且,如果您查看这些方法的实现(在问题中引用了它们),您会发现它们在此答案中与我的实现完全匹配.

And the desugaring process is also a bit more involved than just inserting calls to >>=. Instead, every let! is replaced by a call to result.Bind and every return - by result.Return. And if you look at the implementations of those methods (you quoted them in your question), you'll see that they match exactly my implementations in this answer.

区别在于 Bind Return 不在运算符形式中,它们是 ResultBuilder 上的方法,而不是独立的函数.这在F#中是必需的,因为它没有通用的全局重载机制(例如Haskell中的类型类).但是除此之外,想法是一样的.

The difference is that Bind and Return are not in the operator form and they're methods on ResultBuilder, not standalone functions. This is required in F# because it doesn't have a general global overloading mechanism (such as type classes in Haskell). But otherwise the idea is the same.

此外,F#计算表达式实际上试图不仅仅是单子的实现.他们还有其他所有这些东西- for yield join where ,甚至可以添加您的自己的关键字(有一些限制)等.我不完全相信这是最佳的设计选择,但是,嘿!他们工作得很好,所以我该向谁抱怨?

Also, F# computation expressions are actually trying to be more than just an implementation of monads. They also have all this other stuff - for, yield, join, where, and you can even add your own keywords (with some limitations), etc. I'm not completely convinced this was the best design choice, but hey! They work very well, so who am I to complain?

最后,关于 map 的主题.可以将地图视为 bind 的特例.您可以这样实现:

And finally, on the subject of map. Map can be seen as just a special case of bind. You can implement it like this:

let map fn result = result >>= \r -> mkValue (fn r)

但通常 map 被视为自己的东西,而不是 bind 的小弟弟.为什么?因为与 bind 相比,它实际上适用于更多的事物.不能是单子的事物仍然可以具有 map .我不会在这里对此进行扩展,这是其他整篇文章的讨论.只是想快速提及它.

But usually map is seen as its own thing, not as bind's little brother. Why? Because it's actually applicable to more things than bind. Things that cannot be monads can still have map. I'm not going to expand on this here, it's a discussion for a whole other post. Just wanted to quickly mention it.

这篇关于有人可以用F#来阐明monads/计算表达式及其语法吗的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆