通过像 monads 等方式消除显式状态传递

10

我正在使用F#完成Land of Lisp这本书的学习(是的,很奇怪)。在他们的第一个示例文本冒险游戏中,他们利用了全局变量的突变,而我想避免这种情况。我的Monad编程技能弱,所以现在我正在使用丑陋的状态传递方式:

let pickUp player thing (objects: Map<Location, Thing list>) =
    let objs = objects.[player.Location]
    let attempt = objs |> List.partition (fun o -> o.Name = thing)
    match attempt with
    | [], _ -> "You cannot get that.", player, objs
    | thing :: _, things ->
        let player' = { player with Objects = thing :: player.Objects }
        let msg = sprintf "You are now carrying %s %s" thing.Article thing.Name
        msg, player', things

let player = { Location = Room; Objects = [] }   

let objects =
    [Room, [{ Name = "whiskey"; Article = "some" }; { Name = "bucket"; Article = "a" }];
    Garden, [{ Name = "chain"; Article = "a length of" }]]
    |> Map.ofList

let msg, p', o' = pickUp player "bucket" objects
// etc.

如何因式分解显式状态以使代码更加简洁?(假设我可以访问状态单子类型,如果有帮助的话; 我知道在F#中有示例代码。)

我该如何将显式状态提取出来以使代码更加美观?(假设我可以访问State单子类型,如果有帮助的话;我知道在F#中有一些示例代码可用。)


1
在 F# 中惯用的做法是在类或模块级别使用一些可变变量。但是我了解您可能出于教育目的而想使用状态单子? - wmeyer
1
@wmeyer,你说得对。虽然如果你发布一个习惯用语版本的答案,我仍然会点赞,为了其他可能想知道如何以“正确的方式”使用F#的人。 - J Cooper
1
"Programming F#"一书由Chris Smith编写,其中有一节讲述了这个问题。您可以在Google图书的预览中看到该节的大部分内容(但不是全部)。(我将其作为评论而非答案撰写,因为它并不是一个完整的答案,只是一个参考。)" - wmeyer
2个回答

10

如果你想使用状态单子(State Monad)来贯穿玩家的物品清单和世界状态(通过pickUp函数),这是一个方法:

type State<'s,'a> = State of ('s -> 'a * 's)

type StateBuilder<'s>() =
  member x.Return v : State<'s,_> = State(fun s -> v,s)
  member x.Bind(State v, f) : State<'s,_> =
    State(fun s ->
      let (a,s) = v s
      let (State v') = f a
      v' s)

let withState<'s> = StateBuilder<'s>()

let getState = State(fun s -> s,s)
let putState v = State(fun _ -> (),v)

let runState (State f) init = f init

type Location = Room | Garden
type Thing = { Name : string; Article : string }
type Player = { Location : Location; Objects : Thing list }

let pickUp thing =
  withState {
    let! (player, objects:Map<_,_>) = getState
    let objs = objects.[player.Location]
    let attempt = objs |> List.partition (fun o -> o.Name = thing)    
    match attempt with    
    | [], _ -> 
        return "You cannot get that."
    | thing :: _, things ->    
        let player' = { player with Objects = thing :: player.Objects }        
        let objects' = objects.Add(player.Location, things)
        let msg = sprintf "You are now carrying %s %s" thing.Article thing.Name
        do! putState (player', objects')
        return msg
  }

let player = { Location = Room; Objects = [] }   
let objects =
  [Room, [{ Name = "whiskey"; Article = "some" }; { Name = "bucket"; Article = "a" }]
   Garden, [{ Name = "chain"; Article = "a length of" }]]    
  |> Map.ofList

let (msg, (player', objects')) = 
  (player, objects)
  |> runState (pickUp "bucket")

哇,这比我想象的要简单!谢谢! - J Cooper

9
如果您想在F#中使用可变状态,最好的方法就是编写一个可变对象。您可以像这样声明一个可变的Player类型:
type Player(initial:Location, objects:ResizeArray<Thing>) =
  let mutable location = initial
  member x.AddThing(obj) =
    objects.Add(obj)
  member x.Location 
    with get() = location
    and set(v) = location <- v

使用单子来隐藏可变状态在F#中不是很常见。使用单子可以给你基本上相同的命令式编程模型。它隐藏了状态的传递,但它并没有改变编程模型 - 有一些可变状态使得程序无法并行化。
如果示例使用了突变,则可能是因为它以命令式方式设计。您可以改变程序架构,使其更加函数化。例如,而不是选择物品(并修改玩家),pickUp 函数可以只返回表示请求拾取物品的某个对象。然后,世界将有一些引擎来评估这些请求(从所有玩家收集)并计算世界的新状态。

但是状态单子方法是纯的,这不算什么吗?不过最终我可能会选择你的方法,因为它是最惯用的。虽然这让我感觉像是在写C#而不是F# :) - J Cooper
3
Monad 方法要求你以顺序方式编写程序,这就是为什么在 Haskell 中它很重要的原因。但在 F# 中并不需要。就像可变性一样,它使状态隐含,因此如果重新排序彼此(直接)不依赖的代码行,可能会导致程序行为不同(因为状态改变)。 - Tomas Petricek
如果您将所有对象变成不可变的,并且所有操作都构建对象或整个世界的新状态,那么您可以以更加函数式的方式编写程序。然后,您可能需要使用略微不同的架构。 - Tomas Petricek
1
此外,即使您使用可变对象,也可以从函数式风格中受益 - 在实现某些计算或处理算法时,函数式风格会有所帮助。 - Tomas Petricek
1
你会如何构建新的世界状态呢?假设这个世界有多个玩家,而你需要改变其中一个玩家的状态,你会返回一个新的世界状态,并将旧列表中的1个玩家替换为新列表中的玩家吗?这种方法感觉非常低效。 - chrisortman

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