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

时间:2014-03-28 08:41:14

标签: generics f# pattern-matching

我有这个通用的值容器:

open System

type Envelope<'a> = {
    Id : Guid
    ConversationId : Guid
    Created : DateTimeOffset
    Item : 'a }

我希望能够在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
    | _ -> ""

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

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

这有效,但有以下缺点:

  • 我需要定义像(|CaseA|_|)这样的部分活动模式,以便将MyDU分解为CaseA,即使已经存在内置模式。< / LI>
  • 即使我有一个Discriminated Union,编译器也无法告诉我我是否忘记了一个案例,因为每个模式都是部分活动模式。

有更好的方法吗?

4 个答案:

答案 0 :(得分:3)

这会做你想要的吗?

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。

答案 1 :(得分: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

可能我没有完全理解你的问题,但是如果你最后需要用Envelope< RecA> 来调用函数,那么你可以v包含这个函数。

<强>更新

在理解这也是你的第一次尝试后,有些想法。

理想情况下,您可以使用这样的记录语法:

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 :(得分:3)

要重新解释您的问题,如果我理解正确,您想要打开信封的内容,同时仍然可以访问信封的标题吗? / p>

在这种情况下,为什么不提取内容,然后将内容和标题作为一对传递?

创建对的辅助函数可能如下所示:

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 

BTW我会稍微简单地写一下mapEnvelope:)

let mapEnvelope f envelope =
    {envelope with Item = f envelope.Item}

答案 3 :(得分:2)

首先这有点令人困惑,但这里有一个更简单的版本(由于部分匹配,这显然不完美,但我正在努力改进它):

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