Согласование образцов на общем контейнере Дискриминационного союза

У меня есть этот общий контейнер значений:

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;;