简体   繁体   English

检查 F# 通用参数是否具有相等或比较约束

[英]Checking if an F# generic parameter has an equality or comparison constraint

Is it possible to determine at run-time if a generic parameter to a Type has one of the special F# constraints of equality or comparison ?是否可以在运行时确定Type的泛型参数是否具有equalitycomparison的特殊 F# 约束之一? These constraints are documented here .这些约束记录在此处

As a concrete example, given type X<'y when 'y: equality> = { Y: 'y } , how would I determine that 'y has the equality constraint in typedefof<X<_>> ?作为一个具体示例,给定type X<'y when 'y: equality> = { Y: 'y } ,我将如何确定'ytypedefof<X<_>>中具有equality约束?

I've tried using a few of the reflection APIs like Type.GetGenericParameterConstraints and Type.GenericParameterAttributes but both are empty.我试过使用一些反射 API,例如Type.GetGenericParameterConstraintsType.GenericParameterAttributes ,但两者都是空的。

This question mentions that the F# PowerPack can be used like so:这个问题提到 F# PowerPack 可以像这样使用:

open Microsoft.FSharp.Metadata

let setEntity = FSharpAssembly.FSharpLibrary.GetEntity("Microsoft.FSharp.Collections.FSharpSet`1")
for typeArg in setEntity.GenericParameters do
  printfn "%s - comparison=%b" 
    typeArg.Name 
    (typeArg.Constraints |> Seq.exists (fun c -> c.IsComparisonConstraint))

However, this library does not appear to support .NET core and has since been split up and can now be found here .然而,这个库似乎不支持 .NET 核心,并且已经被拆分,现在可以在这里找到。 The GitHub page mentions that "F# metadata reader is replaced by FSharp.Compiler.Service", but on a brief examination of FSharp.Compiler.Service this API appears to be much more complex to set up and use than the example above. GitHub 页面提到“F# metadata reader is replaced by FSharp.Compiler.Service”,但在FSharp.Compiler.Service的简要检查中,这个 API 的设置和使用似乎比上面的示例复杂得多。

Is there a simple way to access these special constraints in F# 6 / .NET 6 using the reflection API or using some other metadata reader?有没有一种简单的方法可以使用反射 API 或使用其他元数据读取器访问 F# 6 / .NET 6 中的这些特殊约束?

Currently I'm working around the issue by manually annotating the parameter using an attribute:目前我正在通过使用属性手动注释参数来解决这个问题:

[<AttributeUsage(validOn = AttributeTargets.GenericParameter, AllowMultiple = false)>]
type ConstraintAttribute([<ParamArray>] constraints: string []) =
    inherit Attribute()
    member _.Constraints = constraints |> List.ofArray

type X<[<Constraint("equality")>] 'y when 'y: equality> = { Y: 'y }

typedefof<X<_>>.GetGenericArguments().[0].GetCustomAttributes<ConstraintAttribute>()

Obviously having to manually annotate is not ideal, though!但是,显然必须手动注释并不理想!

Rather than pull in the whole FSharp.Compiler.Service package, I think it is easier to re-implement the logic defined in the spec :与其引入整个FSharp.Compiler.Service package,我认为重新实现规范中定义的逻辑更容易:

The constraint type: comparison is a comparison constraint.约束type: comparison是比较约束。 Such a constraint is met if all the following conditions hold:如果满足以下所有条件,则满足此类约束:

  • If the type is a named type, then the type definition does not have, and is not inferred to have, the NoComparison attribute, and the type definition implements System.IComparable or is an array type or is System.IntPtr or is System.UIntPtr .如果类型是命名类型,则类型定义不具有且不被推断具有NoComparison属性,并且类型定义实现System.IComparable或者是数组类型或者是System.IntPtr或者是System.UIntPtr .

  • If the type has comparison dependencies ty1 , ..., tyn , then each of these must satisfy tyi: comparison如果类型具有比较依赖性ty1 , ..., tyn ,那么它们中的每一个都必须满足tyi: comparison

This code works for the test cases below, and I think it would be fairly easy to tweak if more edge cases are discovered.这段代码适用于下面的测试用例,我认为如果发现更多的边缘用例,调整起来会相当容易。

open System
open FSharp.Reflection

let comparisonDeps (t : Type) =
  seq {
    if FSharpType.IsTuple t then
      yield! FSharpType.GetTupleElements t

    elif FSharpType.IsUnion t then
      for uci in FSharpType.GetUnionCases(t, true) do
        for f in uci.GetFields() do
          f.PropertyType

    elif FSharpType.IsRecord t then
      for rf in FSharpType.GetRecordFields t do
        rf.PropertyType
  }

let isComparisonType (t : Type) =
  let seen = System.Collections.Generic.HashSet<Type>()

  let rec loop t =
    seen.Add(t) |> ignore
    (
      t = typeof<IntPtr>
    )
    || (
      t = typeof<UIntPtr>
    )
    || (
      t.IsArray
    )
    || (
      (
        t.GetInterfaces()
        |> Seq.contains typeof<System.IComparable>
      )
      && (
        comparisonDeps t
        |> Seq.filter (fun t -> not (seen.Contains(t)))
        |> Seq.forall loop
      )
    )

  loop t




#r "nuget: Expecto, 9.0.4"

open Expecto

let makeTest candidate expected =
  let t = candidate.GetType()

  let message =
    if expected then
      $"%A{candidate} is a comparison type"
    else
      $"%A{candidate} is not a comparison type"

  test $"%s{t.Name} (e.g. %A{candidate})" {
    let actual = isComparisonType t

    Expect.equal actual expected message
  }

type Foo<'t> =
  {
    Foo : 't
  }

[<NoComparison>]
type Bar<'t> =
  {
    Bar : 't
  }

[<NoComparison>]
type Qux =
  {
    Qux : int
  }

type Baz =
  {
    Baz : Qux
  }

let tests =
  testList "isComparisonType" [
    makeTest 123 true
    makeTest "abc" true
    makeTest [ 1; 2; 3 ] true
    makeTest (1, "a") true
    makeTest (Set.ofSeq [ 1; 2; 3 ]) true
    makeTest { Foo = 123 } true
    makeTest (Some { Foo = 123 }) true
    makeTest { Foo = fun x -> x + 1 } false
    makeTest { Bar = "abc" } false
    makeTest [ { Bar = 7 } ] false
    makeTest { Foo = { Bar = "abc" } } false
    makeTest { Qux = 2 } false
    makeTest (true, { Qux = 2 }) false
    makeTest (Map.ofSeq [ 1, { Qux = 2 } ]) true
    makeTest (Some { Qux = 2 }) false
    makeTest { Baz = { Qux = 2 } } false
    makeTest [| true; false |] true
    makeTest (IntPtr 1) true
    makeTest [| IntPtr 1; IntPtr 2; IntPtr 3 |] true
    makeTest (UIntPtr 42u) true
  ]

let args = fsi.CommandLineArgs |> Array.skip 1

runTestsWithCLIArgs [] args tests

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM