F#计算表达式的使用

3

我是一个想学习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个回答

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,而无需显式地传递map。
请注意,在IdentLet情况下,您需要明确处理map以查找或设置标识符的值-但是您可以使用let!m = ask将map从环境中提取出来。
当然,完全可以编写不使用reader表达式的eval实现,但您可能会发现在代码中到处引用环境只会增加乏味的噪音,使其更难以理解。

2
为了真正理解计算表达式的用法,您需要分别使用和不使用它来实现解决方案。

不使用计算表达式(CE)

处理单子时,您始终需要一个bind函数和一个return函数,这里我将其称为rtn,因为return是关键字:
let bind f reader = fun env -> f (reader env) env
let rtn x = fun _ -> x

这些只是练习中的副本。

要使用bindrtn实现Sum表达式,您可以这样做。

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更直截了当,易于理解。它看起来像普通代码而不是单子代码。

没有 Reader Monad

作为一项练习,让我们看看如果我们不使用 Reader Monad 而是每次传递 env 会出现什么样的 eval:

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代码完全相同,除了感叹号!returnenv在reader monad中是隐式的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接