【问题标题】:How can I determine the json path to a field within a record without actually hard coding the path?如何在不实际对路径进行硬编码的情况下确定记录中字段的 json 路径?
【发布时间】:2020-10-17 08:05:24
【问题描述】:

我想使用以下类型

type RecordPath<'a,'b> = {
    Get: 'a -> 'b
    Path:string
}

它的目的是定义一个getter,用于从记录类型'a'a 中的'b 类型的某个字段。它还为记录的 json 表示形式提供了该字段的路径。

例如,考虑以下字段。

type DateWithoutTimeBecauseWeirdlyDotnetDoesNotHaveThisConcept = {
    Year:uint
    Month:uint
    Day:uint
}

type Person = {
    FullName:string
    PassportNumber:string
    BirthDate:DateWithoutTimeBecauseWeirdlyDotnetDoesNotHaveThisConcept
}

type Team = {
    TeamName:string
    TeamMembers:Person list
}

RecordPath 的例子可能是

let birthYearPath = {
    Get = fun (team:Team) -> team.TeamMembers |> List.map (fun p -> p.BirthDate.Year)
    Path = "$.TeamMember[*].BirthDate.Year" //using mariadb format for json path
}

是否有某种方法可以让图书馆用户创建此记录,而实际上不需要明确指定字符串。理想情况下,用户可以通过某种强类型的方式来指定所涉及的字段。也许某种巧妙地使用反射?

我突然想到,使用支持宏的语言,这是可能的。但是可以在 F# 中完成吗?

PS:我注意到我在路径中遗漏了“TeamMembers”中的 s。为了让用户更容易使用,我想提防这种事情。

【问题讨论】:

  • 我认为这里可能有一个涉及代码引用的答案。如果我弄清楚了,我会发布它。
  • 你有一个getter,你有一个json路径,两者都“指向”同一个字段。你想用json路径做什么?
  • @scrwtp 我正在支持外键和 cosmos 和 mongo 的其他缺点的 rdms 之上制作一个 nosql 库。对于这个涉及代码引用的特定问题,我几乎有一个解决方案,我将在明天发布。
  • 好的,很酷。但是,它并没有解释您需要 json 路径来做什么。
  • json 路径使 rdms 知道如何生成可以索引的计算列。

标签: reflection f#


【解决方案1】:

正如您在 cmets 中所指出的,F# 有一个可以让您执行此操作的引用机制。您可以使用&lt;@ ... @&gt; 表示法显式创建它们,或者使用更优雅的automatic quoting mechanism 隐式创建它们。引号是 F# 代码的非常接近的表示,因此将它们转换为所需的路径格式并不容易,但我认为可以做到。

