Согласование образцов на общем контейнере Дискриминационного союза
У меня есть этот общий контейнер значений:
open System
type Envelope<'a> = {
Id : Guid
ConversationId : Guid
Created : DateTimeOffset
Item : 'a }
Я хотел бы иметь возможность использовать Match Matching в Item
, сохраняя при этом значения огибающей.
В идеале я хотел бы сделать что-то вроде этого:
let format x =
match x with
| Envelope (CaseA x) -> // x would be Envelope<RecA>
| Envelope (CaseB x) -> // x would be Envelope<RecB>
Однако это не работает, поэтому я задаюсь вопросом, есть ли способ сделать что-то вроде этого?
Дополнительная информация
Предположим, что у меня есть следующие типы:
type RecA = { Text : string; Number : int }
type RecB = { Text : string; Version : Version }
type MyDU = | CaseA of RecA | CaseB of RecB
Я хотел бы иметь возможность объявлять значения типа Envelope<MyDU>
и по-прежнему иметь возможность совпадения с содержащимся Item
.
Возможно, это происходит по неверному касанию, но я сначала попытался использовать функцию отображения для конвертов:
let mapEnvelope f x =
let y = f x.Item
{ Id = x.Id; ConversationId = x.ConversationId; Created = x.Created; Item = y }
Эта функция имеет подпись ('a -> 'b) -> Envelope<'a> -> Envelope<'b>
, поэтому она выглядит как-то, что мы видели раньше.
Это позволяет мне определить этот Частичный активный шаблон:
let (|Envelope|_|) (|ItemPattern|_|) x =
match x.Item with
| ItemPattern y -> x |> mapEnvelope (fun _ -> y) |> Some
| _ -> None
и эти вспомогательные частичные активные шаблоны:
let (|CaseA|_|) = function | CaseA x -> x |> Some | _ -> None
let (|CaseB|_|) = function | CaseB x -> x |> Some | _ -> None
С этими строительными блоками я могу написать такую функцию, как этот:
let formatA (x : Envelope<RecA>) = sprintf "%O: %s: %O" x.Id x.Item.Text x.Item.Number
let formatB (x : Envelope<RecB>) = sprintf "%O: %s: %O" x.Id x.Item.Text x.Item.Version
let format x =
match x with
| Envelope (|CaseA|_|) y -> y |> formatA
| Envelope (|CaseB|_|) y -> y |> formatB
| _ -> ""
Обратите внимание, что в первом случае x
является Envelope<RecA>
, который вы можете видеть, потому что можно прочитать значение off x.Item.Number
. Аналогично, во втором случае x
есть Envelope<RecB>
.
Также обратите внимание, что для каждого случая требуется доступ к x.Id
из конверта, поэтому я не могу просто совместить с x.Item
для начала.
Это работает, но имеет следующие недостатки:
- Мне нужно определить Partial Active Pattern, например
(|CaseA|_|)
, чтобы разложить MyDU
на CaseA
, хотя для этого уже есть встроенный шаблон.
- Несмотря на то, что у меня есть дискриминационный союз, компилятор не может сказать мне, забыл ли я случай, потому что каждый из шаблонов - это частичные активные шаблоны.
Есть ли лучший способ?
Ответы
Ответ 1
Кажется, что это работает:
let format x =
match x.Item with
| CaseA r ->
let v = mapEnvelope (fun _ -> r) x
sprintf "%O: %s: %O" v.Id v.Item.Text v.Item.Number
| CaseB r ->
let v = mapEnvelope (fun _ -> r) x
sprintf "%O: %s: %O" v.Id v.Item.Text v.Item.Version
Возможно, я не полностью понял ваш вопрос, но если вам нужно в конце концов вызвать функцию с Envelope< RecA>
, вы можете с тех пор, что v
содержит.
UPDATE
Вот некоторые мысли, поняв, что это была ваша первая попытка.
В идеале вы сможете использовать синтаксис записи следующим образом:
let v = {x with Item = r}
к сожалению, он не будет компилироваться, потому что общий параметр имеет другой тип.
Однако вы можете имитировать эти выражения с именованными аргументами и играть с перегрузками, вы можете заставить компилятор решить конечный тип:
#nowarn "0049"
open System
type Envelope<'a> =
{Id :Guid; ConversationId :Guid; Created :DateTimeOffset; Item :'a}
with
member this.CloneWith(?Id, ?ConversationId, ?Created, ?Item) = {
Id = defaultArg Id this.Id
ConversationId = defaultArg ConversationId this.ConversationId
Created = defaultArg Created this.Created
Item = defaultArg Item this.Item}
member this.CloneWith(Item, ?Id, ?ConversationId, ?Created) = {
Id = defaultArg Id this.Id
ConversationId = defaultArg ConversationId this.ConversationId
Created = defaultArg Created this.Created
Item = Item}
type RecA = { Text : string; Number : int }
type RecB = { Text : string; Version : Version }
type MyDU = | CaseA of RecA | CaseB of RecB
Теперь вы можете клонировать аналогичный синтаксис и в конечном итоге изменять общий тип
let x = {
Id = Guid.NewGuid()
ConversationId = Guid.NewGuid()
Created = DateTimeOffset.Now
Item = CaseA { Text = ""; Number = 0 }}
let a = x.CloneWith(Id = Guid.NewGuid())
let b = x.CloneWith(Id = Guid.NewGuid(), Item = CaseB {Text = ""; Version = null })
let c = x.CloneWith(Id = Guid.NewGuid(), Item = {Text = ""; Version = null })
Тогда ваш матч можно записать следующим образом:
let format x =
match x.Item with
| CaseA r ->
let v = x.CloneWith(Item = r)
sprintf "%O: %s: %O" v.Id v.Item.Text v.Item.Number
| CaseB r ->
let v = x.CloneWith(Item = r)
sprintf "%O: %s: %O" v.Id v.Item.Text v.Item.Version
Конечно, вы должны указать каждое поле в методе CloneWith
(в этом случае дважды). Но на вызывающем сайте синтаксис лучше.
Могут быть решения, не упоминающие все поля, содержащие отражение.
Ответ 2
Будет ли это делать то, что вы хотите?
open System
type Envelope<'a> =
{ Id : Guid
ConversationId : Guid
Created : DateTimeOffset
Item : 'a }
type RecA = { Text : string; Number : int }
type RecB = { Text : string; Version : Version }
type MyDU = | CaseA of RecA | CaseB of RecB
let e =
{ Id = Guid.NewGuid();
ConversationId = Guid.NewGuid();
Created = DateTimeOffset.MinValue;
Item = CaseA {Text = ""; Number = 1 } }
match e with
| { Item = CaseA item } as x -> sprintf "%O: %s: %O" x.Id item.Text item.Number
| { Item = CaseB item } as x -> sprintf "%O: %s: %O" x.Id item.Text item.Version
x - это исходное значение, а "item" - это RecA или RecB.
Ответ 3
Чтобы перефразировать ваш вопрос, если я правильно понял, вы хотите включить содержимое конверта, все еще имея доступ к заголовку конверта?
В этом случае, почему бы не просто извлечь содержимое, а затем передать как содержимое, так и заголовок в виде пары?
Вспомогательная функция для создания пары может выглядеть так:
let extractContents envelope =
envelope.Item, envelope
И тогда ваш код форматирования будет изменен для обработки заголовка и содержимого:
let formatA header (contents:RecA) =
sprintf "%O: %s: %O" header.Id contents.Text contents.Number
let formatB header (contents:RecB) =
sprintf "%O: %s: %O" header.Id contents.Text contents.Version
Используя это место, вы можете использовать сопоставление шаблонов обычным способом:
let format envelope =
match (extractContents envelope) with
| CaseA recA, envA -> formatA envA recA
| CaseB recB, envB -> formatB envB recB
Здесь полный код:
open System
type Envelope<'a> = {
Id : Guid
ConversationId : Guid
Created : DateTimeOffset
Item : 'a }
type RecA = { Text : string; Number : int }
type RecB = { Text : string; Version : Version }
type MyDU = | CaseA of RecA | CaseB of RecB
let extractContents envelope =
envelope.Item, envelope
let formatA header (contents:RecA) =
sprintf "%O: %s: %O" header.Id contents.Text contents.Number
let formatB header (contents:RecB) =
sprintf "%O: %s: %O" header.Id contents.Text contents.Version
let format envelope =
match (extractContents envelope) with
| CaseA recA, envA -> formatA envA recA
| CaseB recB, envB -> formatB envB recB
Если бы я делал это много, я бы, вероятно, создал отдельный тип записи для заголовка, что сделало бы это еще проще.
let extractContents envelope =
envelope.Item, envelope.Header
Кстати, я бы написал немного mapEnvelope
:)
let mapEnvelope f envelope =
{envelope with Item = f envelope.Item}
Ответ 4
Итак, сначала это было немного запутанно, но вот более простая версия того, что у вас есть (это явно не идеально из-за частичного соответствия, но я пытаюсь его улучшить):
open System
type Envelope<'a> = {
Item : 'a }
type RecA = { Text : string; Number : int }
type RecB = { Text : string; Version : Version }
type MyDu = |A of RecA |B of RecB
let (|UnionA|_|) x =
match x.Item with
|A(a) -> Some{Item=a}
|B(b) -> None
let (|UnionB|_|) x =
match x.Item with
|A(_) -> None
|B(b) -> Some{Item=b}
let test (t:Envelope<MyDu>) =
match t with
|UnionA(t) -> () //A case - T is a recA
|UnionB(t) -> () //B case - T is a recB
Общая проблема заключается в том, что мы хотим получить функцию, которая возвращает как Envelope<RecA>
, так и Envelope<RecB>
(что не очень просто).
ИЗМЕНИТЬ
Оказывается, это на самом деле легко:
let (|UnionC|UnionD|) x =
match x.Item with
|A(a) -> UnionC({Item=a})
|B(b) -> UnionD{Item=b}
let test (t:Envelope<MyDu>) =
match t with
|UnionC(t) -> () //A case - T is a recA
|UnionD(t) -> () //B case - T is a recB;;