Open hesxenon opened 1 year ago
@hesxenon
Did you try to use method extension
? For example, this is used a lot in Fable.Browser
packages to extends existing type with new methods:
[<AutoOpen>]
module Browser.CssExtensions
open Fable.Core
open Browser.Types
type Window with
[<Emit("$0.getComputedStyle($1...)")>]
member __.getComputedStyle(elt: Element, ?pseudoElt: string): CSSStyleDeclaration = jsNative
type Element with
/// returns this DocumentOrShadow adopted stylesheets or sets them.
/// https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets
[<Emit("$0.adoptedStyleSheets{{=$1}}")>]
member __.adoptedStyleSheets with get(): CSSStyleSheet array = jsNative and set(v: CSSStyleSheet array) = jsNative
In general, official binding try to provide a 1 to 1 match with the JavaScript API because it makes it easier for people to use them and reduce the need to write our own documentation, design our API, etc.
Some library author prefer to not provide a 1 to 1 match and in this case we speak more of a library instead of binding.
In your case, you should be able to do what you want by creating your own library from scratch or better on top of the existing binding for compatibility purpose.
let setHeader (msg : IncomingMessage) key value =
msg.setHeader(key, value)
Or is setHeader
is mising you can dynamic typing:
open Fable.Core.JsInterop
let setHeader (msg : IncomingMessage) (key : string) (value : string) =
msg?setHeader(key, value) // Note the ?
I also looked at the page you provided but searching for bind
didn't show something like bind<IncomingMessage, string * obj -> unit>
Maybe I did something wrong, but method extension didn't work - apparently because I was trying to extend an abstract class.
I get the "binding 1 to 1 part" and I agree in theory, but how does my suggestion contradict this? Now we have to define a type or record or whatever, import whatever and say "what I just imported looks like this". For object methods it should apparently be a type with the aforementioned (apparent) problems. My suggestion would circumvent this and make the usage more in line with the usual f#/.Net extensions.
And yes, there's no "bind" in the docs I linked, that was already my suggestion for fable. Rescript does this with @send and "external".
I achieved the same result with [<Emit("$2.setHeader($0, $1)")>] and maybe it's possible to abstract this easily with the proposed syntax?
On Fri, Dec 30, 2022, 10:00 Maxime Mangel @.***> wrote:
@hesxenon https://github.com/hesxenon
Did you try to use method extension? For example, this is used a lot in Fable.Browser packages to extends existing type with new methods:
[
]module Browser.CssExtensions open Fable.Coreopen Browser.Types type Window with [<Emit("$0.getComputedStyle($1...)")>] member .getComputedStyle(elt: Element, ?pseudoElt: string): CSSStyleDeclaration = jsNative type Element with /// returns this DocumentOrShadow adopted stylesheets or sets them. /// https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets [<Emit("$0.adoptedStyleSheets{{=$1}}")>] member .adoptedStyleSheets with get(): CSSStyleSheet array = jsNative and set(v: CSSStyleSheet array) = jsNative In general, official binding try to provide a 1 to 1 match with the JavaScript API because it makes it easier for people to use them and reduce the need to write our own documentation, design our API, etc.
Some library author prefer to not provide a 1 to 1 match and in this case we speak more of a library instead of binding.
In your case, you should be able to do what you want by creating your own library from scratch or better on top of the existing binding for compatibility purpose.
let setHeader (msg : IncomingMessage) key value = msg.setHeader(key, value)
Or is setHeader is mising you can dynamic typing:
open Fable.Core.JsInterop let setHeader (msg : IncomingMessage) (key : string) (value : string) = msg?setHeader(key, value) // Note the ?
I also looked at the page you provided but searching for bind didn't show something like bind<IncomingMessage, string * obj -> unit>
— Reply to this email directly, view it on GitHub https://github.com/fable-compiler/Fable/issues/3318#issuecomment-1367802409, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACOHZTTISTU6HMQPMD2GERLWP2QEHANCNFSM6AAAAAATMNNDEA . You are receiving this because you were mentioned.Message ID: @.***>
Maybe I did something wrong, but method extension didn't work - apparently because I was trying to extend an abstract class.
Seems like fable-browser use abstract class too:
// Fable.Node
type [<AllowNullLiteral>] IncomingMessage =
inherit Readable<Buffer>
abstract httpVersion: string with get, set
abstract httpVersionMajor: int with get, set
abstract httpVersionMinor: int with get, set
abstract connection: Net.Socket with get, set
// ...
// Fable.Browser
type [<AllowNullLiteral; Global>] Window =
inherit EventTarget
inherit WindowTimers
inherit WindowSessionStorage
inherit WindowLocalStorage
inherit GlobalEventHandlers
inherit WindowBase64
inherit WindowURI
abstract animationStartTime: float with get, set
abstract closed: bool with get, set
abstract defaultStatus: string with get, set
// ...
I get the "binding 1 to 1 part" and I agree in theory, but how does my suggestion contradict this?
Because, response.setHeader("content-type", "text/html")
is a direct equivalent to setHeader response "content-type" "text/html"
.
The first one make it easier for people that know JavaScript to port their code and still benefit from F# type system. The second one is a more functional approach of the problem. In the past, the problem was solved by having the binding done using classes and people creating library on top of the binding to provide another UX.
I think - after playing around some more - I understand now. I still don't like having to put my type extension and new definitions that might be involved with that extension in seperate places though...
module Node =
module Http =
type SomeThingMissing = obj
type Node.Http.IncomingMessage with
member _.somethingMissing: Node.Http.SomeThingMissing = jsNative
Is there a way around this?
I guess what I'm arguing for is a convenience attribute that wraps this
type Window with
[<Emit("$0.getComputedStyle($1...)")>]
member __.getComputedStyle(elt: Element, ?pseudoElt: string): CSSStyleDeclaration = jsNative
in something like let getComputedStyle = bind<Window, Element * string option -> float>
(which could then be placed in the Window
module) because it's more straightforward to extend modules than it is to extend types, especially if you have to scatter your extensions around a single type over multiple places.
As for the 1:1 binding part: that's just data-first vs data-last, isn't it? Shouldn't F# lean towards the latter?
In theory if something is missing in the binding it is better to contribute it to the original binding so everyone can benefit from it.
I still don't like having to put my type extension and new definitions that might be involved with that extension in seperate places though
I don't get this part, if the code is inside the binding you place everything in the same file in general.
As for the 1:1 binding part: that's just data-first vs data-last, isn't it? Shouldn't F# lean towards the latter?
Well here you not really in F# but more in Fable, and in the past embracing JS has often proven to be the best choice and the one that worked best over time.
Another benefit of using standard F# syntax is that we avoid adding too much magic on top of the F# compiler and for F# developer this is just standard code.
it is better to contribute
absolutely. If you can do that. But what if you want to augment Window
? Or you embed fable into a system that modifies globals? A custom server that adds stuff to the incoming message object? There are scenarios where you can't contribute to the open source repo. And that's completely ignoring the time until the PR is approved and eventual corporate policies.
code is inside the binding ... same file
Yeah, but it's not in the same module. And I don't want to split everything and their grandmother into separate files, one of the reasons F# is awesome imho is because it breaks up this notion of "one file one thing".
not in F# but more in Fable
true, but I doubt people choose fable because of its similarities to JS. But there's already more than enough discussion around that topic in the tc39 drama threads around the pipe operator.
Apart from that: this proposal doesn't need to implement data-last semantics. It would just be a better fit imho.
Another benefit of using standard F# syntax
I thought Emit
wasn't standard F#? And importMember
is also already there... This proposal doesn't add any new syntax, does it? If so then that is purely because of me being a F# noob trying to find a language that is somewhere between JS and Haskell
I thought Emit wasn't standard F#? And importMember is also already there... This proposal doesn't add any new syntax, does it? If so then that is purely because of me being a F# noob trying to find a language that is somewhere between JS and Haskell
Emit
and importMember
are indeed not pure F# syntax but well anchored in Fable now.
I will let other maintainers voice their opinion and see what they think now.
Fable 4, has a plugin system which allows to modify the generated code so perhaps it is possible to use a plugin to create a prototype. Plugin stuff is not that well documented yet but you can look at Feliz.CompilerPlugins if you want to test it.
well that would be an awesome solution either way then :)
Motivation
While working with Fable.Node I inevitably came across some missing members of some types. Extending these types doesn't seem to be possible (couldn't find anything in the F# docs about this though).
So I read through the FS->JS Interop docs and thought how this is all centered around either importing values from modules (which maps cleanly to F# afaict) or defining types that are ultimately not extensible.
Suggestion
Would it be possible to take some inspiration from rescript and create something like
let setHeader = bind<IncomingMessage, string * obj -> unit>
?I mean yeah, you'd have to call that like
msg |> setHeader "content-type" "text/html"
but I'd actually like that. A lot.