-
Notifications
You must be signed in to change notification settings - Fork 304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ParamObject attribute #2555
Conversation
Hello @alfonsogarciacaro, I think the proposition is interesting, here are my thought on it. The You can even write it like that: let c =
MyJsClass(
foo = 4,
baz= 56.
) Which looks similar to let c =
{
foo : 4,
baz : 56.
} We just have the ℹ️ We can't yet write this code in F# because there is a warning about the indentation, but I think the next version of F# will relax a lot of the indentation rules. let c = MyJsClass(
foo = 4,
baz= 56.
) However, I don't like the I think it is better to have a single way to do things which works all the time than one way which works all the time and another one which works only for a very specific case. So using [<ParamObject>]
type Baz (?baz : float) =
class end
[<Global>]
type MyJsClass [<ParamObject>] (?foo: int, ?bar: string, ?baz: float) =
member _.Foo(foo: int, bar: string, ?baz: Baz) = jsNative
let c =
MyJsClass(
foo = 4,
bar = "b"
)
c.Foo(
5,
"b",
Baz(
baz = 4.
)
) Also, is the |
Another thing, that I forget to add is that I think in general this kind of objects are suffixed with Here is an example adapted from a "real npm package": let databaseOptions =
PgSubset.Pg.IConnectionParameters<_>>(
max = 15., // The maximum number of connections to keep open in the pool
idleTimeoutMillis = 10000., // The maximum amount of time a connection can sit idle in the pool before being removed
host = Env.POSTGRES_HOST.Value,
port = (float Env.POSTGRES_PORT.Value),
database = Env.POSTGRES_DATABASE.Value,
user = Env.POSTGRES_USER.Value,
password = !^ Env.POSTGRES_PASSWORD.Value
)
let db = pgp.Invoke(databaseOptions) versus let databaseOptions =
jsOptions<PgSubset.Pg.IConnectionParameters<_>>(fun o ->
o.max <- Some 15. // The maximum number of connections to keep open in the pool
o.idleTimeoutMillis <- Some 10000. // The maximum amount of time a connection can sit idle in the pool before being removed
o.host <- Some Env.POSTGRES_HOST.Value
o.port <- Some (float Env.POSTGRES_PORT.Value)
o.database <- Some Env.POSTGRES_DATABASE.Value
o.user <- Some Env.POSTGRES_USER.Value
o.password <- Some !^ Env.POSTGRES_PASSWORD.Value
)
let db = pgp.Invoke(databaseOptions) The main benefit that we can see directly is that we don't have the |
@MangelMaxime Sorry, my explanation was not complete. The index indicate when the object arguments start. So if you have: [<Global>]
type MyJsClass() =
[<ParamObject(2)>]
member _.Foo(foo: int, bar: string, baz: float, ?lol: int) = jsNative
let test() =
let c = MyJsClass()
c.Foo(5, baz=4., bar="b", lol=3) Becomes: function test() {
const c = new MyJsClass();
return c.Foo(5, "b", {
baz: 4,
lol: 3,
});
} That is, from argument with index 2, all the arguments are transformed into an object.
At the moment this only modifies the call not the receiving arguments. So yes, it should be used only with Global or Imported classes. We should allow it for interfaces too because interfaces are used a lot to represent JS types, but it would be problematic if users implement them in F#. |
Also, note that atm this affects only methods/constructors, not classes. So it's not exactly the same as // This is not possible in the current proposal, but
// imagine we could apply ParamObject to a class.
// We would have to define them like this and the compiler
// should deal with them as if they were an interface.
[<ParamObject>]
type MyJsOptions (?foo: int, ?bar: string, ?baz: float) =
member _.foo = foo
member _.bar = bar
member _.baz = baz |
I like it even less 🤣 Because, what if the JavaScript method ask for something like that: myFunc(1, 2, { propA: "something" }, "b", { propB: "test", propC: "something"}) The
This is was I understand indeed, sorry I forgot the |
@alfonsogarciacaro I really like both proposals. @MangelMaxime there are many functions that an initial number of required parameters, then an As for myFunc(1, 2, { propA: "something" }, "b", { propB: "test", propC: "something"}) Even though |
@MangelMaxime Yes, let c = MyJsClass(
foo = 4,
baz = 56.
) will work in F# 6. |
I know that I gave an edge case. One of the greatest thing in Fable is that it has good interop story with JavaScript not because of magic stuff happening but because it is explicit about what's going on and what is the generated code from it. I see people being confused all the time about magic stuff in Fable and having a hard time understanding when to use solution X versus solution Y. We can also see it happening in the C# language decision where people don't know any more which feature to use to do what because there are too many ways of doing things. Please let's keep the F# philosophy and keep things simple/straightforward :)
According to my experience, this is not often that we need to access an object options/parameters properties but it can happen. So it would be nice to have it supported if possible. It is indeed more verbose but we can have binding generation making the process easier. |
I just thought that I was focusing a lot on F# to JavaScript side but the inverse is true. One benefit, of having a closer mapping to JavaScript is that when consuming or adapting a JavaScript library to F# the JavaScript documentation is still relevant. The farther we go from JavaScript the less relevant is the JavaScript documentation and so people need to write more documentation on how to consume the bindings. |
I think we can merge the PR as is now and we can decide whether to allow // Binding
[<ParamObject>]
type SaveSnapshotOptions(name: string, content: string) =
class end
type WebTestRunner =
static member saveSnapshot(options: SaveSnapshotOptions): JS.Promise<unit> = jsNative
// Usage
WebTestRunner.saveSnapshot(SaveSnapshotOptions("foo", "bar")) Versus: // Binding
type WebTestRunner =
[<ParamObject>]
static member saveSnapshot(name: string, content: string): JS.Promise<unit> = jsNative
// Usage
WebTestRunner.saveSnapshot("foo", "bar")
In most cases, JS APIs mix positional and "object" arguments because of two reasons:
In both cases I think it should be easy for users to mentally map the options with the optional arguments in the original JS api thanks to the argument names. Let's consider the
In F# this can become (suposing type Event
[<ParamObject(fromIndex=1)>]
new (typeArg: string, ?bubbles: bool, ?cancelable: bool, ?composed: bool) =
This is a rightful concern. To avoid adding too much magic to Fable, we try to use standard F# solutions and avoid Fable hacks (e.g. removing |
Just to note that here, we can already leverage [<Global>]
type Options [<ParamObject; Emit "$0">] (?a: int, ?b: int) =
member val a: int = jsNative with get, set
member val b: int = jsNative with get, set
let opts = Options (a = 1) This snippet becomes: export const opts = {
a: 1
}; |
That's true, good one @inosik! |
I am playing with ParamObject attributes to create a binding and one thing that I discovered is that you can use constructor overload to offer an experience without [<AllowNullLiteral>]
[<Global>]
type FuseOptionKeyObject [<ParamObject; Emit("$0")>]
private (name: U2<string, ResizeArray<string>>, weight: float) =
[<ParamObject; Emit("$0")>]
new (name: string, weight: float) =
FuseOptionKeyObject(U2.Case1 name, weight)
[<ParamObject; Emit("$0")>]
new (name: ResizeArray<string>, weight: float) =
FuseOptionKeyObject(U2.Case2 name, weight)
member val name: U2<string, ResizeArray<string>> = jsNative with get, set
member val weight: float = jsNative with get, set See how here, the main constructor is private and the public constructors use standard F# types :) On the usage side it looks like that |
Very often we have to write bindings for JS methods accepting an object argument and the current solution (instantiate options with
jsOptions
or cast an anonymous record with!!
) are not nice.This allows you to add
[<ParamObject>]
attribute to the method with an optional index argument to say where the object arguments start. Optional arguments set to None are omitted. For example, this F# code:Becomes: