模式匹配在被歧视的联盟的一个普通容器上

Mar*_*ann 13 generics f# pattern-matching

我有这个通用的值容器:

open System

type Envelope<'a> = {
    Id : Guid
    ConversationId : Guid
    Created : DateTimeOffset
    Item : 'a }
Run Code Online (Sandbox Code Playgroud)

我想能够使用模式匹配Item,同时仍保留包络值.

理想情况下,我希望能够做到这样的事情:

let format x =
    match x with
    | Envelope (CaseA x) -> // x would be Envelope<RecA>
    | Envelope (CaseB x) -> // x would be Envelope<RecB>
Run Code Online (Sandbox Code Playgroud)

但是,这不起作用,所以我想知道是否有办法做这样的事情?

更多详情

假设我有这些类型:

type RecA = { Text : string; Number : int }
type RecB = { Text : string; Version : Version }

type MyDU = | CaseA of RecA | CaseB of RecB
Run Code Online (Sandbox Code Playgroud)

我希望能够声明类型的值,Envelope<MyDU>并仍然能够匹配包含Item.

也许这是在错误的切线上,但我首先尝试使用信封的映射函数:

let mapEnvelope f x =
    let y = f x.Item
    { Id = x.Id; ConversationId = x.ConversationId; Created = x.Created; Item = y }
Run Code Online (Sandbox Code Playgroud)

这个函数有签名('a -> 'b) -> Envelope<'a> -> Envelope<'b>,所以看起来像我们以前见过的东西.

这使我能够定义此部分活动模式:

let (|Envelope|_|) (|ItemPattern|_|) x =
    match x.Item with
    | ItemPattern y -> x |> mapEnvelope (fun _ -> y) |> Some
    | _ -> None
Run Code Online (Sandbox Code Playgroud)

和这些辅助部分活动模式:

let (|CaseA|_|) = function | CaseA x -> x |> Some | _ -> None
let (|CaseB|_|) = function | CaseB x -> x |> Some | _ -> None
Run Code Online (Sandbox Code Playgroud)

使用这些构建块,我可以编写如下函数:

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
    | _ -> ""
Run Code Online (Sandbox Code Playgroud)

请注意,在第一种情况下,x是一个Envelope<RecA>,您可以看到,因为它可以读取值x.Item.Number.同样,在第二种情况下,xEnvelope<RecB>.

另请注意,每个案例都需要x.Id从信封中访问,这就是为什么我不能仅仅匹配x.Item开始的原因.

这有效,但有以下缺点:

  • 我需要定义一个部分活动模式(|CaseA|_|),以便分解MyDUCaseA,即使已经有一个内置的模式.
  • 即使我有一个Discriminated Union,编译器也无法告诉我是否忘记了一个案例,因为每个模式都是部分活动模式.

有没有更好的办法?

Gus*_*Gus 3

这似乎有效:

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
Run Code Online (Sandbox Code Playgroud)

可能我没有完全理解你的问题,但如果你最终需要用 an 调用一个函数,Envelope< RecA> 你可以,因为这就是v包含的内容。

更新

以下是了解这也是您的第一次尝试后的一些想法。

理想情况下,您可以使用如下所示的记录语法:

let v = {x with Item = r}
Run Code Online (Sandbox Code Playgroud)

不幸的是它无法编译,因为通用参数是不同的类型。

但是,您可以使用命名参数来模拟此表达式,并且使用重载可以使编译器决定最终类型:

#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
Run Code Online (Sandbox Code Playgroud)

现在您可以使用类似的语法进行克隆并最终更改泛型类型

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 })
Run Code Online (Sandbox Code Playgroud)

那么你的匹配可以这样写:

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
Run Code Online (Sandbox Code Playgroud)

当然,您必须提及CloneWith方法中的每个字段(在本例中为两次)。但在调用站点,语法更好。可能有一些解决方案没有提及涉及反射的所有领域。

  • 好的,我们得到了相同的解决方案。不,我认为没有更好的方法来实现这一点,因为克隆记录将无法工作,因为它们具有不同的通用类型,尽管使用反射可能是一些技巧。当然,除了映射之外,您还可以定义一个专门进行重写的函数。 (2认同)