Fable: Add JsConstructor and JsIndexer attributes

Created on 24 Jan 2019  ·  21Comments  ·  Source: fable-compiler/Fable

This could make it easier to write bindings. Instead of using Emit for everything:

[<AllowNullLiteral>]
type ForeignObject =
    abstract foo: string
    [<Emit("$0[$1]{{=$2}}")>] abstract Item: index: int -> string with get, set

type ForeignObjectType =
    abstract foo: string
    [<Emit("new $0($1...)")>] abstract Create: unit -> ForeignObject

We could have:

[<AllowNullLiteral>]
type ForeignObject =
    abstract foo: string
    [<JsIndexer>] abstract Item: index: int -> string with get, set

type ForeignObjectType =
    abstract foo: string
    [<JsConstructor>] abstract Create: unit -> ForeignObject

Most helpful comment

Thanks for your comments @Zaid-Ajaj! Well, that's the discussion between automatically and manually generated bindings. Sometimes it's necessary to interact with JS without spending too much time to write F# idiomatic and those operators may become necessary (although I don't like them particularly either). Hopefully after the new, more manageable, packages for the bindings, improving the documentation and (partially) giving up ts2fable, more volunteers can step forward to help write better bindings 🤞

All 21 comments

I can see a benefit for [<JsConstructor>] indeed.

What is a JsIndexed? It will set the value of index $1 to value $2? Is it that common in Javascript world?

It's used for objects that have indexers (JS foo[4], F# foo.[4]). If the Emit attribute is removed, Fable would compile it as foo.Item(4)).

Hum, ok.

Well if this is common adding it as attribute make sense I guess :)

Although is not possible (or at least not easy) to define a custom indexer in JS, the browser APIs contain many specialized lists (basically, read-only arrays in most cases) like DOMTokenList, NodeList, etc.

Now that I think about it, adding several attributes like JsIndexer, JsConstructor... is still not a big improvement. Maybe we can add a JsInterface attribute that by convention, changes the meaning of the Item, Create and Invoke methods.

type [<JsInterface>] ForeignObject =
    abstract foo: string
    abstract Invoke: int * int -> unit // Compiles as: foo(1, 2)
    abstract Item: index: int -> string with get, set // Compiles as: foo[3] = "bar"

type [<JsInterface>] ForeignObjectType =
    abstract Create: unit -> ForeignObject // Compiles as: new Foo()

The only thing is we are already using AllowNullLiteral attribute and bindings can become quite verbose:

[<AllowNullLiteral; JsInterface>]
type ForeignObject =
    ...

Unfortunately AllowNullLiteral cannot be inherited so we still need the two interfaces. We could give the meaning of JsInterface to AllowNullLiteral instead (and maybe use a literal) but it could happen users create an interface with AllowNullLiteral to implement in their own types.

Another solution is to give up AllowNullLiteral in the new bindings and add a new operator isNullOrUndefined to Fable.Core.JsInterop without restrictions. Also this could be used to JS nullable or optional fields so we don't need to type everything as option.

What do you think? I'm also summoning the champion of Fable interop @Zaid-Ajaj ✨

Maybe we can add a JsInterface attribute that by convention, changes the meaning of the Item, Create and Invoke methods.

I don't like this solution, because what if one day we decide to add another special transform to JsInterface but in fact is invalid for some of the types.

Also, this JsInterface, is something really hard to understand and remember. So, people, writing binding will have a problem using it. F# choose to not follow implicit stuff for a reason and here for me it's similar.

Adding JsIndexer or JsConstructor, as syntax sugar seems a good idea so people don't have to type Emit... all the time. It still says exactly what it's doing or which method is impacted.

Also this could be used to JS nullable or optional fields so we don't need to type everything as option.

In general, if we type something as option this is because there is a meaning to it. If you remove option, the editor and so the user will not have this info.

Hmm, by the same reasoning users can forget to add JsIndexer, JsConstructor, JsInvokable... And I'm afraid it may not be worth to add extra logic to compiler only for some syntax sugar that can be easily messed up (what if you add JsConstructor to and indexer or vice versa?). The specialized attributes are also implicit because consumers won't know about them.

Other alternatives:

  • By convention make always special transformations to Item, Create and Invoke methods in an interface. Problem comes when users want to implement a method with one of those methods themselves, we would have to forbid it.
  • Instead of JsInterface have a JsInterfaces attribute that you attach to the module containing the interfaces. Users still need to remember to add it but they only need to do it once.
  • Instead of an attribute, use a convention in the interface name (e.g. prefixing with Js).
  • Other ideas?

