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 (i.e. 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
.