Skip to content

[C#] How to handle unconstrained optional generic type arguments #322

Closed
@kMutagene

Description

@kMutagene

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:

  1. 🚀 use Optional<T> for ALL optional parameters. This would make the signature of Chart.Column look like this:

image

  1. 🎉 only use Optional<T> where needed. In our example, these are the optional arguments Base, Width, and Text. 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 of Chart.Column look like this:

image

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:

image

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions