F # Вложенные списки неизменяемости - PullRequest
0 голосов
/ 05 июня 2018

Я снова пытаюсь выбрать F #, и у меня есть случай, когда мне очень трудно иметь дело с неизменяемостью.

В большинстве случаев я считаю неизменность очень хорошей.Однако мне очень трудно иметь дело с вложенными коллекциями.

Я хотел бы написать однопользовательскую программу с графическим интерфейсом.

Для простоты, скажем, у нас есть модель:

type Employee = 
    {
        Name : string
    }

type Company =
    {
        Name : string
        Employees : Employee list
    }

Если я изменю существующий Employee с помощью

let myNewEmployee = { myEmployee with Name = "John Smith" }

, я получу новый Employee, и это нормально.Однако здесь возникает цепочка проблем:

  • Мне нужно удалить старую myEmployee и добавить myNewEmployee в список сотрудников Компании.

  • Это провоцирует мутацию в списке с появлением нового списка.

  • Это вынуждает меня создавать новую запись Company в порядкечтобы внедрить новую коллекцию, нужно перестроить весь список Компании, который у меня есть.

Другими словами, изменение имени сотрудника заставляет меня перестраивать всю структуру данных.

Каждый раз, когда я нахожусь в тупике, я обнаруживаю, что у F # есть другой и творческий способ делать вещи.

Я полагаю, что это мое невежество в том, что я не знаю, как справиться с этой ситуацией функционально, пожалуйста,я;)

Должен ли я использовать другие библиотеки F #, например, F # Data?

TIA, David

Ответы [ 3 ]

0 голосов
/ 05 июня 2018

Функционально-первый стиль F # означает, что вы сохраняете свои функции чистыми по умолчанию в ядре вашей программы и перемещаете необходимые побочные эффекты к краю вашей программы.

Мне нужноудалите старую myEmployee и добавьте myNewEmployee в список сотрудников компании.

Вам действительно нужно сделать это?Я призываю вас подумать об этой цепи, идущей в противоположном направлении.Зачем вам этот новый список?Какое влияние это в конечном итоге оказывает на внешний мир?Может быть, полный список можно построить за один проход, перестроив код в функциональный стиль.Возможно, вы отвечаете на ввод пользователя, и каждый раз можно создавать новый список.Возможно, было бы лучше использовать другую структуру данных, такую ​​как Map (неизменный словарь) с идентификатором сотрудника в качестве ключа.

0 голосов
/ 06 июня 2018

Длинная запись без четких ответов, но с некоторыми идеями о том, как обращаться с вложенными неизменяемыми данными

Как отметил OP, при обсуждении неизменности область, которая игнорируется, как обновить вложенные свойства,Хотя это тривиально в изменчивой структуре (просто перейдите туда и обновите свойство) с неизменяемыми структурами, нужно перейти туда, создать копию с обновленным свойством и затем воссоздать всех родителей.

К счастью, люди думают оэто уже и функциональный шаблон для решения этой проблемы называется Объективы или Призмы .

Объективы имеют репутацию несколько сложных.

Learning curves of popular haskell concepts

Но это ИМО, потому что в Хаскеле они говорят о полиморфных линзах , которые можно определить так:

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
0 голосов
/ 05 июня 2018

Я думаю, что ответ частично зависит от более широкого контекста, например, от того, как реализован пользовательский интерфейс вашего приложения.Вы правы, что изменение одного имени в компании требует от вас создания нового списка и новой записи компании.Это не так уж плохо, если вы сделаете это за один вызов List.map.

Чтобы упростить выборку, я добавил ID типа int к каждому emplyee:

let updateName id name company = 
  let newEmployees = company.Employees |> List.map (fun emp ->
    if emp.ID = id then { emp with Name = name } else emp)
  { company with Employees = newEmployees }

Если вы используете что-то похожее на архитектуру Elm, то, вероятно, это разумный путь.

В некоторых случаях вы можете сделать что-то более умное (но это зависит от вашего сценария).Например, вы можете создать новый тип для представления компании со списком примененных к ней обновлений:

type Update = 
  | Rename of id:int * newName:string

type UpdatedCompany =
  { Company : Company 
    Updates : Update list }

Теперь изменение имени - это просто вопрос добавления нового обновления Renameк списку Updates.Конечно, как только вам нужно отобразить окончательный Company, вам нужно будет выполнить итерацию по всем сотрудникам (как указано выше) и применить обновления.Однако, если вы делаете много обновлений, прежде чем вам нужно получить новое окончательное значение Company, это может быть хорошим трюком.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...