Вот тот же код с использованием линз, который в настоящее время определен в FSharpx.Как отмечают другие ответы, здесь удобно использовать записи;среди прочего, они дают вам структурное равенство бесплатно.Я также прикрепляю соответствующие линзы для свойств в качестве статических элементов;Вы также можете определить их в модуле или как свободные функции.Я предпочитаю статические члены здесь, для практических целей это просто как модуль.
open FSharpx
type Monster = {
Awake: bool
} with
static member awake =
{ Get = fun (x: Monster) -> x.Awake
Set = fun v (x: Monster) -> { x with Awake = v } }
type Room = {
Locked: bool
Monsters: Monster list
} with
static member locked =
{ Get = fun (x: Room) -> x.Locked
Set = fun v (x: Room) -> { x with Locked = v } }
static member monsters =
{ Get = fun (x: Room) -> x.Monsters
Set = fun v (x: Room) -> { x with Monsters = v } }
type Level = {
Illumination: int
Rooms: Room list
} with
static member illumination =
{ Get = fun (x: Level) -> x.Illumination
Set = fun v (x: Level) -> { x with Illumination = v } }
static member rooms =
{ Get = fun (x: Level) -> x.Rooms
Set = fun v (x: Level) -> { x with Rooms = v } }
type Dungeon = {
Levels: Level list
} with
static member levels =
{ Get = fun (x: Dungeon) -> x.Levels
Set = fun v (x: Dungeon) -> { x with Levels = v } }
static member print (d: Dungeon) =
d.Levels
|> List.iteri (fun i e ->
printfn "Level %d: Illumination %d" i e.Illumination
e.Rooms |> List.iteri (fun i e ->
let state = if e.Locked then "locked" else "unlocked"
printfn " Room %d is %s" i state
e.Monsters |> List.iteri (fun i e ->
let state = if e.Awake then "awake" else "asleep"
printfn " Monster %d is %s" i state)))
Я также определяю print
как статический член;опять же, это как функция в модуле, и она более компонуема, чем метод экземпляра (хотя я не буду здесь ее составлять).
Теперь для генерации данных примера.Я думаю, что { Monster.Awake = true }
более скептически, чем new Monster(true)
.Если бы вы хотели использовать классы, я бы назвал параметр явно, например, Monster(awake: true)
// generate test dungeon
let m1 = { Monster.Awake = true }
let m2 = { Monster.Awake = false }
let m3 = { Monster.Awake = true }
let m4 = { Monster.Awake = false }
let m5 = { Monster.Awake = true }
let m6 = { Monster.Awake = false }
let m7 = { Monster.Awake = true }
let m8 = { Monster.Awake = false }
let r1 = { Room.Locked = true; Monsters = [m1; m2] }
let r2 = { Room.Locked = false; Monsters = [m3; m4] }
let r3 = { Room.Locked = true; Monsters = [m5; m6] }
let r4 = { Room.Locked = false; Monsters = [m7; m8] }
let l1 = { Level.Illumination = 100; Rooms = [r1; r2] }
let l2 = { Level.Illumination = 50; Rooms = [r3; r4] }
let dungeon = { Dungeon.Levels = [l1; l2] }
Dungeon.print dungeon
Теперь самое интересное: составление линз для обновления монстров для всех комнат для определенного уровня в подземелье:
open FSharpx.Lens.Operators
let mapMonstersOnLevel nLevel f =
Dungeon.levels >>| Lens.forList nLevel >>| Level.rooms >>| Lens.listMap Room.monsters
|> Lens.update (f |> List.map |> List.map)
// toggle wake status of all monsters
let dungeon1 = dungeon |> mapMonstersOnLevel 0 (Monster.awake.Update not)
Dungeon.print dungeon1
Для второго подземелья я также использую линзы, но без состава линз.Это своего рода DSL, определяемый небольшими составными функциями (некоторые функции взяты из линз).Может быть, есть линзы, чтобы выразить это более кратко, но я не понял этого.
// remove monsters that are asleep
// which are in locked rooms on levels where illumination < 100
// and unlock those rooms
let unlock = Room.locked.Set false
let removeAsleepMonsters = Room.monsters.Update (List.filter Monster.awake.Get)
let removeAsleepMonsters_unlock_rooms = List.mapIf Room.locked.Get (unlock >> removeAsleepMonsters)
let isLowIllumination = Level.illumination.Get >> ((>)100)
let removeAsleepMonsters_unlock_level = Level.rooms.Update removeAsleepMonsters_unlock_rooms
let removeAsleepMonsters_unlock_levels = List.mapIf isLowIllumination removeAsleepMonsters_unlock_level
let dungeon2 = dungeon |> Dungeon.levels.Update removeAsleepMonsters_unlock_levels
Dungeon.print dungeon2
Я слишком много использовал линзы и немного бессмысленно, частично специально, просто чтобы показать, как это может выглядеть.Кому-то это не понравится, утверждая, что это не идиоматично и не понятно.Может быть и так, но это еще один инструмент, который вы можете использовать или нет, в зависимости от вашего контекста.
Но что более важно, поскольку Update - это Get, за которым следует функция, за которой следует Set, это не так эффективно, как ваш код, когда дело доходит до обработки списков: Update в Lens.forList сначала получает nthэлемент в списке, который является операцией O (n).
Подведем итог:
Плюсы:
- Очень кратко.
- Включаетстиль pointfree.
- Код с линзами обычно не учитывает представление типа источника (это может быть класс, запись, единичный DU, словарь, это не имеет значения).
Минусы:
- Может быть неэффективным в некоторых случаях в текущей реализации.
- Из-за отсутствия макросов требуется некоторый шаблон.
Спасибо за этот пример, в результате я пересмотрю текущий дизайн линз в FSharpx и посмотрю, можно ли его оптимизировать.
Я передал этот код в репозиторий FSharpx: https://github.com/fsharp/fsharpx/commit/136c763e3529abbf91ad52b8127ce11cbb3dff28