glutinum-org / cli

https://glutinum.net/
59 stars 5 forks source link

💡 Explore using method extension to provide a direct `method` for a `delegate` creating from `FunctionType` #144

Open MangelMaxime opened 2 weeks ago

MangelMaxime commented 2 weeks ago

This is an idea to avoid having to use Invoke from the user POV, I am not sure if this path we want to take or not.

Context

When converting FunctionType we generates a delegate in order to keep the named arguments, but this means the user needs to Invoke the delegate as they can't be called like a normal member in F#.

export interface MyObject {
    random: (min: number, max: number) => number;
}
[<AllowNullLiteral>]
[<Interface>]
type MyObject =
    abstract member random: MyObject.random with get, set

module MyObject =

    type random =
        delegate of min: float * max: float -> float

let value = unbox<MyObject> null

value.random.Invoke(0.0, 1.0)  

Proposition

An idea was proposed to create a member as an alias to the delegate to offer the standard F# member call API:

[<AllowNullLiteral>]
[<Interface>]
type MyObject =
    abstract member random: MyObject.random with get, set

type MyObject with
    member inline this.Random(min: float, max: float) =
        this.random.Invoke(min, max)

module MyObject =

    type random =
        delegate of min: float * max: float -> float

let value = unbox<MyObject> null

value.Random(0.0, 1.0)

Impact

But this solution has 2 impacts:

  1. It means we need to change the casing of the added member (random -> Ramdom) in order to access it. We can't really suffix the low level member because we need it for the setter too.

  2. Here the example above works as is, but sometimes type are defined in nested modules. In this case, the extension method would not be visible until the user open the correct modules

This can be workaround by adding a new Extensions module decorated with [<AutoOpen>] .

module ModuleA =

    module ModuleB =

        module MyObject =

            type random =
                delegate of min: float * max: float -> float

        [<AllowNullLiteral>]
        [<Interface>]
        type MyObject =
            abstract member random: MyObject.random with get, set

[<AutoOpen>]
module Extensions =

    type ModuleA.ModuleB.MyObject with
        member inline this.Random(min: float, max: float) =
            this.random.Invoke(min, max)

let value = unbox<ModuleA.ModuleB.MyObject> null

value.Random(0.0, 1.0)

Note: Make sure to check #143 for all the ideas

MangelMaxime commented 2 weeks ago

We probably also want to checks how it behaves with inheritance. TypeScript allows inheritance for class at minimal, not sure about interface.

So we need to check what happens with the method extensions or if we need to re-declare them on the inheriting type.

roboz0r commented 2 weeks ago

Did some testing and F# 9 allows extension methods with the same name as the underlying object's property. Both can be random instead of random & Random