Announcing Nagareyama (Fable 3) (III)

Alfonso García-CaroNovember 20, 2020

This is the third post in the "Announcing Nagareyama (Fable 3)" series. You can check the previous articles below:

This time we will be focusing on the actual new features of Nagareyama. There are not so many because we've been focusing on the tooling improvements for this release, but there are still a couple of interesting surprises so let's go through them together!

Plugins!

Plugins are probably the most important new feature in Fable 3 but they're actually a comeback: Fable 1 already supported them. Unfortunately, their design was not good and only a few users could take some advantage of them. Because of this, plugins were removed during the Fable 2 rewrite. We hope the design issues have been fixed and we are confident plugins are going to be much more useful to Fable developers.

Why does Fable need plugins? Because one of Fable's goals is to interop well with JS code and tooling. Sometimes, JS tooling expects code to be written in a particular way that differs to the code generated by Fable. This poses a dilemma when developing the compiler: Do we change the code generation to fit this particular tool? Or do we add some magic for the specific use case, likely complicating things both for Fable maintainers and users? Or do we try to resolve the problem with code even if it results in non-idiomatic F# or conflicts with the type-checker?

Plugins are a way to solve this dilemma: we can add some magic to manipulate the generated code for this specific case, while keeping the Fable codebase cleaner. The most obvious example and the first plugin of the Nagareyama-gen are React components. On first sight, React function components look very similar to F# code. I will spare you of the details (you can have a look at this long discussion if interested) but there are actually many nuances that have caused many problems. Last week at .NET Conf, Zaid Ajaj presented a new way of creating React components simply by decorating a function with an attribute using Nagareyama plugins, as in:

[<ReactComponent>]
let counter() =
    let (count, setCount) = React.useState(0)
    Html.div [
        prop.style [ style.padding 20 ]
        prop.children [
            Html.h1 count
            Html.button [
                prop.text "Increment"
                prop.onClick (fun _ -> setCount(count + 1))
            ]
        ]
    ]

If you haven't watched his presentation yet, please do it. It's a great introduction to creating React apps using F# and Feliz and Zaid is an awesome presenter!

Witnesses

It may be a hard-sell to promote this as a new feature, as it is something "hidden" int the F# compiler and you probably won't notice it all in your Fable code. It is however, one of the biggest improvements in Fable 3 and it has to do with SRTP resolution. So far, SRTP were resolved internally by the F# compiler in an opaque way, and because of this Fable and other F#-to-X compilers had to do their own resolution, rendering different results in some situations. Don Syme has been working a lot to change this and now, thanks to the witnesses the F# compiler exposes the information necessary to make sure Fable resolves SRTP in the same way F# does and avoiding compilation errors because of the use of advanced patterns. You can read more about witnesses in F# here.

Mangle interfaces

If you've read the documentation about calling Fable code from JS or inspected the generated JS code, you'll know that Fable detaches and mangles instance members. This is necessary for several reasons like overload resolution or tree shaking. But an exception, interface members, was introduced to make interop with JS easier. Thanks to this, in order to type an object or module coming from JS we just need to declare an interface (as ts2fable does) without any special syntax. On the other hand, this imposes some limitations: interface members cannot be overloaded and we cannot implement two or more interfaces when the member names conflict.

To overcome this limitations, Nagareyama introduces the Mangle attribute which can be used to decorate interfaces that won't be used to interop with JS and can safely be mangled, as you can see in this rather contrived example in the REPL.

Please note by default interface members will still NOT be mangled, so interop with JS won't break.

In order to use the Mangle attribute you need to use a prerelease version of Fable.Core, though we are considering to publish the new Fable.Core as a minor release when Fable 3 is out as there are no breaking changes.

Emit JS helpers

There are two attributes Fable developers use often when interacting with JS code: Import and Emit. You can read more about them here.

In the case of the Import attribute, Fable.Core also provides function helpers to get a somewhat cleaner syntax.

open Fable.Core
open Fable.Core.JsInterop

// Instead of...
[<ImportMember("my-module")>]
let myFunction(x: int): int = jsNative

// We can do...
let myFunction(x: int): int = importMember "my-module"

In the case of emitting JS this was not balanced. Nagareyama resolves this by also including function helpers to emit JS raw strings, with the same replacement rules for arguments as with the attributes. A big difference with the attributes is the code will be emitted in the declaring site instead of the call site.

let measureTime (f: unit -> unit) = emitJsStatement f """
   const startTime = process.hrtime();
   $0();
   const elapsed = process.hrtime(startTime);
   console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6);
"""

// Note we can pass multiple arguments with a tuple
let addAnything (x: obj) (y: obj): obj = emitJsExpr (x, y) "$0 + $1"

As usual, we don't recommend relying on inlined JS code too much, after all, Fable was developed to be able to use and advanced statically-typed language with the JS ecosystem, but sometime we just need to paste a piece of code we borrowed from the internet (we don't do that very often, but sometimes happens, right?) and the new helpers make this easier than using the attributes.

A word of caution about emitting raw javascript. Fable 2 relied on Babel to parse this raw code and integrate it with JS AST. Now Fable 3 prints this code directly, which surfaces a problem we don't have in F#: in JS as in other languages, there's a difference between "expressions" and "statements". Fable 3 won't be able to tell them apart from a JS code string. That's why there are two helpers: emitJsExpr and emitJsStatement. The Emit attribute also includes now an optional isStatement parameter:

// `debugger` can only appear in statement in JS
[<Emit("debugger", isStatement=true)>]
let stopDebugger(): unit = jsNative

Again, while Fable 3 is prerelease you'll have to also download a prerelease version of Fable.Core in order to use the new Emit helpers.


We've just published the (hopefully) latest release candidate. If you're already using Nagareyama please update to latest version and test that everything is fine. If there are no critical reports in the upcoming days, we will re-publish this as the official Fable 3 release! Exciting times ahead!