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