提供类型的模式匹配

Pattern matching on provided types

首先,获取模式并解析:

type desc = JsonProvider< """[{"name": "", "age": 1}]""", InferTypesFromValues=true >
let json = """[{"name": "Kitten", "age": 322}]"""
let typedJson = desc.Parse(json)

现在我们可以访问 typedJson.[0] .Age 和 .Name 属性,但是,我想在编译时对它们进行模式匹配,以便在架构更改时出现错误。

由于这些属性已被删除,我们无法在 运行 时获得它们:

let ``returns false``() = 
  typedJson.[0].GetType()
    .FindMembers(MemberTypes.All, BindingFlags.Public ||| BindingFlags.Instance, 
                 MemberFilter(fun _ _ -> true), null) 
  |> Array.exists (fun m -> m.ToString().Contains("Age"))

...我使用活动模式制作了一个运行时间检查版本:

let (|Name|Age|) k = 
  let toID = NameUtils.uniqueGenerator NameUtils.nicePascalName
  let idk = toID k
  match idk with
  | _ when idk.Equals("Age") -> Age
  | _ when idk.Equals("Name") -> Name
  | ex_val -> failwith (sprintf "\"%s\" shouldn't even compile!" ex_val)

typedJson.[0].JsonValue.Properties()
|> Array.map (fun (k, v) -> 
     match k with
     | Age -> v.AsInteger().ToString() // ...
     | Name -> v.AsString()) // ... 
|> Array.iter (printfn "%A")

理论上,如果 FSharp.Data 不是 OS,我将无法实施 toID。一般来说,整个方法似乎是错误的,重做。

我知道无法使用类型提供程序生成可区分的联合,但也许有更好的方法在编译时进行所有这些检查?

如果你使用 InferTypesFromValues=false,你会得到一个强类型:

type desc = JsonProvider< """[{"name": "", "age": 1}]""", InferTypesFromValues=false >

您可以使用 desc 类型在您关心的属性上定义活动模式:

let (|Name|_|) target (candidate : desc.Root) =
    if candidate.Name = target then Some target else None

let (|Age|_|) target (candidate : desc.Root) =
    if candidate.Age = target then Some target else None

这些活动模式可以这样使用:

let json = """[{"name": "Kitten", "age": 322}]"""
let typedJson = desc.Parse(json)

match typedJson.[0] with
| Name "Kitten" n -> printfn "Name is %s" n
| Age 322m a -> printfn "Age is %M" a
| _ -> printfn "Nothing matched"

给定此处的 typedJson 值,该匹配将打印出 "Name is Kitten"。

据我所知,无法在编译时使用给定的 TP 找出是否 "Json schema has changed"。

这就是为什么:

  • JsonProvider<sample> 正是在编译时启动的,它提供了一种在 运行 时操作 Json 内容的类型。这个提供的擦除类型有几个 运行 时间的静态方法,适用于任何 sample 和类型 Root 使用少量实例属性扩展 IJsonDocument,包括基于编译时提供的示例的实例属性(在您的情况下 - 属性 NameAge)。只有一个 非常宽松的隐式 Json "schema" 在 JsonProvider 提供的类型后面,没有另一个这样的实体可以在编译时比较变化;
  • 在 运行 时仅提供类型 desc 及其静态方法及其 Root 类型及其对应的实例方法 随时为您处理任意 Json 内容。所有这些爵士乐对于 "Json schema",在你给定的情况下,只要 运行 时间 Json 内容代表一个数组,它的元素几乎可以是任何元素。 例如,

    type desc = JsonProvider<"""[{"name": "", "age": 1}]"""> // @ compile-time
    
    let ``kinda "typed" json`` = desc.Parse("""[]""") // @ run-time
    let ``another kinda "typed" json`` =
        desc.Parse("""[{"contents":"whatever", "text":"blah-blah-blah"},[{"extra":42}]]""")
    

    两者都将在 运行 时被愉快地解析为 "typed Json" 符合 TP 从给定的 sample 派生的 "schema",尽管显然 NameAge 丢失,如果访问将引发异常。

  • 建立另一个依赖正式 Json schema 的 Json TP 是可行的。 它可以在类型创建时从模式存储库中使用对给定模式的引用,并允许操作元素 仅通过在编译时从模式派生的提供的访问器来解析 Json 有效负载。

    在这种情况下改变 如果代码中使用的访问器与更改不兼容,则引用模式可能会破坏编译。 这种安排伴随 运行-time Json 有效载荷验证器或验证解析器可以提供可靠的企业质量 Json 架构变更管理。

    JsonProvider 来自 Fsharp.Data 的 TP 缺乏这种 Json 模式处理能力,因此有效负载验证只能在 运行 时间内完成。

    引用您的评论可以更好地解释您要实现的目标:

    Thank you! But what I'm trying to achieve is to get a compiler error if I add a new field e.g. Color to the json schema and then ignore it while later processing. In case of unions it would be instant FS0025.

    和:

    yes, I must process all fields, so I can't rely on _. I want it so when the schema changes, my F# program won't compile without adding necessary handling functionality(and not just ignoring new field or crashing at runtime).

    最简单的解决方案是构造一个 "test" 对象。

    提供的类型带有两个构造函数:一个接受 JSonValue 并对其进行解析 - 实际上与 JsonValue.Parse 相同 - 而另一个 需要 每个字段都被填充英寸

    这是我们感兴趣的。

    我们还将使用命名参数调用它,这样我们不仅在添加或删除字段时是安全的,而且在重命名或删除字段时也是安全的改变了。

    type desc = JsonProvider< """[{"name": "SomeName", "age": 1}]""", InferTypesFromValues=true >
    
    let TestObjectPleaseIgnore = new desc.Root (name = "Kitten", age = 322)
    // compiles
    

    (请注意,我将示例中 name 的值更改为 "SomeName",因为 "" 被推断为通用 JsonValue。)

    现在,如果类型提供者使用的示例中突然出现更多字段,构造函数将变得不完整,无法编译。

    type desc = JsonProvider< """[{"name": "SomeName", "age": 1, "color" : "Red"}]""", InferTypesFromValues=true >
    
    let TestObjectPleaseIgnore = new desc.Root (name = "Kitten", age = 322)
    // compilation error: The member or object constructor 'Root' taking 1 arguments are not accessible from this code location. All accessible versions of method 'Root' take 1 arguments.
    

    显然,错误指的是 1 参数构造函数,因为这是它试图适合的构造函数,但您会看到现在提供的类型有一个 3 参数构造函数替换了 2 参数构造函数。

    <tldr>
    Build some parser to handle your issues. http://www.quanttec.com/fparsec/
    </tldr>
    

    所以...

    你想要一些可以读取内容并用它做一些事情的东西。不知道这些东西是什么。

    祝你好运。

    希望类型提供者为您做这件事。类型提供者的全部目的是 "at compile time this is what I saw, and thats what I will use".

    话虽如此:

    您需要一些其他类型的解析器,您可以在其中检查 "schema"(您知道将要出现的内容或您上次看到的内容与实际出现的内容的一些定义)。实际上,某些动态解析器​​将数据获取到具有动态类型的某些动态结构。

    请注意,动态不是静态的。 F# 有静态类型,很多都是基于它的。类型提供者更是如此。战斗会让你头疼。这当然是可能的,甚至有可能通过与类型提供者对抗来实际使用这种方法,但它又不是真正的类型提供者,也不是它的目的。