In general, if we type something as option this is because there is a meaning to it. If you remove option, the editor and so the user will not have this info.

The issue is when translating a JS options object, because most of the fields are optional we usually get an object like this:

type MyOptions =
   abstract option1: int option
   abstract option2: int option
   abstract option3 int option
   abstract option4: int option

And we have to initialize it like this:

let opts = jsOptions(fun o ->
    o.option1 <- Some 5
    o.option3 <- Some 5
    o.option4 <- Some 5
)

Removing option avoids having to type Some every time. And users can still check if a field is present or not by using isNullOrUndefined.

Removing option avoids having to type Some every time. And users can still check if a field is present or not by using isNullOrUndefined.

If I understand correctly than this will be at the cost of trust into the type system. Now users will have to write isNullOrUndefined for every field access?

@alfonsogarciacaro I remember mentioning this situation (about option for JS object) in the past but I can't find the discussion again. It was probably when designing Fable 2, I wanted to remove the option but convinced me that it better to let it here so the compiler will force people to check for a non-null value.

I do think helping people to check they type is better than expecting people to remember checking for null or undefined values. This is kind of against F# spirit :)

By convention make always special transformations to Item, Create and Invoke methods in an interface

I am against this because it's possible that a JS library defined an Item, Create method. And this case Fable would do special treatments of the methods when it shouldn't.

Instead of JsInterface have a JsInterfaces attribute that you attach to the module containing the interfaces. Users still need to remember to add it but they only need to do it once.

This still makes Fable do a lot of magic that the user needs to understand and remember that he puts the attributes at the top of his file of 1000 lines of codes.

Instead of an attribute, use a convention in the interface name (e.g. prefixing with Js).

Same as before nothing guarantee that a JS library will not have method prefixed by Js

TBH I don't think we should change something regarding Emit or option in the interface. If the type was marked in the JS docs or TypeScript system as optional then the field is really optional and we need to reflect that in the bindings too.

@matthid Yes, this would be up to the user to remember checking for null or undefined values.

Just as if you where in a language who doesn't understand "nullable" types.

Is it possible to have an attribute with a [<ParamArray>] constructor? Maybe having the empty case for an alias for all special methods Invoke, Create and Item, but then when used with arguments it will transform only the selected. That way you can remember only one attribute and have some autocompletion help to show what arguments are supported.

@matthid Please bear in mind we're talking here only about objects coming from JS. When you define an F# interface for a JS object (the bindings) this is just a compromise between the compiler and the user. Fable doesn't inject any check to verify that the interface signatures are fulfilled in the runtime. Adding option to one field is a way to force the user to check if the field is missing or not, yes, but other problems may still happen (you can expect an int and get a float or a string, for example). For F# types this is not possible, because the F# compiler will type check everything to ensure this doesn't happen.

I personally haven't found much benefit of typing fields from JS objects with option. This is particularly obvious with options objects (as in the sample above), because you usually build the object, send it to JS and forget about it. Having to add Some whenever you initialize one field only adds boilerplate without much value (moreover, many people get surprised when they assign a value like o.foo <- 4 and the compiler complains).

BTW, the int | null notation is coming to F#. This may be a better way to type fields of JS objects.

Is it possible to have an attribute with a [] constructor? Maybe having the empty case for an alias for all special methods Invoke, Create and Item, but then when used with arguments it will transform only the selected. That way you can remember only one attribute and have some autocompletion help to show what arguments are supported.

@Nhowka True, having just one attribute (say JsTransform) and letting the compiler do the transformation based on the method name is a good option and simplifies things. But I don't understand exactly what do you mean about the arguments, could you please post an example?

True, having just one attribute (say JsTransform) and letting the compiler do the transformation based on the method name is a good option and simplifies things

@alfonsogarciacaro That's probably way better than my idea! What I was thinking was something like this:

type [<Flags>] JsMethods =    
    | Invoke = 1
    | Create = 2
    | Item = 4

type JsInterfaceAttribute =
    inherit Attribute
    val methods : JsMethods

    new ([<ParamArray>] methods: JsMethods[]) =
        {methods = methods |> Seq.reduce (&&&)}

    new() = 
        {methods = JsMethods.Invoke &&& JsMethods.Create &&& JsMethods.Item}

Then you could have interfaces like:

type [<JsInterface>] SomeJsInterfaceWithAll =
    abstract Create: unit -> SomeJsInterfaceWithAll
    abstract Item: int -> string with get, set
    abstract Invoke : int -> unit

