BuckleScript+polyvar branch is available for testing


#1

Note this is not a formal release, but the change is big that we would like to gather user feedback as early as possible.

You can try it npm i bs-platform@polyvar

After installation:

npx bsc -e '(`hello, `world) -> Js.log'

You will see the output:

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';
console.log([
      "hello",
      "world"
    ]);

/*  Not a pure module */

As you can see, the polyvar hello is compiled into string literal "hello". For polyvar with payload, such as "`a 3" it will be compiled into "{NAME:“a”,VAL:3}

This change will make interop with JS string based API much easier, feedback is appreciated!


#2

Coul you elaborate? Is it `hello("a 3")?

I think if we talk about interop, the closest thing to variants we have in TypeScript are discriminating unions, and idiomatically, they’re flat, e.g.:

type Union =
  | { tag: "foo", a: string, b: number }
  | { tag: "bar", c: boolean, d: Date, e: number[] }
  ;

But of course, if all we have in Reason is `foo(string, int), in JS/TS the best we can do is something like: { tag: "foo", value: [string, number]} (that [string, number] being a tuple). I see a couple issues with it:

  1. It’s not quite idiomatic in TypeScript (even though { type, payload } is kind of idiomatic in Redux).
  2. You cannot use polyvar for interop with preexisting discriminating unions. Don’t know if a lot of teams have that need though.

#3

`a compiles to "a".

`a(3) compiles to {NAME: "a", VALUE: 3}.

