In F# how do I call a constructor with an overload picked based on whether some option values are empty or not?

Viewed 114

Summary

I want to instantiate a class by only assigning some named parameters dynamically. I want to do this based on whether or not some values are None.

So something that looks like this:

new PhotoEditInput(id=id, title=titleOpt, slug=slugOpt, description=descrOpt, origImageHash=hashStrOpt)

PhotoEditInput's constructor has 64 overloads because it can take any of 6 parameters while leaving the others unassigned. So the constructor can be called from new PhotoEditInput() with no params at all, to new PhotoEditInput(id: string, slug: string, title: string, description: string, orderindex: string, origImageHash: string) with all params assigned, and anything in between.

Each of the xOpt values is an option and I want to leave Nones unassigned. So e.g. if titleOpt is None then I don't want to assign the title named parameter.

Question: How can I assign these named variables or pick a specific overload dynamically?


Background

I am using GraphQLProvider to generate queries and mutations. I want to create a function that lets a user edit any field of a photo object while leaving all the others unchanged.

So for the optional fields in the below function if they are None I don't want to change that field in photo with ID id. I only want the Some values changed.

let changePhotoFields (id : string) (titleOpt : string option) (slugOpt : string option) (descrOpt : string option) (hashOpt : string option) =
  // function body

PhotoEditInput is an alias for GraphQlClient.Types.Photos_set_input, which is a type generated by GraphQLProvider that represents the input of changes to a photo entity.

I initially tried instantiating the class with an empty constructor and assigning the fields afterwards but F# won't let me do this. Probably because they are Option<string> with get so I assume that means they are not settable? I'm not so familiar with the .NET ecosystem.

let makeNewPhotoEditInput id titleOpt slugOpt descrOpt hashOpt =
    let input = new PhotoEditInput(id=id)

    input.Title <- titleOpt        // Property 'Title' cannot be set
    input.Slug <- slugOpt          // Property 'Slug' cannot be set
    input.Description <- descrOpt  // Property 'Description' cannot be set
    input.OrigImageHash <- hashOpt // Property 'OrigImageHash' cannot be set

    input

If I can get this way to work that would probably be the simplest option. Is there a way I can set these fields despite them not having setters?


I'm now trying to assign fields conditionally using named arguments. E.g. something like this:

new PhotoEditInput(id=id, title=titleOpt, slug=slugOpt, description=descrOpt, origImageHash=hashOpt)

However this doesn't work because title, slug, and the other args are strings, not string options.

I've also tried making them strings but nullable so that F# doesn't complain about the types, but that breaks at runtime.

let toNull = function Some x -> x | _ -> null

new PhotoEditInput(id=id, title=toNull titleOpt, slug=toNull slugOpt, description=toNull descrOpt, origImageHash=toNull hashOpt)

Apparently the constructor can't handle nulls – even though presumably not assigning an argument is no worse than just making it null ‍♂️.

So the question is: how can I dynamically choose a specific overload that only assigns fields that are non-null?

In JS if a function supported (the equivalent of) named arguments you'd do something like

new PhotoEditInput({ id: id, title: titleOpt ?? undefined, slug: slugOpt ?? undefined, description: descrOpt ?? undefined, origImageHash: hashOpt?? undefined })

is there an equivalent in F#? Perhaps with some of F#'s introspection magic? If so how would I do this?


EDIT in response to @CaringDev's answer:

It looks like the constructor does take 64 overloads. You can pass each parameter individually. I'd imagine that the only way to differentiate between them is by naming the parameter you're assigning, e.g. new PhotoEditInput(id=id).

enter image description here enter image description here

Maybe the fact that it's a generated type means it can do things that you can't do with regular classes, I don't know. Either way your way won't work unfortunately.

In addition to that I'm also getting a warning that This downcast will erase the provided type 'PhotoEditInput' to the type 'RecordBase'.

2 Answers

If the PhotoEditInput type was a type with a constructor taking optional parameters, then you should be able to pass option values to those optional arguments (automatically mapping None to a missing value) using the following syntax:

new PhotoEditInput(id=id, ?title=titleOpt, 
       ?slug=slugOpt, ?description=descrOpt, ?origImageHash=hashStrOpt)

I'm not sure if this works with the type provider generated type that you are using (as I have no easy way of testing that), but this certainly works with normal types:

type Demo(id:string, ?n:int) = 
  override x.ToString() = sprintf "%s %A" id n

let opt = if 1=1 then Some 123 else None
Demo("123", ?n=opt)

Both in F# Duplicate method. The method '.ctor' has the same name and signature as another method in type 'Foo'.

type Foo() =
    new(a : string) = Foo() 
    new(b : string) = Foo()

and C# CS0111 Type 'Foo' already defines a member called 'Foo' with the same parameter types

public class Foo {
    public Foo() {}
    public Foo(string a) {}
    public Foo(string b) {}
}

as parameter names are not part of the signature. So I doubt that there are 64 overloads... if they look like this however:

type Foo() =
    new(a : string) =
        printfn "a"
        Foo()
    new(a : string, b : string) =
        printfn "b"
        Foo()
    new(a : string, b : string, c : string) =
        printfn "c"
        Foo()

the following works, given that the parameter order is correct and you don't need to set parameters that come after others:

let parameters =
    [ Some "a"; None; Some "c" ] // c will be ignored
    |> Seq.takeWhile Option.isSome
    |> Seq.choose (Option.map(fun s -> s :> obj))
    |> Array.ofSeq

Activator.CreateInstance(typeof<Foo>, parameters) :?> Foo // prints 'a'
Related