F#计算表达式的用法

时间:2018-12-06 21:32:32

标签: f# f#-interactive

我是一名想学习F#和函数式编程逻辑的学生,但是我对计算表达式有疑问。我想我无法理解计算表达式的逻辑,因为我无法解决此问题,并且在该问题中看不到任何有关使用计算表达式的有用信息。我认为这是一种重写F#的几个基本功能并以我们自己的方式实现的方法,但是在这个问题上我看不出重点。谢谢您的时间,也很抱歉问了很长时间。

A function from a type 'env to a type 'a can be seen as a computation that
computes a value of type 'a based on an environment of type 'env. We call such
a computation a reader computation, since compared to ordinary computations,
it can read the given environment. Below you find the following:

    • the definition of a builder that lets you express reader computations
    using computation expressions

    • the definition of a reader computation ask : 'env -> 'env that returns the
    environment

    • the definition of a function runReader : ('env -> 'a) -> 'env -> 'a that
    runs a reader computation on a given environment

    • the definition of a type Expr of arithmetic expressions

Implement a function eval : Expr -> Map<string, int> -> int that evaluates
an expression using an environment which maps identifiers to values.

NB! Use computation expressions for reader computations in your implementation.

Note that partially applying eval to just an expression will yield a function of
type map <string, int> -> int, which can be considered a reader computation.
This observation is the key to using computation expressions.

The expressions are a simplified subset based on
Section 18.2.1 of the F# 4.1 specification:
https://fsharp.org/specs/language-spec/4.1/FSharpSpec-4.1-latest.pdf

*)

type ReaderBuilder () =
member this.Bind   (reader, f) = fun env -> f (reader env) env
member this.Return x           = fun _   -> x

let reader = new ReaderBuilder ()

let ask = id

let runReader = (<|)

type Expr =
| Const  of int          // constant
| Ident  of string       // identifier
| Neg    of Expr         // unary negation, e.g. -1
| Sum    of Expr * Expr  // sum 
| Diff   of Expr * Expr  // difference
| Prod   of Expr * Expr  // product
| Div    of Expr * Expr  // division
| DivRem of Expr * Expr  // division remainder as in 1 % 2 = 1
| Let    of string * Expr * Expr // let expression, the string is the identifier.


let eval (e:Expr) : (Map<string, int> -> int) = failwith "not yet implemented"

// //Example:
// //keeping in mind the expression: let a = 5 in (a + 1) * 6
// let expr = Let ("a",Const 5, Prod(Sum(Ident("a"),Const 1),Const 6))
// eval expr Map.empty<string,int>
// should return 36     
`

2 个答案:

答案 0 :(得分:2)

阅读器计算表达式将允许您通过多次计算隐式地对环境进行线程化。因此,例如,您可能会有类似的内容:

let rec eval e : Map<string,int> -> int =
    reader {
        match e with
        ...
        | Add(e1, e2) ->
            let! i1 = eval e1 // implicitly thread environment through
            let! i2 = eval e2 // same here
            return i1 + i2
        ...
    }

尽管eval的完整签名是Expr -> Map<string,int> -> int,但是当我们在计算表达式中使用let!时,我们只需要传入Expr即可,可以将结果绑定到int,而无需显式传递地图。

请注意,对于IdentLet情况,您实际上需要显式处理地图以查找或设置标识符的值-但您可以使用let! m = ask来拉映射到环境之外。

当然可以编写不使用eval表达式的reader的实现,但是您可能会发现到处线程化环境只会给代码增加乏味的噪音,很难追踪。

答案 1 :(得分:2)

要真正了解计算表达式有多有用,您需要同时使用和不使用解决方案。

没有计算表达式(CE)

在处理单子时,您始终需要一个bind和一个返回函数,在这里我将其称为rtn,因为return是一个关键字:

let bind f reader = fun env -> f (reader env) env
let rtn x = fun _ -> x

它们只是练习中的副本。

要通过Sumbind实现rtn表达式,您可以通过这种方式实现

let rec eval e : Map<string,int> -> int =
    match e with
    ...
    | Sum(e1, e2) -> 
        eval     e1 |> bind (fun i1 -> 
            eval e2 |> bind (fun i2 -> 
               rtn (i1 + i2) ))
    ...

此代码有效,但很难阅读。


有操作员

您可以使用一些bindmap运算符来简化单子代码:

let (>>=) reader f = bind  f         reader
let (|>>) reader f = bind (f >> rtn) reader  // map

然后eval可能看起来像这样:

let rec eval e : Map<string,int> -> int = reader {
    match e with
    ...
    | Sum(e1, e2) -> 
        eval e1 >>= fun i1 -> 
        eval e2 |>> fun i2 -> 
        i1 + i2
    ...

这是一个改进,但是如果您不习惯这种类型的代码,仍然有些奇怪。


使用CE

您可以将其与@kvb答案中的计算表达式进行比较:

let rec eval e : Map<string,int> -> int = reader {
    match e with
    ...
    | Sum(e1, e2) ->
        let! i1 = eval e1
        let! i2 = eval e2
        return i1 + i2
    ...

所有元素都相同,但是CE更加简单明了,更易于理解。看起来像普通代码,而不是单子代码。


没有阅读器Monad

作为练习,让我们看看如果我们不使用Reader Monad而是每次都通过env时,评估会是什么样:

let rec eval e (env: Map<string,int>) : int =
    match e with
    ...
    | Sum(e1, e2) -> 
        let i1 = eval e1 env
        let i2 = eval e2 env
        i1 + i2
    ...

嘿!看起来几乎与CE代码完全一样,除了在读者monad中是隐式的!returnenv之外。