【问题标题】:Serializing F# discriminated unions with protobuf使用 protobuf 序列化 F# 可区分联合
【发布时间】:2014-09-13 02:55:59
【问题描述】:

有没有办法让 protobuf 序列化/反序列化 F# 的可区分联合?

我正在尝试使用 protobuf 序列化消息。消息是 F# 记录和区分联合。

序列化似乎可以很好地处理记录,但我无法让它与受歧视的工会一起使用。

在下面的代码中,测试 testMessageA 和 testMessageB 是绿色的。测试testMessageDU是红色的。

module ProtoBufSerialization

open FsUnit
open NUnit.Framework

open ProtoBuf

type MessageA = {
  X: string;
  Y: int;
}

type MessageB = {
  A: string;
  B: string;
}

type Message =
| MessageA of MessageA
| MessageB of MessageB

let serialize msg =
  use ms = new System.IO.MemoryStream()
  Serializer.SerializeWithLengthPrefix(ms, msg, PrefixStyle.Fixed32)
  ms.ToArray()

let deserialize<'TMessage> bytes =
  use ms = new System.IO.MemoryStream(buffer=bytes)
  Serializer.DeserializeWithLengthPrefix<'TMessage>(ms, PrefixStyle.Fixed32)

[<Test>]
let testMessageA() =
  let msg = {X="foo"; Y=32}
  msg |> serialize |> deserialize<MessageA> |> should equal msg

[<Test>]
let testMessageB() =
  let msg = {A="bar"; B="baz"}
  msg |> serialize |> deserialize<MessageB> |> should equal msg

[<Test>]
let testMessageDU() =
  let msg = MessageA {X="foo"; Y=32}
  msg |> serialize |> deserialize<Message> |> should equal msg

我尝试在 Message 类型上添加不同的属性,例如 ProtoInclude 和 KnownType,在 MessageA 和 MessageB 类型上添加 CLIMutable,...但似乎没有任何帮助。

我宁愿不必将我的 DU 映射到类以使序列化工作......

【问题讨论】:

  • 我真的不知道 F# 是如何表示这些的,就 protobuf-net 在运行时会看到什么而言。如果这里(在库级别)有明显的“修复”,我会很乐意看看 - 但我需要深入了解 F# 在这里做了什么。
  • Afaik F#(总是?)为有区别的联合创建类层次结构。这就是为什么我希望我可以使用 ProtoInclude 属性。现在在反编译器中查看了它,看起来可能需要在 lib 中进行调整才能使其正常工作.. :( 除非有人想出一种不太明显的方法来使其工作。我在这里上传了反编译的代码:codepad.org/DB0CQU4K

标签: f# protobuf-net


【解决方案1】:

我已经使用了您非常有用的生成输出,它看起来基本上一切正常 - 除了 Message.MessageA 子类型。这些非常接近工作 - 它们与“自动元组”代码(匹配所有成员的构造函数)基本相同,除了自动元组当前不适用于子类型。

认为应该可以通过扩展自动元组代码以在这种情况下工作来调整代码以自动工作(我正在尝试考虑任何可能的不良副作用的,但我没有看到任何)。我没有具体的时间框架,因为我需要平衡多个项目和全职日常工作、家庭和志愿者工作等之间的时间。

在短期内,以下 C# 足以使其工作,但我不认为这是一个有吸引力的选择:

RuntimeTypeModel.Default[typeof(Message).GetNestedType("MessageA")]
                .Add("item").UseConstructor = false;
RuntimeTypeModel.Default[typeof(Message).GetNestedType("MessageB")]
                .Add("item").UseConstructor = false;

顺便说一句,这里的属性没有帮助,应该避免:

| [<ProtoMember(1)>] MessageA of MessageA
| [<ProtoMember(2)>] MessageB of MessageB

如果他们做了什么,他们就是在复制&lt;ProtoInclude(n)&gt; 的意图。如果在此处指定它们更方便,那可能会很有趣。但我发现真正有趣的是,F# 编译器完全忽略了AttributeUsageAttribute,对于[ProtoMember],它是:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,
    AllowMultiple = false, Inherited = true)]
