-
Notifications
You must be signed in to change notification settings - Fork 101
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.Columnlook 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.Columnlook 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.