我试图让它至少对你的小例子起作用。首先,我需要一个辅助函数来对代码进行两次转换:

  • let x = e1 in e2 转换为 e2[x &lt;- e1](使用符号 e2[x &lt;- e1] 表示替代,即表达式 e2 将所有出现的 x 替换为 e1
  • e1 |&gt; fun x -&gt; e2 转为 e2[x &lt;- e1]

这就是您的示例所需的全部内容,但您可能还需要更多案例:

open Microsoft.FSharp.Quotations

let rec simplify dict e = 
  let e' = simplifyOne dict e
  if e' <> e then simplify dict e' else e'

and simplifyOne dict = function 
  | Patterns.Call(None, op, [e; Patterns.Lambda(v, body)]) 
        when op.Name = "op_PipeRight" ->
      simplify (Map.add v e dict) body 
  | Patterns.Let(v, e, body) -> simplify (Map.add v e dict) body 
  | ExprShape.ShapeVar(v) when Map.containsKey v dict -> dict.[v]
  | ExprShape.ShapeVar(v) -> Expr.Var(v)
  | ExprShape.ShapeLambda(v, e) -> Expr.Lambda(v, simplify dict e)
  | ExprShape.ShapeCombination(o, es) -> 
      ExprShape.RebuildShapeCombination(o, List.map (simplify dict) es)

通过这种预处理,我设法编写了一个像这样的extractPath 函数:

let rec extractPath var = function
  | Patterns.Call(None, op, [Patterns.Lambda(v, body); inst]) when op.Name = "Map" ->
      extractPath var inst + "[*]." + extractPath v.Name body
  | Patterns.PropertyGet(Some(Patterns.Var v), p, []) when v.Name = var -> p.Name
  | Patterns.PropertyGet(Some e, p, []) -> extractPath var e + "." + p.Name
  | e -> failwithf "Unexpected expression: %A" e

这会查找 (1) 对 map 函数的调用,(2) 对表示数据源的变量的属性访问,以及 (3) 实例具有更多属性访问的属性访问。

以下内容现在适用于您的小示例(但可能没有其他用途!)

type Path = 
  static member Make([<ReflectedDefinition(true)>] f:Expr<'T -> 'R>) = 
    match f with
    | Patterns.WithValue(f, _, Patterns.Lambda(v, body)) ->
      { Get = f :?> 'T -> 'R
        Path = "$." + extractPath v.Name (simplify Map.empty body) }
    | _ -> failwith "Unexpected argument"

Path.Make(fun (team:Team) -> team.TeamMembers |> List.map (fun p -> p.BirthDate.Year))

【讨论】:

  • 我以不同(更简单?)的方式解决了它。你能看出我的方法有什么问题吗?
【解决方案2】:

我解决这个问题的方法是

let jsonPath userExpr = 
    let rec innerLoop expr state =
        match expr with
        |Patterns.Lambda(_, body) ->
            innerLoop body state
        |Patterns.PropertyGet(Some parent, propInfo, []) ->
            sprintf ".%s%s" propInfo.Name state  |> innerLoop parent
        |Patterns.Call (None, _, expr1::[Patterns.Let (v, expr2, _)]) when v.Name = "mapping"->
            let parentPath = innerLoop expr1 "[*]"
            let childPath = innerLoop expr2 ""
            parentPath + childPath
        |ExprShape.ShapeVar x ->
            state
        |_ -> 
            failwithf "Unsupported expression: %A" expr
    innerLoop userExpr "" |> sprintf "$%s"

type Path = 
  static member Make([<ReflectedDefinition(true)>] f:Expr<'T -> 'R>) = 
    match f with
    |Patterns.WithValue(f, _, expr) ->
        let path = jsonPath expr
        {
            Get = f :?> 'T -> 'R
            Path = path
        }
    | _ -> failwith "Unexpected argument"

警告:我对这些技术的了解还不够,无法判断 Tomas 的答案在某些极端情况下是否比我的表现更好。

【讨论】:

  • 实现这样的引用翻译总是有点棘手,而且容易出错。我的方法相当保守,因为它适用于示例,但不应接受无效参数(可能还有一些有效的 - 您需要添加)。这主要是通过跟踪表示“当前值”的变量来完成的——我认为如果有其他一些变量(可能是从 lambda 外部捕获的),您的代码可能会出错。但是在这里很难找到正确和不太复杂之间的平衡!
  • 例如,如果你有let x = System.DateTime.Now,然后使用Path.Make(fun y -&gt; x.Year),那么我认为你的代码不会检测到这是一个错误的参数,而是返回$.Year
  • 好的,很高兴知道。我会花一些时间进一步了解您的代码。
  • @TomasPetricek 我尝试了你捕获的局部变量的想法,看看它是如何失败的。它不编译。显然“引用可能不涉及分配或获取捕获的局部变量的地址”。我将尝试找到两种方法不同的其他示例。
  • 在玩了一会儿你的版本之后,我认为它实际上工作得很好。获得意想不到的东西的唯一方法是插入伪造的 lambda,即Path.Make(fun (team:Team) (x:int) -&gt; team.TeamName)。您还可以(在这两种方法中)使用不受支持的属性,例如 Path.Make(fun (team:Team) -&gt; team.TeamName.Length)
猜你喜欢
  • 2021-11-11
  • 1970-01-01
  • 2012-08-14
  • 2013-03-18
  • 2017-05-19
  • 1970-01-01
  • 2013-11-23
  • 1970-01-01
  • 2015-05-17
相关资源
最近更新 更多