简体   繁体   中英

OR pattern with F# SRTP constraints is interpreted as AND

I'm wondering why the | , "or" pattern in a SRTP context in my function doThingsWithOrProps isn't working as expected below, aka it should be able accept a type that has either a property PropA or another property PropB , but it is interpreted as an AND & pattern instead like for the function doThingsWithAndProps which doesn't really make sense since the pattern expected for the input of the respective two functions is clearly different.

let inline (|PropA|) source =
    (^Source: (member PropA: 'PropA) source)

let inline (|PropB|) source =
    (^Source: (member PropB: 'PropB) source)

let inline (|PropAAndB|) (PropA (propA: 'PropA) & PropB (propB: 'PropB)) = (propA, propB)

let inline (|PropAOrB|) (PropA p | PropB p) = p

let inline doThingsWithAndProps (PropAAndB (propA: 'PropA, propB: 'PropB)) =
    printfn $"({nameof propA} = %A{propA}, {nameof propB} = %A{propB})"

let inline doThingsWithOrProps (PropAOrB propAOrB) =
    printfn $"{nameof propAOrB} = %A{propAOrB}"

// Compiles just fine
doThingsWithAndProps {| PropA = "hello"; PropB = "world" |}

// The type '{| PropA: 'a |}' does not support the operator 'get_PropB'
doThingsWithOrProps {| PropA = "wer" |}

// The type '{| PropB: 'a |}' does not support the operator 'get_PropA'
doThingsWithOrProps {| PropB = "wer" |}

===

Additional bits using https://sharplab.io to convert valid parts of the F# code above to C# (skimming some part parts about equality, hashcode, to making a little terser)

let inline (|PropA|) source =
    (^Source: (member PropA: 'PropA) source)

let inline (|PropB|) source =
    (^Source: (member PropB: 'PropB) source)

let inline (|PropAAndB|) (PropA (propA: 'PropA) & PropB (propB: 'PropB)) = (propA, propB)

let inline (|PropAOrB|) (PropA p | PropB p) = p

let inline doThingsWithAndProps (PropAAndB (propA: 'PropA, propB: 'PropB)) =
    printfn $"({nameof propA} = %A{propA}, {nameof propB} = %A{propB})"

let inline doThingsWithOrProps (PropAOrB propAOrB) =
    printfn $"{nameof propAOrB} = %A{propAOrB}"

// Compiles just fine
doThingsWithAndProps {| PropA = "hello"; PropB = "world" |}
[assembly: FSharpInterfaceDataVersion(2, 0, 0)]
[assembly: AssemblyVersion("0.0.0.0")]

[CompilationMapping(SourceConstructFlags.Module)]
public static class @_
{
    [SpecialName]
    public static Tuple<PropA, PropB> |PropAAndB|$W<a, PropB, PropA>(FSharpFunc<a, PropA> get_PropA, FSharpFunc<a, PropB> get_PropB, a _arg1)
    {
        PropA item = get_PropA.Invoke(_arg1);
        PropB item2 = get_PropB.Invoke(_arg1);
        return new Tuple<PropA, PropB>(item, item2);
    }

    [SpecialName]
    public static b |PropAOrB|<a, b>(a _arg1)
    {
        if (false)
        {
            return (b)(object)null;
        }
        throw new NotSupportedException("Dynamic invocation of get_PropA is not supported");
    }

    [SpecialName]
    public static b |PropAOrB|$W<a, b>(FSharpFunc<a, b> get_PropA, FSharpFunc<a, b> get_PropB, a _arg1)
    {
        return get_PropA.Invoke(_arg1);
    }

    public static void doThingsWithAndProps<a, PropB, PropA>(a _arg1)
    {
        if (0 == 0)
        {
            throw new NotSupportedException("Dynamic invocation of get_PropA is not supported");
        }
        PropA val = (PropA)(object)null;
        if (0 == 0)
        {
            throw new NotSupportedException("Dynamic invocation of get_PropB is not supported");
        }
        PropB val2 = (PropB)(object)null;
        object[] array = new object[4];
        array[0] = "propA";
        array[1] = val;
        array[2] = "propB";
        array[3] = val2;
        Type[] array2 = new Type[2];
        array2[0] = typeof(PropA);
        array2[1] = typeof(PropB);
        ExtraTopLevelOperators.PrintFormatLine(new PrintfFormat<Unit, TextWriter, Unit, Unit, Tuple<string, PropA, string, PropB>>("(%P() = %A%P(), %P() = %A%P())", array, array2));
    }

    public static void doThingsWithAndProps$W<a, PropB, PropA>(FSharpFunc<a, PropA> get_PropA, FSharpFunc<a, PropB> get_PropB, a _arg1)
    {
        PropA val = get_PropA.Invoke(_arg1);
        PropB val2 = get_PropB.Invoke(_arg1);
        object[] array = new object[4];
        array[0] = "propA";
        array[1] = val;
        array[2] = "propB";
        array[3] = val2;
        Type[] array2 = new Type[2];
        array2[0] = typeof(PropA);
        array2[1] = typeof(PropB);
        ExtraTopLevelOperators.PrintFormatLine(new PrintfFormat<Unit, TextWriter, Unit, Unit, Tuple<string, PropA, string, PropB>>("(%P() = %A%P(), %P() = %A%P())", array, array2));
    }

    public static void doThingsWithOrProps<a, b>(a _arg1)
    {
        if (0 == 0)
        {
            throw new NotSupportedException("Dynamic invocation of get_PropA is not supported");
        }
        b val = (b)(object)null;
        object[] array = new object[2];
        array[0] = "propAOrB";
        array[1] = val;
        Type[] array2 = new Type[1];
        array2[0] = typeof(b);
        ExtraTopLevelOperators.PrintFormatLine(new PrintfFormat<Unit, TextWriter, Unit, Unit, Tuple<string, b>>("%P() = %A%P()", array, array2));
    }

    public static void doThingsWithOrProps$W<a, b>(FSharpFunc<a, b> get_PropA, FSharpFunc<a, b> get_PropB, a _arg1)
    {
        b val = get_PropA.Invoke(_arg1);
        object[] array = new object[2];
        array[0] = "propAOrB";
        array[1] = val;
        Type[] array2 = new Type[1];
        array2[0] = typeof(b);
        ExtraTopLevelOperators.PrintFormatLine(new PrintfFormat<Unit, TextWriter, Unit, Unit, Tuple<string, b>>("%P() = %A%P()", array, array2));
    }
}

namespace <StartupCode$_>
{
    internal static class $_
    {
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        internal static readonly <>f__AnonymousType1562431155<string, string> _arg1@11;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        internal static readonly string activePatternResult1923786_0@18;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        internal static readonly string activePatternResult1923786_1@18;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        internal static readonly PrintfFormat<Unit, TextWriter, Unit, Unit> format@1;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        [CompilerGenerated]
        [DebuggerNonUserCode]
        internal static int init@;

        static $_()
        {
            _arg1@11 = new <>f__AnonymousType1562431155<string, string>("hello", "world");
            activePatternResult1923786_0@18 = @_._arg1@11.PropA;
            activePatternResult1923786_1@18 = @_._arg1@11.PropB;
            object[] array = new object[4];
            array[0] = "propA";
            array[1] = @_.activePatternResult1923786_0@18;
            array[2] = "propB";
            array[3] = @_.activePatternResult1923786_1@18;
            Type[] array2 = new Type[2];
            array2[0] = typeof(string);
            array2[1] = typeof(string);
            format@1 = new PrintfFormat<Unit, TextWriter, Unit, Unit, Tuple<string, string, string, string>>("(%P() = %A%P(), %P() = %A%P())", array, array2);
            PrintfModule.PrintFormatLineToTextWriter(Console.Out, @_.format@1);
        }
    }
}

You're confusing runtime with compile time.

The SRTP constraints work at compile time. The compiler has to make sure that any values that could possibly be passed to your function when runtime comes around will definitely satisfy the SRTP constraint.

Active matchers, on the other hand, work at runtime. It's not known ahead of time what the value passed to the active matcher will be. The active matcher gets the value, looks at it, and determines how it should be classified. That's the whole point of an active matcher: it classifies a value by some classification that's not known in advance.

So when you make an active matcher like PropAOrB , the compiler sees that, when a value is passed to it at runtime, your matcher will have to first call PropA to see if it matches the value, and if it doesn't - call PropB and see if it matches. Which means that, potentially, both PropA and PropB will be called, and therefore, the static (ie known in advance) type of the values that can be passed to PropAOrB must satisfy both PropA 's and PropB 's STRP constraints.


If you wanted to let PropAOrB accept either values that have only PropA or values that have only PropB or values that have both, one option would be to match at runtime using reflection:

let inline (|PropA|_|) source =
    let prop = source.GetType().GetProperty("PropA")
    if prop = null then None else Some (PropA (prop.GetValue(source)))

let inline (|PropB|_|) source =
    let prop = source.GetType().GetProperty("PropB")
    if prop = null then None else Some (PropB (prop.GetValue(source)))

let inline (|PropAOrB|) (PropA p | PropB p) = p

let inline doThingsWithOrProps (PropAOrB propAOrB) =
    printfn $"{nameof propAOrB} = %A{propAOrB}"

doThingsWithOrProps {| PropA = "wer" |}

doThingsWithOrProps {| PropB = "wes" |}

But of course, now both PropA and PropB return object (because that's what PropertyInfo.GetValue returns), so you'd have to know the type and cast it I guess.

Plus, you'd have to deal with incomplete pattern matches, because there is a third unhandled possibility: the value has neither PropA nor PropB .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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