type [<JsInterface(JsMethods.Create)>] SomeJsInterfaceWithOnlyCreate =
    abstract Create: unit -> SomeJsInterfaceWithOnlyCreate
    abstract Item: int -> string with get, set
    abstract Invoke : int -> unit

type [<JsInterface(JsMethods.Create, JsMethods.Invoke)>] SomeJsInterfaceWithCreateAndInvoke =
    abstract Create: unit -> SomeJsInterfaceWithCreateAndInvoke
    abstract Item: int -> string with get, set
    abstract Invoke : int -> unit

This way you could have the method with the same name coming from JS but without giving it the special meaning, but having the attribute directly on the method seems good enough.

Thanks @Nhowka, this is an interesting idea :clap: However, it seems that all proposals have one issue or another and there's no ideal solution to replace Emit in these case. So we can close this for now and revisit later with fresh minds :)

However, it seems that all proposals have one issue or another and there's no ideal solution to replace Emit in these case.

Indeed, you would still need Emit in many other cases so these attributes won't necessarily make bindings easier to build.

That said, I think one shouldn't rely too much on what the compiler generates by default because if you want to build any idiomatic F# API you will always end up with using Emit to customize how it will be used.

An ideal binding library should hide away any javascript sepecific nuances from the user's perspective. The user should not have to think about what javascript expects, this is the job of the library. This means that exposing the following to the user from a library IMHO is not recommended:

  • isNullOrUndefined: -1 for this because it makes you think about internal representations of values during runtime
  • jsOptions: -1 because this is not how to configure API's in F# and it does not allows F# specific types as input like List because it is being consumed directly in javascript API's and these don't understand List, Map
  • the use of dynamic operators (!^, !!) and U<'t1, 't2.....'tn> because they cheat the type system, we only want to workaround the type system internally.

Use these only in implementation details of a binding, not from user code. I will probably write about this very soon because lately I have being seeing bindings with really incomprehensible consumer code.

Thanks for your comments @Zaid-Ajaj! Well, that's the discussion between automatically and manually generated bindings. Sometimes it's necessary to interact with JS without spending too much time to write F# idiomatic and those operators may become necessary (although I don't like them particularly either). Hopefully after the new, more manageable, packages for the bindings, improving the documentation and (partially) giving up ts2fable, more volunteers can step forward to help write better bindings 🤞

I just had an idea! We could add a few alias for Emit attribute to make it easier to write idiomatic bindings. And because all of them start with Emit they'll show in autocompletion and should be easier to handle by Fable. For example:

  • EmitMethod("foo") same as Emit("$0.foo($1...)")
  • EmitConstructor same as Emit("new $0($1...)")
  • EmitSelfInvokation same as Emit("$0($1...)")
  • EmitIndexer same as Emit("$0[$1]{{=$2}}")
  • EmitProperty("bar") same as Emit("$0.bar") with not arguments and Emit("$0.bar <- $1") with an argument.

And while we're at it we can add some aliases for Import:

  • ImportAll("./foo.js") same as Import("*", "./foo.js")
  • ImportDefault("./foo.js") same as Import("default", "./foo.js")

What do you think?

This seems a good idea.

The only down side, is that people will need to know what a constructor is, what a method is, etc.

Compared to "pure string" emit, where they can directly see what the resulting JavaScript will be.

However, it will probably make the code of the binding looks cleaner and if we add the preview of the generated JS in the doc comments it should be ok I think.

https://github.com/ts2fable-imports/ReactXPSample/blob/master/src/Imports/ReactXP.fs

https://github.com/fable-compiler/ts2fable/pull/161

I Proposed wo go forward to ts2fable

Actuall mutiple linked files is not so far away to go

You can view the ReactXP.fs to see. no intelisense error in it

But as personally reasons i have to go a little far from Fable

So maybe a experienced developer should come.

About the import alias @alfonsogarciacaro I think it is better to keep the JavaScript semantic on this particular point.

I just had an idea! We could add a few alias for Emit attribute to make it easier to write idiomatic bindings. And because all of them start with Emit they'll show in autocompletion and should be easier to handle by Fable.

They are easier to remember indeed, I like them 👍

Was this page helpful?
0 / 5 - 0 ratings

Related issues

krauthaufen picture krauthaufen  ·  3Comments

forki picture forki  ·  3Comments

ncave picture ncave  ·  3Comments

tomcl picture tomcl  ·  4Comments

MangelMaxime picture MangelMaxime  ·  3Comments