`a(true, 1, "hello") compiles to {NAME: "a", VALUE: [true, 1, "hello"]}.

I believe the major use-case for this feature (compile polyvar to string) is to make it simple to deal with JavaScript string literals, which are used all over the place. E.g. request.on('complete', (err, data) => ...). Right now we can bind to these kinds of APIs with things like [@bs.string], [@bs.deriving jsConverter], and [@bs.inline]. But after the polyvar change we won’t need those any more, the bindings will just be regular polyvariant types.

EDIT: although to be fair with that particular example we would probably write a binding external onComplete: ([@bs.as "complete"] _, .... Anyway the point is that string literals are everywhere in JavaScript and this feature makes them type-safe.


#4

Oh. Gotcha.

I see. Yeah, if we need polymorphic variants for the good old "red" | "green" first and foremost, there’s no sense in compiling every case to a JS object.


#5

@hoichi With this new release, you can already write 0-cost bindings to TS tagged unions if you don’t mind your objects having extra fields:

type hero =
  | Jedi({
      kind: [ | `Jedi],
      name: string,
    })
  | Droid({
      kind: [ | `Droid],
      serialNumber: string,
    });

let luke = Jedi({kind: `Jedi, name: "Luke Skywalker"});

compiles to:

var luke = {
  TAG: /* Jedi */0,
  kind: "Jedi",
  name: "Luke Skywalker"
};

Beware though, this would only work to bind to objects meant to be consumed by JS, since tagged union created in JS would miss a TAG field to be consumed by Bucklescript.


#6

@bobzhang I’ve quickly tested the polyvar release, it’s pretty impressive, I can’t wait to update my bindings!

I think RLS might need an update though since it raises this error:

unknown option: '-bin-annot'.
Usage: bsc <options> <files>
Options are:
Options:
  -I                        <dir>  Add <dir> to the list of include directories
  -w                        <list>  Enable or disable warnings according to <list>:
                            +<spec>   enable warnings in <spec>
                            -<spec>   disable warnings in <spec>
                            @<spec>   enable warnings in <spec> and treat them as errors
                            <spec> can be:
                            <num>             a single warning number
                            <num1>..<num2>    a range of consecutive warning numbers
                            default setting is +a-4-9-20-40-41-42-50-61-102
  -warn-error               <list>  Enable or disable error status for warnings according
                            to <list>.  See option -w for the syntax of <list>.
                            Default setting is -a+5+6+101
  -o                        <file>  set output file name to <file>
  -ppx                      <command>  Pipe abstract syntax trees through preprocessor <command>
  -open                     <module>  Opens the module <module> before typing
  -bs-package-output        Set npm-output-path: [opt_module]:path, for example: 'lib/cjs', 'amdjs:lib/amdjs', 'es6:lib/es6' 
  -bs-syntax-only           Only check syntax
  -bs-g                     Debug mode
  -bs-package-name          Set package name, useful when you want to produce npm packages
  -bs-ns                    Set package map, not only set package name but also use it as a namespace
  -as-ppx                   As ppx for editor integration
  -no-alias-deps            Do not record dependencies for module aliases
  -bs-super-errors          Better error message combined with other tools 
  -unboxed-types            Unannotated unboxable types will be unboxed
  -bs-re-out                Print compiler output in Reason syntax
  -bs-D                     Define conditional variable e.g, -D DEBUG=true
  -color                    Enable or disable colors in compiler messages
                            The following settings are supported:
                            auto    use heuristics to enable colors only if supported
                            always  enable colors
                            never   disable colors
                            The default setting is 'always'
                            The current heuristic for 'auto'
                            checks that the TERM environment variable exists and is
                            not empty or "dumb", and that isatty(stderr) holds.
  -bs-list-conditionals     List existing conditional variables
  -e                        (experimental) set the string to be evaluated in ReasonML syntax
  -bs-no-version-header     Don't print version header
  -bs-cross-module-opt      Enable cross module inlining(experimental), default(false)
  -bs-no-cross-module-opt   Disable cross module inlining(experimental)
  -bs-diagnose              More verbose output
  -fmt                      Format as Res syntax
  -where                    Print location of standard library and exit
  -verbose                  Print calls to external commands
  -keep-locs                Keep locations in .cmi files
  -no-keep-locs             Do not keep locations in .cmi files
  -v                         Print compiler version and location of standard library and exit
  -version                  Print version and exit
  -pp                       <command>  Pipe sources through preprocessor <command>
  -absname                  Show absolute filenames in error messages
  -bs-no-bin-annot          Disable binary annotations (by default on)
  -i                        Print inferred interface
  -nolabels                 Ignore non-optional labels in types
  -principal                Check principality of type inference
  -short-paths              Shorten paths in types
  -unsafe                   Do not compile bounds checking on array and string access
  -warn-help                Show description of warning numbers

#7

Well, having both TAG and kind does seem wasteful, but overall it’ll totally work, thanks!

Do you know if there’s any plan to automatically compile Jedi(..) | Droid(..) to { tag: 'jedi', ..} | { tag: 'droid', ..}?


#8

well that would be ideal, if regular variants could use string tags, but I don’t know if it’s doable, it would be less boilerplatey and would allow to bind to tagged union not only for input of JS functions but also for output.


#9

I don’t know much about ppx’es, but maybe at least it’s possible to insert kind: [ | `Droid] via a ppx.


#10

Sure, you could write a ppx that would generate the creator functions for you


#11

I think I found a neat trick to bind to “open string literals”, JS strings that can have some predefined values or be any other string.

module Status = {
  type t = pri [> | `Idle | `Offline | `Online];
  external toString: t => string = "%identity";
};

You can then use it that way, you have type-safety and exhaustiveness check:

[@react.component]
let make = (~status: Status.t) =>
  (
    switch (status) {
    | `Idle => "idle"
    | `Offline => "offline"
    | `Online => "online"
    | other => "unknown status : " ++ Status.toString(other)
    }
  )
  ->React.string;

#12

an even more idiomatic way would be to have a dedicated BS annotation:

 module Status = {
  type t = [ | `Idle | `Offline | `Online | [@bs.default] `Other(string) ];
};

You would then use it like that:

[@react.component]
let make = (~status: Status.t) =>
  (
    switch (status) {
    | `Idle => "idle"
    | `Offline => "offline"
    | `Online => "online"
    | `Other(otherStatus) => "unknown status : " ++ otherStatus
    }
  )

`Other(other) would allow to get the default case but couldn’t be created as Status.t since its JS representation is different.
Would you be open to such a feature @bobzhang? I can open an issue if you’re interested, I think the implementation would be doable.


#13

How would you use this component?

<Comp status=??? />

EDIT: oh, I see the type system allows using the defined values. Neat trick!


#14

I’ve implemented this in ReasonRelay for zero cost enums using polymorphic variants, and for typing string literals properly. From what I can see, this seems to be working great!

I fully agree with @tsnobip that having a “built in” way of handling defaults would be incredibly valuable to match enums coming from external sources like server responses.


#15

For those who are interested, I created an issue: