Description
This is a specific interop problem between the F# core API and the native C# layer.
In short, I'd like to ask for feedback on which type of API surface looks better/more intuitive for C# users and why. I also added a in-depth problem description for the curious further below.
Ask for feedback
Please react with 🚀 for Option 1, or 🎉 for Option 2. Detailed feedback via comments are highly appreciated
As the C# API is shaping out, we encountered some specific friction points between F# and C# that need workarounds. This is not bad per se, but has an influence on the API surface.
In short, we need a Optional<T>
type for unconstrained optional parameters that can be both value and reference types.
We now have two ways of using them:
- 🚀 use
Optional<T>
for ALL optional parameters. This would make the signature ofChart.Column
look like this:
- 🎉 only use
Optional<T>
where needed. In our example, these are the optional argumentsBase
,Width
, andText
. This makes the signature more concise, but makes it less obvious that there is no special ceremony of setting these values. This would make the signature ofChart.Column
look like this:
Which of these signatures/API surfaces are more comprehensible for a C# user? This will influence hundreds of functions, so it is an important decision for Plotly.NET's C# API.
in both cases, you can call the function the same, so from a usage perspective these approaches are identical:
Chart.Column<int, string, string>(
values: new int[] { 3, 4 },
Keys: new string[] { "first", "second" },
Width: 1,
Base: 4
)
In-depth problem description
Plotly.NET's core F# API makes heavy use of generic optional parameters. Here is an example:
type Foo() =
static member Bar(
mandatory: string,
?optNoProblemo1: int,
?optNoProblemo2: DateTime,
?optNoProblemo3: seq<#IConvertible>,
?optProblem: #IConvertible // this one is problematic
) =
...
for the respective C# Layer, we have to add type constraints on these optional parameters and make them nullable so we have a sure way of wrapping parameters that are set by the caller as Option.Some(value)
, and parameters not set by the caller as Option.None
. This is a little awkwardness coming from optional parameter interop between the two, but that's not the problem:
public static int Bar<OptType>(
string mandatory,
int? optNoProblemo1 = null,
DateTime? optNoProblemo2 = null,
IEnumerable<OptType>? optNoProblemo3 = null,
OptType? optProblem = null
)
where OptType : IConvertible
=>
Plotly.NET.Chart2D.Chart.Foo(
mandatory: mandatory,
optNoProblemo1: optNoProblemo1,
optNoProblemo2: optNoProblemo2,
optNoProblemo3: optNoProblemo3,
optProblem: optProblem
);
The problem is that optProblem
cannot use null
as default value, because it can be either reference or value type:
We also cannot use = default
instead of = null
, because then we have no way of checking if the value was actually set as a default value (in that case we want to convert to Some(value)
) or if it was not set (and should therefore be wrapped as Option.None
).
To fix this, we use a class Optional<T>
:
public readonly record struct Optional<T>(T Value, bool IsValid)
{
public static implicit operator Optional<T>(T Value) => new(Value, true);
}
which sets IsValid
to true when a value is set, and false otherwise.