I have this generic container of values:
open System
type Envelope<'a> = {
Id : Guid
ConversationId : Guid
Created : DateTimeOffset
Item : 'a }
I would like to be able to use Pattern Matching on the Item
, while still retaining the envelope values.
Ideally, I would like to be able to do something like this:
let format x =
match x with
| Envelope (CaseA x) -> // x would be Envelope<RecA>
| Envelope (CaseB x) -> // x would be Envelope<RecB>
However, this doesn't work, so I wonder if there's a way to do something like this?
Further details
Assume that I have these types:
type RecA = { Text : string; Number : int }
type RecB = { Text : string; Version : Version }
type MyDU = | CaseA of RecA | CaseB of RecB
I would like to be able to declare values of the type Envelope<MyDU>
and still be able to match on the contained Item
.
Perhaps this is going off on the wrong tangent, but I first attempted with a mapping function for envelopes:
let mapEnvelope f x =
let y = f x.Item
{ Id = x.Id; ConversationId = x.ConversationId; Created = x.Created; Item = y }
This function has the signature ('a -> 'b) -> Envelope<'a> -> Envelope<'b>
, so that looks like something we've seen before.
This enables me to define this Partial Active Pattern:
let (|Envelope|_|) (|ItemPattern|_|) x =
match x.Item with
| ItemPattern y -> x |> mapEnvelope (fun _ -> y) |> Some
| _ -> None
and these auxiliary Partial Active Patterns:
let (|CaseA|_|) = function | CaseA x -> x |> Some | _ -> None
let (|CaseB|_|) = function | CaseB x -> x |> Some | _ -> None
With these building blocks, I can write a function like this one:
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
| _ -> ""
Notice that in the first case, x
is an Envelope<RecA>
, which you can see because it's possible to read the value off x.Item.Number
. Similarly, in the second case, x
is Envelope<RecB>
.
Also notice that each case requires access to x.Id
from the envelope, which is the reason why I can't just match on x.Item
to begin with.
This works, but has the following drawbacks:
(|CaseA|_|)
in order to decompose MyDU
to CaseA
, even though there's already a built-in pattern for that.Is there a better way?
This seems to be working:
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
May be I didn't fully understand your question, but if you need in the end to call a function with an Envelope< RecA>
you can since that's what v
contains.
UPDATE
Here are some thoughts after understanding this was also your first attempt.
Ideally you would be able to use record syntax like this:
let v = {x with Item = r}
unfortunately it wont compile, because the generic parameter is of a different Type.
However you can mimic this expressions with named arguments, and playing with overloads you can make the compiler to decide the final type:
#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
Now you can clone with a similar syntax and eventually change the generic type
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 })
Then your match could be written like this:
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
Of course you have to mention each field in the CloneWith
method (in this case twice). But at the calling site the syntax is nicer.
There might be solutions not mentioning all fields involving reflection.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With