Skip to content
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

Merging properties in createObj #3626

Open
OrfeasZ opened this issue Nov 30, 2023 · 9 comments
Open

Merging properties in createObj #3626

OrfeasZ opened this issue Nov 30, 2023 · 9 comments

Comments

@OrfeasZ
Copy link
Contributor

OrfeasZ commented Nov 30, 2023

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!

@MangelMaxime
Copy link
Member

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

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Nov 30, 2023

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
Copy link
Contributor Author

OrfeasZ commented Nov 30, 2023

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.

@MangelMaxime
Copy link
Member

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.

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Nov 30, 2023

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:

member printer.ComplexExpressionWithParens(expr: Expression) =
if printer.IsComplex(expr) then
printer.WithParens(expr)
else
printer.Print(expr)

Essentially adding a check in IsComplex for when expr is an EmitExpression with said flag set to false:

member printer.IsComplex(expr: Expression) =
match expr with
| CommentedExpression(_, e) -> printer.IsComplex(e)
| Undefined _
| Literal(NullLiteral _)
| Literal(Literal.StringLiteral _)
| Literal(BooleanLiteral _)
| Literal(NumericLiteral _)
| Literal(EnumCaseLiteral _)
| Expression.Identifier _
| MemberExpression _
| CallExpression _
| ThisExpression _
| Super _
| SpreadElement _
| ArrayExpression _
| ObjectExpression _
| JsxTemplate _
| JsxElement _
| UnaryExpression _ -> false
| _ -> true

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!

@MangelMaxime
Copy link
Member

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?

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Dec 1, 2023

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.

@Zaid-Ajaj
Copy link
Member

@OrfeasZ Object.assign is your friend when merging objects

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Dec 3, 2023

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants