【问题标题】:Pattern Matching on a generic container of a Discriminated Union可区分联合的通用容器上的模式匹配
【发布时间】:2014-05-07 14:52:21
【问题描述】:

我有这个通用的值容器:

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&lt;MyDU&gt; 类型的值,并且仍然能够匹配包含的 Item

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

let mapEnvelope f x =
    let y = f x.Item
    { Id = x.Id; ConversationId = x.ConversationId; Created = x.Created; Item = y }

这个函数有签名('a -&gt; 'b) -&gt; Envelope&lt;'a&gt; -&gt; Envelope&lt;'b&gt;,所以它看起来像我们以前见过的东西。

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

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&lt;RecA&gt;,您可以看到它,因为可以从 x.Item.Number 中读取值。同样,在第二种情况下,xEnvelope&lt;RecB&gt;

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

这可行,但有以下缺点:

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

有没有更好的办法?

【问题讨论】:

  • 这有什么问题:让格式 x = 匹配 x.Item 与 | CaseA r -> sprintf "%O: %s: %O" x.Id r.Text r.Number | CaseB r -> sprintf "%O: %s: %O" x.Id r.Text r.Version
  • 这相当于匹配x.Item,因为它导致r 成为RecA 值,而不是Envelope&lt;RecA&gt; 值。在上面的示例中,这并不重要,因为正如您所建议的,我仍然可以访问x。但是,我真正需要做的是调用一个需要Envelope&lt;RecA&gt; 值的函数。
  • 我更新了我的问题,以更好地说明为什么我需要映射信封,而不是分解它。

标签: generics f# pattern-matching


【解决方案1】:

重新表述您的问题,如果我理解正确,您想打开信封的内容,同时仍然可以访问信封的标题

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

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

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}

【讨论】:

    【解决方案2】:

    这似乎有效:

    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&lt; RecA&gt; 调用函数,您可以使用 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 方法中提及每个字段(在本例中是两次)。但在调用站点,语法更好。 可能存在未提及所有涉及反射的领域的解决方案。

    【讨论】:

    • 是的,这行得通。这实际上是我的原始代码 :) 我刚刚发现 mapEnvelope (fun _ -&gt; r) 部分重复,并想知道是否有 DRYer 方法可以做到这一点。
    • 好的,我们达成了相同的解决方案。不,我认为没有更好的方法来实现这一点,因为克隆记录将不起作用,因为它们具有不同的泛型类型,尽管使用反射可能是一些技巧。当然,您可以定义一个专门用于重写的函数,而不是 map。
    • 虽然有很多好的建议,但我选择这个作为答案,因为更新之前的建议是我已经拥有的,并且是迄今为止最简单的解决方案。我问了这个问题,看看是否有一个优雅的解决方案,但是虽然这是主观的,但似乎没有。鉴于此,我将选择最简单的方法,而这个答案的第一部分就是最好的。
    【解决方案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 教我一些我不知道的语法。但是,尽管我无法解释原因,但它给了我这个编译器警告:“警告 FS0025:此表达式上的模式匹配不完整。”
    • 这很奇怪。我不理解。你的 DU 有两个以上的案例吗?
    • 不,它没有......我也无法解释......我可能做错了什么......
    • 是的,您不能将类型投影为模式匹配的一部分,这本来会很好。我相信 Erlang 允许你这样做,也许应该是用户语音请求。 :)
    • 显然分解是你想要的地图的第一阶段,但我不能说我明白你为什么需要这个。
    【解决方案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&lt;RecA&gt;Envelope&lt;RecB&gt; 的函数(这不是很简单)。

    编辑

    事实证明这很容易:

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

    【讨论】:

    • +1 以获得一些好的建议,但 (|UnionC|UnionD|) 中的匹配表达式给了我这个编译器警告,我实际上无法解释:“警告 FS0025:此表达式上的模式匹配不完整。”跨度>
    • 另一个问题是它迫使我为我拥有的可区分联合的每个信封定义一个活动模式......虽然我没有那么多,但我希望有一个通用的-目的解决方案。
    • @MarkSeemann - 我在 VS2012 上没有收到任何编译器警告(也许你有一些旧定义?)。不过,我看不到避免活动模式的方法。
    • 对不起,我的错误。是的,我确实有一个旧定义。
    猜你喜欢
    • 1970-01-01
    • 2018-02-03
    • 2019-01-10
    • 1970-01-01
    • 1970-01-01
    • 2017-04-10
    • 2019-06-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多