public class ProtoMemberAttribute {...}

是的,F# 编译器显然(非法地)将其卡在了一个方法上:

[ProtoMember(1)]
[CompilationMapping(SourceConstructFlags.UnionCase, 0)]
public static ProtoBufTests.Message NewMessageA(ProtoBufTests.MessageA item)

顽皮的 F# 编译器!

【讨论】:

  • 好吧,是的,编译器似乎没有检查 AttributeUsage 条目,但没有将属性粘贴在那里.... stmax 在这里:type Message = | [&lt;ProtoMember(1)&gt;] MessageA of MessageA ;)
  • @CarstenKönig 哦,我意识到了;我只是认为 F# 编译器应该至少发出一个警告(最好是一个错误,尽管这可能不可行)
  • 我让它与 RuntimeTypeModel 一起工作 - 虽然“Item”需要是小写的“item”,否则反序列化时它将为空。我认为原因是 Item 只有一个 getter 而没有 setter,而 item 是一个可以读写的字段。感谢您的解决方法!有朝一日在库中拥有对 F#/DU 的本机支持会很棒。这是所有测试为绿色的最终代码:codepad.org/rZgV6HOQ
  • +1 感谢@stmax 的代码转储。见 riff 和我的结论:stackoverflow.com/a/25206246/11635
【解决方案2】:

我用protobuf-net 加了event sourcing DUs,非常感谢json.net v6's seamless support for DUs

