-
-
Notifications
You must be signed in to change notification settings - Fork 52
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
rfc: bindings #777
base: main
Are you sure you want to change the base?
rfc: bindings #777
Conversation
I have some ad hoc bindings: module Performance = struct
external now : unit -> float = "performance.now"
end
module BigInt = struct
type t
type op = t -> t -> t
external toString : t -> string = "toString" [@@mel.send]
let zero : t = [%mel.raw "0n"]
let one : t = [%mel.raw "1n"]
let ( + ) : op = [%mel.raw "(x, y) => x + y"]
end They'd become: module Performance = struct
let%js now : unit -> float = "performance.now()"
end
module BigInt = struct
type t
let%js zero : t = "0n"
let%js one : t = "1n"
let%js toString : this_:t -> string = "$this_.toString()"
let%js ( + ) : x_:t -> y_:t -> t = "$x_ + $y_"
end Correct? I like:
Could you support positional arguments inspired by Swift? let%js toString : t -> string = "$0.toString()"
let%js ( + ) : op = "$0 + $1" |
@texastoland Your snippet looks good, but I think you're missing
I thought about this, but it introduces some complexity if we mix positional and labeled arguments in the same expression. If I have |
Oops fixed 🙈
My intuition is positional terms refer to unlabeled arguments otherwise you'd use
That's a reasonable compromise if you think it'd be confusing. 1 labeled argument with many positional ones sounds undesirable anyway. I prefer vice versa. I was just experimenting more and love that it generalizes arbitrary JS including PureScript originally exposed a similar feature but removed it in purescript/purescript#887. Their reasoning was it tied libraries to specific compiler back ends. I assume Melange's solution is selective compilation with Dune? PS this is 🔥 |
1 additional note I like and prefer the |
This is a really great improvement towards a binding API that doesn't make me need to look up the docs each time I need to do it (even after like 6 years 😵💫). So after reading through this I have 2 thoughts: Mixing arg types
That would prevent things like t-middle bindings without some workarounds. Ex: let%js slice : indexEnd:int -> string -> indexStart:int -> string = "$0.slice($indexStart, $indexEnd)" Would have to become something like: let%js slice : indexStart : int -> indexEnd : int option -> str : string -> string = "$str.slice($indexStart, $indexEnd)"
(* using shadowing to override the API *)
let slice ?indexEnd str ~indexStart => slice(indexStart, indexEnd, str) Which might be ok but I wanted to present a valid use case for mixing labeled, optional labeled, and positional args. Importing class modulesBefore: type t
external book : unit -> t = "Book" [@@mel.new] [@@mel.module]
let myBook = book () After:
Maybe the After was typo-ed but it is resulting in a more confusing API IMO. The module that is imported is of type Maybe we could have an type t
let%js.import_constructor book: unit -> t = "Book" (* converts to "new Book(...args)" *)
let myBook = book() wdyt? |
Speaking of needing to reference the docs every time I make a binding... |
I think your slice example would just be: let%js slice : ?end_:int -> this_:string -> start:int -> string = "$this_.slice($start, $end_)" Underscore is supposed to allow it to be used positionally (not sure why though). I'm not familiar with "t-middle" style? |
@johnhaley81 I think your book example should be: (* Book module *)
type t
open struct (* private? *)
let%js.import construct = "@book/lib"
end
let%js make : unit -> t = "new construct()"
(* in another file *)
let book = make () The issue I see with a special constructor annotation is it doesn't add labels, spread arguments in JS, etc. |
Thanks for the feedback @texastoland @johnhaley81.
As Texas mentioned above, you can use the underscored labelled args to place positional arguments wherever in the signature. Using a shorter name like let%js slice : indexStart : int -> t_ : string -> indexEnd : int option -> string = "$t_.slice($indexStart, $indexEnd)"
(* no need for shadowing to override the API *)
I think the way I see it is: there is a type to represent the default value exported by the JavaScript module itself (the class), and another type to represent the values / instances created when the class is invoked: (* the type of the js class "Book" *)
type c
(* the type of instances created *)
type i
let%js.import book: c = "Book"
let%js create_book : ~c_:c -> unit -> i = "new $c_()"
let myBook = book |> create_book () I am not sure about adding Maybe we could allow decorating types in the destructuring using type t
let%js.import (book : (unit -> c[@new])) = "Book"
let myBook = book() This would play well with more complex destructuring: let%js.import {foo = (hey : (string -> int -> c[@new])); bar: u} = "./bar" wdyt? |
@jchavarri Did you see my solution? Another way of using import is an imperative statement that introduces identifiers in the current JS environment. Its type can be inferred or type t
let%js.import construct = "@book/lib"
let%js make : unit -> t = "new construct()" IFL the
|
@texastoland yeah, I agree that Your solution could work, but it would need some changes to prevent breakages. Notice that Melange identifiers can compile to different JavaScript identifiers for various reasons. For example, this code: let t = Js.Date.make()
let u b = t
let t = 3 Will produce: var t = new Date();
function u(b) {
return t;
}
var t$1 = 3; The second So we can't directly reference Melange identifiers safely from the JavaScript code in the To work around this, maybe we can rely in the recently added type t
let%js.import [@mel.as "Book"] book = "@book/lib"
let%js make : unit -> t = "new Book()" Maybe there could be some other way to directly reference Melange identifiers in scope from the JavaScript strings, using type t
let%js.import book = "@book/lib"
let%js make : unit -> t = "new $book()" This solution looks more complex technically, as we would need to "resolve" the Melange identifiers into the JS ones, @anmonteiro must know if this could be done directly from a ppx, but I believe it would need a deep integration with the compiler. |
Thanks that makes sense. I guess I assumed |
The preprocessor of `let%js` will check that type definitions to have at least | ||
an arrow type. So in cases where before one would just have some abstract type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this contradictory with the pi
example below? where's the arrow type then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is. I think I started from a point where removing single typed externals was a goal, but as the proposal evolved, I got distracted by migrating existing bindings to the proposal approach.
|
||
## `let%js.obj` | ||
|
||
Would be similar to `mel.obj` but as an extension applied to `let` bindings: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so it's never possible to construct a JS object without using a let
binding?
how would you express Js.log([%mel.obj { foo= "bar"}])
- Another source of friction comes from the optimizations done by the compiler | ||
in `external` statements. Melange compilation model is module-oriented, unlike | ||
js_of_ocaml, which has an executable-oriented compilation model. In Melange, | ||
users expect that the code referenced by an external function will be compiled |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this an actual expectation of users?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is, either consciously or unconsciously, considering how confused we all are when bugs related to this happen.
itself | ||
|
||
Sometimes only the first situation happens. For example, if we write bindings to | ||
`React.useCallback` we do not need to care about uncurrying, because the `react` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is simply incorrect. You can pass the result of React.useCallback
to a JavaScript library that calls the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you pass the result of React.useCallback
to a JavaScript library that calls the function, there will be some binding to that function. There should be mel.uncurry
at the boundary layer of that function, not at the React.useCallback
definition.
If we apply your point more generically, every function in a Melange program should be uncurried because potentially it can be passed to a JavaScript library as callback.
cb:('a -> 'b -> 'c) -> | ||
'c array = | ||
[%mel.raw | ||
"function (arr1, arr2, cb) { return map(arr1, arr2, Js.wrap_callback(cb)) }"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the definition of Js.wrap_callback(cb)
? I don't understand this example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK so I took a first look at this. Here are some unrefined thoughts (I need to analyze the proposal a bit more carefully):
Those are all the downsides I found with this approach. That said, I think this is a great proposal, and absolutely worth it to implement and put in front of users. If it turns out it works, and people like it better, then we should refine it and move forward with it. As for moving forward, I wonder:
As a final note: let me reiterate that I've only read this once and need to think more carefully about it holistically, so please take my (subjective) comments with a grain of salt. |
Thanks for the feedback @anmonteiro
Is this for everything in the proposal? I find surprising that examples like
I would argue that using
I don't think the current approach to externals in Melange is simple, nor easy to maintain. The amount of combinations that exist between the different attributes makes it (imo) much more complex to validate. But of course it's already implemented, while this proposal is not.
Yes, I think this and the
This is precisely what I had in mind, so I think I might proceed with it (if/when time allows). |
I really like the proposal, few comments:
So to summarise — if all FFI uses could be covered with (hope I didn't forget anything):
I'd love that! P.S. commenting on current state of JS FFI, I agree that it is a bit too complex. I feel this is because of over reliance on attributes — need to think which attribute and to what place to attach. |
I don't have much of a horse in this race, but I largely agree with everything @andreypopp has said. I personally think that the
That being said, I also don't know or understand all of the implications of that wrt the compiler and or PPXs, but regardless I'm stoked to see thought, experimentation, and iteration going into this - your rfc was well written @jchavarri 🚀 |
A proposal to improve the bindings layer in Melange.
Preview markdown.