Длинная запись без четких ответов, но с некоторыми идеями о том, как обращаться с вложенными неизменяемыми данными
Как отметил OP, при обсуждении неизменности область, которая игнорируется, как обновить вложенные свойства,Хотя это тривиально в изменчивой структуре (просто перейдите туда и обновите свойство) с неизменяемыми структурами, нужно перейти туда, создать копию с обновленным свойством и затем воссоздать всех родителей.
К счастью, люди думают оэто уже и функциональный шаблон для решения этой проблемы называется Объективы или Призмы .
Объективы имеют репутацию несколько сложных.
Но это ИМО, потому что в Хаскеле они говорят о полиморфных линзах , которые можно определить так:
type Lens s t a b = forall f. Functor f => (a -> f b) -> (s -> f t) // Que?
Храбрее, чем я, реализовали полиморфные линзы вF # (который имеет более упрощенную систему типов, чем Haskell): http://www.fssnip.net/7Pk
(Реализация полиморфной линзы в F # предоставлена AFAIK парнем из Hopac. Он довольно приличный, я говорю)
Полиморфные линзы - это круто, но если снять полиморфные битовые линзы , то их станет намного проще для понимания и все еще оченьгодный к употреблению.
По сути, объектив - это пара функций получения и установки.Это может выглядеть так в F #:
type Lens<'T, 'U> = ('T -> 'U)*('T -> 'U -> 'T)
При заданном значении получатель получает свойство этого значения.При заданном значении и значении свойства установщик создает новую копию значения с обновленным свойством.
Его можно рассматривать как функционально компонуемые свойства, удаленно сопоставимые со свойствами .NET.
Учитываячто ваш пример (и многие вещи реального мира) имеет дело с картами и списками Призмы, как правило, более удобны в использовании.Вот одно предложение:
type [<Struct>] Prism<'T, 'U> = Prism of ('T -> 'U option)*('T -> 'U -> 'T)
Единственное отличие здесь в том, что получатель может вернуть None
, если свойство не существует, например, если сотрудник не существует в списке.
Для призмы мы определяем оператор >->
, который объединяет две призмы в новую, которая позволяет получить свойство focus и обновлять его следующим образом:
let p = PropertyA >-> PropertyB >-> Property C
// Updates the nested property c in b in a and returns a new instance
let newA = a |> set p c
Давайте посмотрим, какэто можно найти в примере из поста OP.
type Company =
{
Name : string
Employees : Map<EmployeeNo, Employee>
}
// Define Prisms for properties of Company
static member _Name : Prism<Company, _> = prism' (fun t v -> { t with Name = v }) (fun t -> t.Name |> Some)
static member _Employees : Prism<Company, _> = prism' (fun t v -> { t with Employees = v }) (fun t -> t.Employees |> Some)
К сожалению, есть немного кода, который окружает призмы, но это можно исправить с помощью инструментов code-gen и, возможно, даже провайдеров типов.
Мы определяем Employee аналогичным образом, и мы можем приступить к определению функций, позволяющих нам манипулировать вложенной неизменной структурой.
// Uses Prisms to update the email
let updateEmail company employeeNo newEmail =
company
// The path to the Employee email
|> set (Company._Employees >-> lookup employeeNo >-> Employee._Email) newEmail
Призмы могут быть цепными, полезными при обновлении более одного свойства.
// Uses Prisms to update the position and salary
let updatePosition company employeeNo newPosition newSalary =
company
// The path to the Employee position
|> set (Company._Employees >-> lookup employeeNo >-> Employee._Position) newPosition
// The path to the Employee salary
|> set (Company._Employees >-> lookup employeeNo >-> Employee._Salary ) newSalary
Хотя приведенный выше пример работает, неэффективно построить два объекта Company и выбросить первый.Лучше было бы перейти к нужному Сотруднику и обновить его перед обновлением объекта Компании.
// Uses Prisms to update the position and salary
// Does so in a more efficient manner
let updatePosition' company employeeNo newPosition newSalary =
// The problem with updatePosition above is that it constructs a new company
// object with position updated and then another one with the salary updated
// A faster approach is to navigate to the employee and once found
// update both the position and the salary
// Updates an employee position & salary
let updater = function
| None -> None
| Some e -> { e with Position = newPosition; Salary = newSalary} |> Some
company
// The path to the employee
|> update (Company._Employees >-> lookup employeeNo) updater
В заключение;реализация, представленная здесь, предназначена для того, чтобы стать отправной точкой для, возможно, другого подхода к работе с вложенными неизменяемыми структурами.Есть некоторые проблемы с реализацией (например, знание, если и почему обновление не удалось).Это решаемо, но я не хотел загрязнять идею множеством забот.
Надеюсь, вы нашли ее интересной.
Полный пример кода:
// A Prism consists of two parts
// a getter that gets a property of a value (might return None)
// a setter that sets a property of a value (returns a new instance)
type [<Struct>] Prism<'T, 'U> = Prism of ('T -> 'U option)*('T -> 'U -> 'T)
module Prism =
let inline prism g s = Prism (g, s)
let inline prism' s g = Prism (g, s)
// join joins two Prisms into a new Prism, this is how we navigate nested structures
// Note: Creates in addition to a nested getter also a nested setter so a Prism
// allows both getting and setting of nested properties
let inline join (Prism (tg, ts)) (Prism (ug, us)) =
let getter t =
match tg t with
| None -> None
| Some tv -> ug tv
let setter t v =
match tg t with
| None -> t
| Some tv -> ts t (us tv v)
prism getter setter
// Prism that allows us to navigate Maps
let inline lookup key =
let getter m = Map.tryFind key m
let setter m v = Map.add key v m
prism getter setter
// Given a Prism and a value returns the nested property pointed out by the prism
let get (Prism (tg, _)) t = tg t
// Given a Prism and a value sets the nested property pointed out by the prism
let set (Prism (_, ts)) v t = ts t v
// Given a Prism and a value allows an update function to see the nested property
// and return update it
let update (Prism (tg, ts)) u t =
match u (tg t) with
| None -> t
| Some tv -> ts t tv
type Prism<'T, 'U> with
static member inline ( >-> ) (t, u) = Prism.join t u
module Demo =
open System
open Prism
// Our Domain Model
type [<Struct>] EmployeeNo = EmployeeNo of int
type Position = Contractor | IndividualContributor | Manager
// So prisms enforces some measure of boiler plating.
// Can be mitigated by code generations and possibly type providers
type Employee =
{
No : EmployeeNo
Name : string
Email : string
Hired : DateTime
Salary : decimal
Position : Position
}
// Define Prisms for properties of Employee
static member _No = prism' (fun t v -> { t with No = v }) (fun t -> t.No |> Some)
static member _Name = prism' (fun t v -> { t with Name = v }) (fun t -> t.Name |> Some)
static member _Email = prism' (fun t v -> { t with Email = v }) (fun t -> t.Email |> Some)
static member _Hired = prism' (fun t v -> { t with Hired = v }) (fun t -> t.Hired |> Some)
static member _Salary = prism' (fun t v -> { t with Salary = v }) (fun t -> t.Salary |> Some)
static member _Position = prism' (fun t v -> { t with Position = v }) (fun t -> t.Position |> Some)
type Company =
{
Name : string
Employees : Map<EmployeeNo, Employee>
}
// Define Prisms for properties of Company
static member _Name : Prism<Company, _> = prism' (fun t v -> { t with Name = v }) (fun t -> t.Name |> Some)
static member _Employees : Prism<Company, _> = prism' (fun t v -> { t with Employees = v }) (fun t -> t.Employees |> Some)
open Prism
// Uses Prisms to update the email
let updateEmail company employeeNo newEmail =
company
// The path to the Employee email
|> set (Company._Employees >-> lookup employeeNo >-> Employee._Email) newEmail
// Uses Prisms to update the position and salary
let updatePosition company employeeNo newPosition newSalary =
company
// The path to the Employee position
|> set (Company._Employees >-> lookup employeeNo >-> Employee._Position) newPosition
// The path to the Employee salary
|> set (Company._Employees >-> lookup employeeNo >-> Employee._Salary ) newSalary
// Uses Prisms to update the position and salary
// Does so in a more efficient manner
let updatePosition' company employeeNo newPosition newSalary =
// The problem with updatePosition above is that it constructs a new company
// object with position updated and then another one with the salary updated
// A faster approach is to navigate to the employee and once found
// update both the position and the salary
// Updates an employee position & salary
let updater = function
| None -> None
| Some e -> { e with Position = newPosition; Salary = newSalary} |> Some
company
// The path to the employee
|> update (Company._Employees >-> lookup employeeNo) updater
let test () =
// The initial state of the company
let company : Company =
let e no name email year month day salary position =
let eno = EmployeeNo no
let e : Employee =
{
No = eno
Name = name
Email = email
Hired = DateTime (year, month, day)
Salary = salary
Position = position
}
eno, e
let es =
[|
e 1 "Bill Gates" "billg@microsoft.com" 1979 1 1 100000M Manager
e 2 "Melinda Gates" "melindag@microsoft.com" 1985 6 6 20000M IndividualContributor
|] |> Map.ofArray
{ Name = "Microsoft"; Employees = es}
// Does some organizational changes of the company
printfn "Initial: %A" company
let company = updateEmail company (EmployeeNo 1) "billg@hotmail.com"
printfn "Changing Bill Gates email: %A" company
let company = updatePosition company (EmployeeNo 2) Manager 200000M
printfn "Promoting Melinda Gates: %A" company
let company = updatePosition' company (EmployeeNo 1) IndividualContributor 10000M
printfn "Demoting Bill Gates: %A" company