我最初放弃优先使用 protobuf-net 的原因是:

  1. 我从来没有证明我正在寻找的性能差距
  2. 我希望在我的消息合同中对字段重命名(依赖于通过[&lt;ProtoMember(n)&gt;] 进行寻址)具有弹性的愿望通过以下组合得到缓解:

    • 字段名称别名(即使用属性告诉 F# 以旧名称编译)
    • 通过在同一 DU 中添加 EventXXXV2EventXxx 来使用 DU 模式匹配到版本事件的优势

我没有找到比以下更清洁的方法:

let registerSerializableDuInModel<'TMessage> (model:RuntimeTypeModel) =
    let baseType = model.[typeof<'TMessage>]
    for case in typeof<'TMessage> |> FSharpType.GetUnionCases do
        let caseType = case.Name |> case.DeclaringType.GetNestedType 
        baseType.AddSubType(1000 + case.Tag, caseType) |> ignore
        let caseTypeModel = model.[caseType]
        caseTypeModel.Add("item").UseConstructor <- false
    baseType.CompileInPlace()

let registerSerializableDu<'TMessage> () = registerSerializableDuInModel<'TMessage> RuntimeTypeModel.Default

registerSerializableDu<Message> ()

解决对[&lt;ProtoInclude(100, "ProtoBufTests+Message+MessageA")&gt;] cruft 的需求。 (我仍在思考 F# 和 protbuf-net 改进的哪种组合最能解决这个问题)

一个非常重要的区别是不需要[&lt;ProtoContract; CLIMutable&gt;] 洒水(除了ProtoIncludeProtoMember 之外)。

代码转储:

module FunDomain.Tests.ProtobufNetSerialization

open ProtoBuf
open ProtoBuf.Meta

open Swensen.Unquote
open Xunit

open System.IO
open Microsoft.FSharp.Reflection

[<ProtoContract; CLIMutable>]
type MessageA = {
    [<ProtoMember(1)>] X: string;
    [<ProtoMember(2)>] Y: int option;
}

[<ProtoContract>]
[<CLIMutable>]
type MessageB = {
    [<ProtoMember(1)>] A: string;
    [<ProtoMember(2)>] B: string;
}

[<ProtoContract>]
type Message =
    | MessageA of MessageA
    | MessageB of MessageB

let serialize msg =
    use ms = new MemoryStream()
    Serializer.SerializeWithLengthPrefix(ms, msg, PrefixStyle.Fixed32)
    ms.ToArray()

let deserialize<'TMessage> bytes =
    use ms = new MemoryStream(buffer=bytes)
    Serializer.DeserializeWithLengthPrefix<'TMessage>(ms, PrefixStyle.Fixed32)

let registerSerializableDuInModel<'TMessage> (model:RuntimeTypeModel) =
    let baseType = model.[typeof<'TMessage>]
    for case in typeof<'TMessage> |> FSharpType.GetUnionCases do
        let caseType = case.Name |> case.DeclaringType.GetNestedType 
        baseType.AddSubType(1000 + case.Tag, caseType) |> ignore
        let caseTypeModel = model.[caseType]
        caseTypeModel.Add("item").UseConstructor <- false
    baseType.CompileInPlace()

let registerSerializableDu<'TMessage> () = registerSerializableDuInModel<'TMessage> RuntimeTypeModel.Default

registerSerializableDu<Message> ()

let [<Fact>] ``MessageA roundtrips with null`` () =
    let msg = {X=null; Y=None}
    let result = serialize msg
    test <@ msg = deserialize result @>

let [<Fact>] ``MessageA roundtrips with Empty`` () =
    let msg = {X=""; Y=None}
    let result = serialize msg
    test <@ msg = deserialize result @>

let [<Fact>] ``MessageA roundtrips with Some`` () =
    let msg = {X="foo"; Y=Some 32}
    let result = serialize msg
    test <@ msg = deserialize result @>

let [<Fact>] ``MessageA roundtrips with None`` () =
    let msg = {X="foo"; Y=None}
    let result = serialize msg
    test <@ msg = deserialize result @>

let [<Fact>] ``MessageB roundtrips`` () =
    let msg = {A="bar"; B="baz"}
    let result = serialize msg
    test <@ msg = deserialize result @>

let [<Fact>] ``roundtrip pair``() =
    let msg1 = MessageA {X="foo"; Y=Some 32}
    let msg1' = msg1 |> serialize |> deserialize
    test <@ msg1' = msg1 @>

    let msg2 = MessageB {A="bar"; B="baz"}     
    let msg2' = msg2 |> serialize |> deserialize
    test <@ msg2' = msg2 @>

let [<Fact>] many() =
    for _ in 1..1000 do
        ``roundtrip pair``()      

【讨论】:

  • 将编辑更多内容,但在“无缝”支持中需要注意的一件事是元组的字段名称在示例中未序列化。不确定这是否也适用于记录,但我的主要观点是不要盲目地使用 OOTB 支持(不会改变我的一般原则 contextless processing is important
【解决方案3】:

我最终做的是这样的

    let typeModel = TypeModel.Create()
    let resultType = typedefof<Result>
    let resultNestedTypes = resultType.GetNestedTypes() |> Array.filter (fun x -> x.Name <> "Tags")
    for nestedType in resultNestedTypes do 
        let model = typeModel.Add( nestedType, true )
        model.UseConstructor <- false
        nestedType.GetFields( BindingFlags.NonPublic ||| BindingFlags.Instance ||| BindingFlags.GetField ) |> Array.map (fun x -> x.Name ) |> Array.sort |> model.Add |> ignore

        types.[ nestedType.Name ] <- nestedType

在我的例子中,types 是应用程序启动时构建的联合类型的字典。我需要在序列化数据之前将名称保存在消息中,以便以后加载。

只要只添加新字段,这将起作用,因为每个字段都变为item1。如果需要删除字段,我认为它可以很容易地扩展为从字段名称中获取字段顺序号

type Result = 
    | Success of Item1: string * Item3:bool
    | Failure of string

然后提取项目之后的数字,或者任何最有效的方法。有很多方法。

【讨论】:

    猜你喜欢
    • 2018-04-25
    • 1970-01-01
    • 1970-01-01
    • 2011-11-19
    • 2020-10-04
    • 1970-01-01
    • 1970-01-01
    • 2017-12-27
    • 2014-09-15
    相关资源
    最近更新 更多