Fable icon indicating copy to clipboard operation
Fable copied to clipboard

Merging properties in createObj

Open OrfeasZ opened this issue 2 years ago • 9 comments

I'm trying to figure out if there's a way to merge multiple lists / sequences of key-value pairs into a single createObj call, and have them compile down to a JS object.

For example, I'd like to be able to do something like this:

type Something =
    static member inline x = [
        "a" ==> "b"
        "c" ==> "d"
    ]

let y = createObj [
    "e" ==> "f"
    Something.x
]

and have it compile to:

const y = {
    "e": "f",
    "a": "b",
    "c": "d"
};

I can currently do this:

let y = createObj [
    "e" ==> "f"
    yield! Something.x
]

But that ends up compiling to a bunch of shim calls to create the object, which I'm trying to avoid.

I also tried doing this using the Emit attribute and emitJs functions, but those end up parenthesizing my arguments which breaks the object creation syntax in JS.

Any suggestions or alternatives would be very much appreciated!

OrfeasZ avatar Nov 30 '23 03:11 OrfeasZ

I don't think there is a way to do it like that.

I was able to do it for a single element but not for a sequence:

let inline x<'T> : (string * obj)  =
    "a" ==> "b"
        
let y = createObj [
    "e" ==> "f"
    x
]

generates

export const y = {
    e: "f",
    a: "b",
};

Are you forced to do it in a single pass? Would setting properties with setter works?

open Fable.Core 
open Fable.Core.JsInterop

[<AllowNullLiteral>]
[<Global>]
type Options
    [<ParamObject; Emit("$0")>]
    (
        searchTerm: string, 
        ?isCaseSensitive: bool,
        ?limit: int
    ) =
    member val searchTerm: string = jsNative with get, set
    member val isCaseSensitive: bool option = jsNative with get, set
    member val limit: int option = jsNative with get, set


let additionalOptions (o : Options) =
    o.isCaseSensitive <- Some true
    o.limit <- Some 10


let o = Options("a")
additionalOptions o

JS.console.log o
// Output:
// {
//     "searchTerm": "a",
//     "isCaseSensitive": true,
//     "limit": 10
// 

If you don't like [<ParamObject>] because it is too verbose, you can achieve the same with jsOption

MangelMaxime avatar Nov 30 '23 09:11 MangelMaxime

Unfortunately I can't do it like this, since I'd like for the final object in the generated JS code to have all the fields (instead of them being assigned to it), in order to allow a babel plugin to transform it as a post-processing step. I'll keep experimenting!

OrfeasZ avatar Nov 30 '23 10:11 OrfeasZ

Closest I've gotten is this:

[<Erase>]
type Something =
    [<Emit("a: \"b\", c: \"d\"")>]
    static member inline x = jsNative

    [<Emit("e: \"f\"")>]
    static member inline y = jsNative
    
    [<Emit("{$0...}")>]
    static member inline create ([<ParamArray>] styles: obj list) : obj = jsNative

let jsObj = Something.create [
    Something.x
    Something.y
]

But unfortunately this compiles to:

export const jsObj = {(a: "b", c: "d"), (e: "f")};

And I can't find any way I can make Fable omit the parentheses.

OrfeasZ avatar Nov 30 '23 13:11 OrfeasZ

On a side note, I suspect it should be possible to do some more compiler magic in Fable to detect when user do something like:

let a =
    createObj [
        "a" ==> 1
        yield! [
            "b" ==> 2
        ]
    ]

So where the yield! but right now I am not familiar enough with that portion of Fable code to confirm it.

MangelMaxime avatar Nov 30 '23 13:11 MangelMaxime

Been poking at the Fable internals a bit, and I think this wouldn't be trivial. When using yield!, the whole thing seems to get compiled as a sequence expression instead of a regular tuple value list, so I guess it would need quite a bit of acrobatics to get it to print the desired JS code.

I've instead been playing around with the idea of adding a new flag to EmitAttribute that controls whether the emitted expressions get parenthesized or not here: https://github.com/fable-compiler/Fable/blob/308aceff8602cfb0f6242c7fa2e85acd29413506/src/Fable.Transforms/BabelPrinter.fs#L532-L536

Essentially adding a check in IsComplex for when expr is an EmitExpression with said flag set to false: https://github.com/fable-compiler/Fable/blob/308aceff8602cfb0f6242c7fa2e85acd29413506/src/Fable.Transforms/BabelPrinter.fs#L509-L529

Not sure if that's a good approach for this, so if anyone who's more familiar with the internals of Fable can pitch in that'd be great!

OrfeasZ avatar Nov 30 '23 21:11 OrfeasZ

TBH I am feeling like this is pushing Fable interop boundaries a bit too much compared to the complexity it can introduce.

Is there a reason why you can create the object in one go or build an anonymous record from scratch?

MangelMaxime avatar Dec 01 '23 09:12 MangelMaxime

Yeah I'm not super happy with this either, so I'll probably be taking the hit in regards to the generated code and deal with it in other ways. The main reason I would like this is for the developer experience. Being able to compose objects like this would make life a lot easier while maintaining some performance characteristics that are relevant to my application.

OrfeasZ avatar Dec 01 '23 13:12 OrfeasZ

@OrfeasZ Object.assign is your friend when merging objects

Zaid-Ajaj avatar Dec 03 '23 20:12 Zaid-Ajaj

@Zaid-Ajaj That's what I've ended up doing currently, but I'd like for objects to be emitted with all their properties without having to compose that at runtime with Object.assign or otherwise.

OrfeasZ avatar Dec 03 '23 21:12 OrfeasZ