/**
* Create a new position relative to this position.
*
* @param lineDelta Delta value for the line value, default is `0`.
* @param characterDelta Delta value for the character value, default is `0`.
* @return A position which line and character is the sum of the current line and
* character and the corresponding deltas.
*/
translate(lineDelta?: number, characterDelta?: number): Position;
This is from the VSCode API. Note that the type of the first argument lineDelta is number | undefined. This is what the editor tells me when I hover over it.
/// <summary>Create a new position relative to this position.</summary>
/// <param name="lineDelta">Delta value for the line value, default is `0`.</param>
/// <param name="characterDelta">Delta value for the character value, default is `0`.</param>
abstract translate: ?lineDelta: int * ?characterDelta: int -> Position
Here is the binding generated by ts2fable. I've changed float to int by hand here.
let t = startPosition.translate(characterDelta=x.Length)
This is how I use it in my code. I am omitting the first argument on purpose.
const t = startPosition.translate(null, x$$6[0].length);
This is the generated code for it. Note the null in the first argument. When I step through it with a debugger, I get an exception raised.
translate(change: { lineDelta?: number; characterDelta?: number; }): Position;
translate(lineDelta?: number, characterDelta?: number): Position;
translate(lineDeltaOrChange: number | undefined | { lineDelta?: number; characterDelta?: number; }, characterDelta: number = 0): Position {
if (lineDeltaOrChange === null || characterDelta === null) {
throw illegalArgument();
}
This is how it is implemented. I omitted the extra overload you see here in the previous description.
@mrakgr By looking to MDN and this blog post.
It indeed seems like we should emit undefined instead of null. This allows JavaScript to pick up the default value for example.
As a side note, we need to test this change and see what impact it can have on the existing libraries of Fable etc. Because this is a breaking change.
A temporary solution in your case could be to use Emit and overload:
open Fable.Core
type Test =
abstract translate: ?lineDelta: int * ?characterDelta: int -> string
[<Emit("$0.translate(undefined, $1)")>]
abstract translate: ?characterDelta: int -> string
let x = unbox<Test> null // This is just to get intellisense
let a = x.translate(2, 2)
let b = x.translate(2)
This generates:
export const x = null;
export const a = x.translate(2, 2);
export const b = x.translate(undefined, 2);
The above bug made me ansty so I've checked something out.
let f (x : int option) = 1
f None
function f(x) {
return 1;
}
f(null);
As I feared it is generating null for regular option arguments. This is a problem as Fable is a language that compiles to JS.
For example, in the VS Code API...I want to give an example, but it occurs everywhere. Everywhere you have x | undefined in Typescript, ts2fable will translate it to x option. That mean that if I am implementing some interface method translated by ts2fable which returns some 'x option type I have to watch out for it actually being x | undefined. If I return a None, it will get compiled to null on the JS side and that will be an error.
Right now from my perspective, the Fable JS interop story is completely broken. I am not sure where to place the blame, but ts2fable is essentially generating erroneous code everywhere. As a language creator myself, I really feel that these issues should have been hammered out during the design stage because as a user I am not at all happy to run into them right now.
My suggestions:
1) As annoying as it is, you actually need 3 different types to adequately represent JS's doubled-down brand of the billion dollar mistake.
_ | undefined. _ | null._ | null | undefined.This should be introduced into the language and the libraries.
2) Right now you have undefined : 'a in Fable.Core.JS. This is totally the wrong implementation for it and should be replaced with what I am suggesting in the above point.
3) The label enhancement is wholly inappropriate for this issue. This is a severe compiler bug.
I agree that there is some impedance there, it was a design decision (taken very early on) to erase options completely and encode None with null instead of undefined.
Fable itself is not a language (no more than Babel is a language), but I fully agree that undefined is a better representation for None.
It's easy to fix, see #2029. It could be a breaking change if code depends on it, but at least for projects I tried it on, it works fine (some of them are quite large, like fcs-fable).
It is really great that implementing a fix for this is so easy. From what I've seen so far, Fable definitely favors having a .NET idiomatic experience at the expense of JS interop (which I've found a questionable design decision), but this option issue is a deal breaker for me. I won't use Fable until it is fixed.
I know that I saw some method arguments in the VS Code's API take in _ | null | undefined, so after this issue is taken care of dealing with the other two cases cases like I suggest would be great. ReasonML has JS.null and JS.null_undefined which are modules with some utility functions, but personally I would prefer it if Fable took a pattern matching approach to this.
Right now from my perspective, the Fable JS interop story is completely broken. I am not sure where to place the blame, but ts2fable is essentially generating erroneous code everywhere.
@mrakgr It seems to me that you are taking the output of ts2fable as a standard to what Fable should adhere to and ts2fable has been obsolete for quite some time now although some people are using it to get them a head start when the API simple and has no weirdness. The recommendation s that you write the bindings by hand because code generated by ts2fable won't make anyone happy.
Fable definitely favors having a .NET idiomatic experience at the expense of JS interop
Actually, Fable does the exact opposite
There are many ways to do interop in Fable without having to change anything in the compiler which might to many breaking changes if not properly communicated to the users.
The fact that VSCode API is typed Option<T> for things that might be undefined means the type signature is wrong, not that the compiler implementation of Option<T> should be modified to suit the type signature.
The suggestions you provided:
Can be easily implemented using Option<'T> using a thin layer between the F# API and its underlying Javascript library: here are just a few examples of how to do it. Just keep in mind these are not the only ways and can be changed to suit the API you are trying to bind with proper type signatures.
I will be using static classes in order to be able to use optional parameters which will be my solution when undefined is expected.
T | undefinedUsing optional parameters or overloads. I personally prefer using overloads but optional parameters work nicely too:
// Internal module for calling the Javascript library
// This module is not and should be exposed to the F# consumers
module internal Interop =
type JavascriptModule =
abstract specialFunction: value: obj -> string
let jsModule = unbox<JavascriptModule> "some-js-module"
[<Emit("undefined")>]
let undefined : obj = jsNative
// Exposed API for F# consumers
type SomeJsModule =
static member specialFunction(?value: string) =
match value with
| None ->
// value was not provided -> use undefined as input
Interop.jsModule.specialFunction(Interop.undefined)
| Some providedValue ->
Interop.jsModule.specialFunction(unbox providedValue)
let output0 = SomeJsModule.specialFunction() // here, undefined will be used
let output1 = SomeJsModule.specialFunction("value") // "value" will be used
T | nullUsing Option<T> because it will work out of the box
module internal Interop =
type JavascriptModule =
abstract specialFunction: value: obj -> string
let jsModule = unbox<JavascriptModule> "some-js-module"
[<Emit("undefined")>]
let undefined : obj = jsNative
// Exposed API for F# consumers
type SomeJsModule =
static member specialFunction(value: string option) =
Interop.jsModule.specialFunction(unbox value)
T | null | undefinedUsing an optional parameter which itself is Option<T>
T | null for the underlying function undefined for the underlying functionmodule internal Interop =
type JavascriptModule =
abstract specialFunction: value: obj -> string
let jsModule = unbox<JavascriptModule> "some-js-module"
[<Emit("undefined")>]
let undefined : obj = jsNative
// Exposed API for F# consumers
type SomeJsModule =
static member specialFunction(?value: string option) =
match value with
| None -> Interop.jsModule.specialFunction(Interop.undefined)
| Some valueOrNull -> Interop.jsModule.specialFunction(unbox valueOrNull)
let output0 = SomeJsModule.specialFunction() // undefined
let output1 = SomeJsModule.specialFunction(Some "value") // "value"
let output2 = SomeJsModule.specialFunction(None) // null
I believe that writing an F# binding for a Javascript is all about hiding the ugliness of JS and its weird nothingness values of null or undefined instead of giving them actual types and telling users to deal with it. Sure, this is more work for the people who are actually writing the binding but that just means we need a better code generator and a revamp for ts2fable
Actually, Fable does the exact opposite
I had to some time to think during the night of the full implications of what changing the representation of option would entail. What Javascript uses undefined is what .NET uses null for - uninitialized values. Changing the representation of option is one thing, but if Fable wants to be truly JS idiomatic rather than .NET idiomatic, its uninitialized values would need to be undefined.
As they are one of the rare ways to get accidental nulls in F#, right now I am looking into how uninitialized arrays are being handled. It seems special code is generated so that arrays of value types are set to zero, and for reference types, they are specially inited to null. I am guessing that null classes have special handling code so that their undeclared references are set to null rather than undefined.
No way is this a natural thing to do in JS.
I can think a few more ways in which Fable is .NET idiomatic - its arrays are fixed by default, while the JS ones have variable sizes. This is also something that needs to be imposed on purpose.
Furthermore the way the async monad works is not like Typescript's async/await - it does not work over the native Promises. I'd like to have that happen and make my own builder, but I am not sure how to implement the monadic bind for Promises.
The recommendation s that you write the bindings by hand because code generated by ts2fable won't make anyone happy.
Those 10k LOC worth of bindings? I have better things to do than become the maintainer of such a large quantity of boilerplate.
For the VS Code API, I think the generated bindings are of pretty good quality - excepting the option issue here. Assuming this issue is taken care of promptly, and in the way I've suggested with the equivalent of ReasonML's JS.null and JS.null_undefined being added to the standard library, my intention is to open as issue for ts2fable so it takes advantage of them. I'd like this to be standardized so we can converge on a common idiom rather than have this be broken when it inevitably gets added to the library.
(Those examples)
I do not want to use ugly hacks for what should be one of the most essential interop concerns that the language should have solved years ago.
Look, I like both F# and functional programming, but I am not going to bend over backwards to please the Fable compiler. If I am going to write Fable as opposed to F#, of course I'll expect it to handle optional function arguments the same way JS and TS would. Why would somebody even use Fable unless he wanted to interop with the JS world in a functional manner? I can't imagine any reason at all why I should accept the current state of affairs where Fable treats undefined variables as nulls as if it were .NET code. I don't need this extra work.
Hello @mrakgr ,
you can use Fable.Promise to support CEs style:
let private getRandomUser () = promise {
// We add a delay of 300ms so the button animation is more visible
do! Promise.sleep 300
let! response = Fetch.fetch "https://randomuser.me/api/" []
let! responseText = response.text()
let resultDecoder = Decode.field "results" (Decode.index 0 User.Decoder)
return Decode.fromString resultDecoder responseText
}
@MangelMaxime Nice. If you are interested, consider following that up on this SO question.
Most helpful comment
@MangelMaxime Nice. If you are interested, consider following that up on this SO question.