Устранение моего явного состояния, проходящего через лайки, монады и прочее

Я работаю над книгой Land of Lisp на F# (да, странно, я знаю). Для своего первого примера текстового приключения они используют мутацию глобальной переменной, и я бы хотел ее избежать. Моя монад-фу слаба, поэтому сейчас я делаю ужасное состояние, как это:

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.

Как я могу выделить явное состояние, чтобы сделать его красивее? (Предположим, у меня есть доступ к типу монады State, если он помогает; я знаю, что для этого есть пример кода на F#.)

2 ответа

Решение

Если вы хотите использовать монаду состояния, чтобы продвинуть инвентарь игрока и состояние мира через 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")

Если вы хотите использовать изменяемое состояние в 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 Функция может просто вернуть некоторый объект, представляющий запрос на выбор элемента. Тогда у мира будет какой-то движок, который оценивает эти запросы (собранные от всех игроков) и вычисляет новое состояние мира.

Другие вопросы по тегам