我是一名想学习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
`
答案 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
,而无需显式传递地图。
请注意,对于Ident
和Let
情况,您实际上需要显式处理地图以查找或设置标识符的值-但您可以使用let! m = ask
来拉映射到环境之外。
当然可以编写不使用eval
表达式的reader
的实现,但是您可能会发现到处线程化环境只会给代码增加乏味的噪音,很难追踪。
答案 1 :(得分:2)
要真正了解计算表达式有多有用,您需要同时使用和不使用解决方案。
在处理单子时,您始终需要一个bind
和一个返回函数,在这里我将其称为rtn
,因为return
是一个关键字:
let bind f reader = fun env -> f (reader env) env
let rtn x = fun _ -> x
它们只是练习中的副本。
要通过Sum
和bind
实现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) ))
...
此代码有效,但很难阅读。
您可以使用一些bind
和map
运算符来简化单子代码:
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
...
这是一个改进,但是如果您不习惯这种类型的代码,仍然有些奇怪。
您可以将其与@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而是每次都通过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中是隐式的!
,return
和env
之外。