Сопоставление с различными типами возврата - PullRequest
1 голос
/ 15 марта 2020

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

В моем очень простом примере c, скажем, мы хотим управлять клиентами для отеля. Клиент может находиться в разных состояниях:

  1. Новый клиент
  2. Определена контактная информация.
  3. Клиент принял GPDR.
  4. Клиент имеет проверено.

Все эти состояния представлены в виде различных типов. Мы также определяем, что клиент находится в состоянии «Ожидание», пока клиент не зарегистрировался, но предоставил контактную информацию и / или принял GPDR:

type CustomerId = CustomerId of Guid
type ContactInformation = ContactInformation of string
type AcceptDate = AcceptDate of DateTime
type CheckInDate = CheckInDate of DateTime

type NewCustomer =
    private
        { Id: CustomerId }

type ContactOnlyCustomer =
    private
        { Id: CustomerId
          Contact: ContactInformation }

type AcceptedGdprCustomer =
    private
        { Id: CustomerId
          Contact: ContactInformation
          AcceptDate: AcceptDate }

type PendingCustomer =
    private
        | ContactOnly of ContactOnlyCustomer
        | AcceptedGdpr of AcceptedGdprCustomer  

type CheckedInCustomer =
    private
        { Id: CustomerId
          Contact: ContactInformation
          AcceptDate: AcceptDate
          CheckInDate: CheckInDate }    

type Customer =
    private
        | New of NewCustomer
        | Pending of PendingCustomer
        | CheckedIn of CheckedInCustomer

Теперь я хочу обновить контакт информация для клиента со следующей функцией (независимо от того, в каком «состоянии» находится данный клиент):

let updateContact (customer: Customer) contact =
    match customer with
    | New c -> ContactOnly { Id = c.Id; Contact = contact }
    | Pending pending ->
        match pending with
        | ContactOnly c -> ContactOnly { c with Contact = contact }
        | AcceptedGdpr c -> AcceptedGdpr { c with Contact = contact }
    | CheckedIn c -> CheckedIn { c with Contact = contact } // <- Here I get a compile error saying that all branches must return the same type.

Проблема здесь в том, что различные типы возвращаются выражением сопоставления с образцом. Случаи объединения ContactOnly и AcceptedGdpr относятся к типу PendingCustomer, тогда как случаи объединения CheckedIn относятся к типу Customer.

Как справиться с таким сценарием? По сути, случай объединения New должен превратить клиента в клиента ContactOnly. Все остальные случаи (когда клиент уже определил контактную информацию) должны быть обновлены с использованием новой контактной информации.


Я пытался определить тип Customer следующим образом, т.е. переместить случаи объединения DetailsOnly и AcceptedGdpr непосредственно в тип Customer:

type Customer =
    private
        | New of NewCustomer
        | ContactOnly of ContactOnlyCustomer
        | AcceptedGdpr of AcceptedGdprCustomer     
        | CheckedIn of CheckedInCustomer

При этом мне не нужно совпадать с вложенным шаблоном:

let updateDetails (customer: Customer) contact =
    match customer with
    | New c -> ContactOnly { Id = c.Id; Contact = contact }
    | ContactOnly c -> ContactOnly { c with Contact = contact }
    | AcceptedGdpr c -> AcceptedGdpr { c with Contact = contact }
    | CheckedIn c -> CheckedIn { c with Contact = contact }

Это работает , но это не похоже на правильный подход, поскольку это приводит к дублированию определений типов, когда я все еще хочу использовать тип PendingCustomer также для других функций.

Как начинающий F #, у меня есть чувство что я скучаю по крошечной простой вещи здесь.

Ответы [ 3 ]

2 голосов
/ 15 марта 2020

Я думаю, что вы можете упростить (например, извлечь общее состояние) и сделать ваши случаи более понятными, это должно облегчить решение этой проблемы.

type CustomerId = CustomerId of Guid
type ContactInformation = ContactInformation of string
type AcceptDate = AcceptDate of DateTime
type CheckInDate = CheckInDate of DateTime

type CheckedInCustomer =
    private
        { Contact: ContactInformation
          AcceptDate: AcceptDate
          CheckInDate: CheckInDate }    

type CustomerState =
    private
        | New
        | ContactOnly of ContactInformation
        | AcceptedGdpr of AcceptDate
        | ContactAndGdpr of ContactInformation * AcceptDate
        | CheckedIn of CheckedInCustomer

type Customer =
    private
        { Id: CustomerId
          State: CustomerState }

let updateContact (customer: Customer) contact =
    match customer.State with
    | New -> { customer with State = ContactOnly contact }
    | ContactOnly _ -> { customer with State = ContactOnly contact }
    | AcceptedGdpr acceptDate -> { customer with State = ContactAndGdpr(contact, acceptDate) }
    | ContactAndGdpr (_,acceptDate) -> { customer with State = ContactAndGdpr(contact, acceptDate) }
    | CheckedIn checkedIn -> { customer with State = CheckedIn { checkedIn with Contact = contact } }

Что-то, что вы также можете проверить библиотеки, такие как FSharp.Validationblocks для упрощения работы с проверкой примитивного типа.

1 голос
/ 15 марта 2020

Я симпатизирую c идее пытаться использовать типы, чтобы избежать незаконных состояний, особенно когда речь идет о таких важных вещах, как согласие GDPR / договорные соглашения.

После обсуждения немного в комментариях следует updateContact обновить контактную информацию Customer?

let updateContact (customer: Customer) (contact : ContactInformation) : Customer =
    match customer with
    | New c -> ContactOnly { Id = c.Id; Contact = contact } |> Pending
    | Pending pending ->
        match pending with
        | ContactOnly c -> ContactOnly { c with Contact = contact } |> Pending
        | AcceptedGdpr c -> AcceptedGdpr { c with Contact = contact } |> Pending
    | CheckedIn c -> CheckedIn { c with Contact = contact }

В исходном коде updateContact вернуть контактную информацию, но не обновленного клиента, что приведет к проблемам с поиском типа выражения, подходящего для всех ветвей. Здесь все ветви дают Customer, избегая проблемы.

0 голосов
/ 15 марта 2020

Не вижу веской причины иметь такую ​​сложную модель. Это не значит, что его нет, но, насколько вы объяснили в вопросе, похоже, что он удовлетворяет всем вашим ограничениям, но при этом значительно проще:

type Customer =
  { Id: CustomerId
  , Contact: ContactInformation option
  , AcceptedGDPR: DateTime option
  , CheckedIn: DateTime option
  }

let updateDetails (customer: Customer) contact =
  { customer with Contact = contact }
...