DEV Community

loading...

Starting with Fable (F#)

semuserable profile image semuserable Updated on ・11 min read

TLDR: https://github.com/semuserable/fable-basic-interop

Starting with Fable (F#)

Preface

I like F# and somewhat dislike vanilla JavaScript. When I found out that you can write front-end using F#, I was perplexed. That's how I met Fable - a compiler which translates F# into JavaScript via Babel. There are already a plethora of different languages which can be converted into JavaScript. So, why choosing F# specifically?

This is not a tutorial about F#, so I want to suggest checking out the awesome fsharpforfunandprofit and you can decide for youself. But for me personally, F# is a very powerful, pragmatic, functional language which allows to write succinct, statically typed (bullet-proof!) code without any semantic noise.

I want to write front-end with statically-typed language. Today, TypeScript is standard de-facto if you want to write 'JavaScript-with-types'. But still, you must understand what exactly TypeScript brings to the table. You must be prepared mentally. If you're too accustomed to type-less (dynamic) code, migration to TypeScript will be painful. You must embrace the types and learn how to use them otherwise you will be writing the same old JavaScript.

For me, Fable was a rough start. It's a fresh technology and as F# is not that widespread in the wild (in comparison to JavaScript, TypeScript), information about Fable is scarce. The official example projects are not easy to comprehend when you just want to start simple. They usually involve some tinkering around.

So, I decided to create an easy to follow tutorial about how you can start using Fable today. I want to spread the word about F# and Fable specifically, so more people can try using it.

I hope you'll like it. Enjoy!

Tools

Before the start, make sure you have everything installed.

Bootstrapping

Let's start by understanding the basic folder structure of a simple Fable app. I prefer to start simple and build upon that.

In order to bootstrap a project you could use an official guide, but personally I'm not using that approach much. You will be redirected to fable2-sample repository, but it's not that convenient to navigate inside (as for my taste). There are a bunch of projects with different setup and it's hard to find a really 'empty' project. Even 'minimal' one is not that 'minimal' and contains Fable.React, Fable.Elmish.React dependencies. For a person who just want to start, it could be overwhelming.

I still highly recomment to check fable2-sample as it contains a plethora of different approaches which you can use in Fable.

Latest Fable version is 3, so you can think that fable2-sample contains outdated projects, which is not true. All projects are on the latest version.

In dotnet world we use dotnet new commands to start new projects. I would like to do the same with Fable, but there are no official templates. That's why I created templates project where you just type dotnet new fable-empty and an empty project without redundant dependencies is created.

It's time to create a new empty project.

  • install templates
dotnet new -i Semuserable.Fable.Templates::*
  • create a folder and move into it
  • run
dotnet new fable-empty

If you want to uninstall templates, run dotnet new -u Semuserable.Fable.Templates

A minimal Fable project is created.

Let's ensure that everything is working

  • execute npm install
  • execute npm start
  • open up http://localhost:8080
  • press F12 and open Console tab

Here we can see Hello from Fable!

Project structure

Each Fable project can be split into 2 sides

  • JavaScript side - webpack, npm, static content (.html, .css etc)
  • Fable side - F# project
- public
    | index.html 
- src
    | App.fs
    | App.fsproj
| package.json
| package-lock.json
| webpack.config.js
  • public - static content
  • src - F# project (Fable) itself
  • package.json, package-lock.json - npm dependencies
  • webpack.config.js - webpack configuration

In this tutorial I'll use Fable.Core library without any additional dependencies.

JavaScript side

In order for Fable to interop with JavaScript eco-system, we must ensure that all needed libraries are installed with npm. For such a simple example, we won't use many dependencies, just the core ones in order to start the dev server and run Fable compiler.

Let's quickly review two important files - package.json and webpack-config.js

  • open up package.json
{
  "name": "App",
  "private": false,
  "scripts": {
    "start": "webpack-dev-server"
  },
  "dependencies": {
    "@babel/core": "^7.7.7",
    "fable-compiler": "^2.4.12",
    "fable-loader": "^2.1.8",
    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
  }
}

Here we see minimal dependencies that are needed to start a server. There are Fable specific ones: fable-compiler and fable-loader.

  • open up webpack.config.js
// Note this only includes basic configuration for development mode.
// For a more comprehensive configuration check:
// https://github.com/fable-compiler/webpack-config-template

var path = require("path");

var contentFolder = "./public";

module.exports = {
    mode: "development",
    entry: "./src/App.fsproj",
    output: {
        path: path.join(__dirname, contentFolder),
        filename: "bundle.js",
    },
    devServer: {
        contentBase: contentFolder,
        port: 8080,
    },
    module: {
        rules: [{
            test: /\.fs(x|proj)?$/,
            use: "fable-loader"
        }]
    }
}

Very basic webpack config. All we need to know right now is that content will be served from ./public folder (must have index.html created there), server will be listening to port 8080 and the bundle.js (an app) will be generated in ./public folder.

Fable side

Here we have a very basic .fsproj project setup. App.fsproj is a netstandard application and App.fs contains the actual Fable app.

Load src/App.fsproj project into IDE of your choice and open up App.fs file.

module App

// import Fable core types
open Fable.Core 

// JavaScrpt interop call
JS.console.log "Hello from Fable!"

By default, we just wrote some info into JavaScript console. Input some new stuff into log and refresh the page - http://localhost:8080.

That's some very basic interop provided by Fable.Core library.

Basic interop

In order to utilize the full power of Fable and F# we need to write some interop code to glue F# and JavaScript together. Our minimum job is to understand how to map JavaScript types to F# ones. We also can use some helpers in the form of TypeScript type definition files, more on that later.

There is an official Fable interop documentation which you can try on your own. Armed with the interop knowledge we can start implementing it by ourselves.

In this tutorial we'll implement window.alert() (which is absent from Fable.Core), Math.random() (which exists in Fable.Core, but we'll implement it nonetheless and I'll show additionally how you can find what is implemented by default and what's not), a little bit of DOM API and p5.js lib.

window.alert()

Let's start with window.alert(). Firstly, we need to understand what we try to implement here. Is this a JavaScript library, a React component (heavily used in real Fable apps, but we won't touch it here) or maybe something global?

window is a global object in JavaScript. Next thing, what is alert() call actually do? Does it accept parameters? These questions are usually answered by comprehensive documentation. Let's open it and see how it can help. From docs, we see that alert function accepts one optional parameter of type string.

window.alert()

Now we have all necessary info for F# implementation. Let's try!

Open up App.fs and write

// interface
type Window =
    // function description
    abstract alert: ?message: string -> unit

// wiring-up JavaScript and F# with [<Global>] and jsNative
let [<Global>] window: Window = jsNative

// client calls
window.alert ("Global Fable window.alert")
window.alert "Global Fable window.alert without parentheses"

Here I used an F# interface approach which described a mapping between JavaScript and F# via some interface magic.

If you still has a process running after previous npm start just save and reload the page. You should see two sequential alert popups!

Alt Text

Let's try another approach.

// Special attribute for mapping, $0 == message parameter
[<Emit("window.alert($0)")>]
let alert (message: string): unit = jsNative

alert ("Emit from Fable window.alert")
alert "Emit from Fable window.alert without parentheses"
"Emit from Fable window.alert with F# style" |> alert 

You can choose whatever approach you want, it mostly depends on your style or libraries which you try to incorporate.

Have you noticed a subtle difference between the two calls? The former call has an optional parameter while the latter one is required.

Math.random()

Next one is Math.random(). Using the knowledge we already gained, we know that math is also a global object in JavaScript. If you are unsure you can always check the official docs.

// interface
type Math =
    abstract random: unit -> float

let [<Global>] Math: Math = jsNative

// client call
JS.console.log (Math.random())

Pretty easy, but I want to point out one important thing. Have you noticed that we wrote Math and not math where jsNative is used? Thats's because by importing it like that you must be sure that F# name is exactly the same as a JavaScript one. JavaScript API is Math.random(), not math.random().

Another one

[<Emit("Math.random()")>]
let random(): float = jsNative

JS.console.log (random())

Math.random() is implemented in Fable.Core, you don't need to recreate it again. You can find what's implemented or not by taking a look at official Fable packages.

Reload the page and check the console (F12 -> Console)

Alt Text

DOM

Now, we'll work with DOM! We'll create a div element with a text inside and attach it to some div.

Here's what we'll implement

// https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement#JavaScript

var newDiv = document.createElement("div"); 
var newContent = document.createTextNode("Hi from F# Fable!"); 
newDiv.appendChild(newContent);  

var currentDiv = document.getElementById("app"); 
document.body.insertBefore(newDiv, currentDiv); 

Alright, we need to understand where we want a new div to be attached. Do you remember index.html file? It's time to open it - project_folder/public/index.html.

Open index.html and add <div id="app"></div> above script

<!DOCTYPE html>
<html>
    <body>
        <div id="app"></div>
        <script src="bundle.js"></script>
    </body>
</html> 

We'll be adding new stuff before app div.

Where should we start with this one? Again, documentation. We need to understand what functions we're going to use.

By using info from the docs, create the following F# structure

// interfaces
type Node =
    abstract appendChild: child: Node -> Node
    abstract insertBefore: node: Node * ?child: Node -> Node

type Document =
    abstract createElement: tagName: string -> Node
    abstract createTextNode: data: string -> Node
    abstract getElementById: elementId: string -> Node
    abstract body: Node with get, set

let [<Global>] document: Document = jsNative

// client code
let newDiv = document.createElement("div")

"Good news everyone! Generated dynamically by Fable!"
|> document.createTextNode
|> newDiv.appendChild
|> ignore

let currentDiv = document.getElementById("app")
document.body.insertBefore (newDiv, currentDiv) |> ignore

Notice that I implemented document.createElement without an options parameter which is optional in docs. That's totally fine.

  • close the npm process by pressing Ctrl-Z. index.html in public folder is NOT auto-reloading.
  • run npm start

Open up http://localhost:8080/ in a browser and behold!

Alt Text

If you want to work with DOM you don't need to recreate all the bindings from scratch, there is an official library! It's called Fable.Browser.Dom. Also, there are all other sorts of default stuff implemented in this official repository.

Now you know how to work with DOM via Fable.

p5.js

It's time to implement some part of the 3rd party library. I chose p5js as it's a library to draw graphics and animation!

Stop the current npm process (Ctrl+Z), move to the root of the project and run

npm install p5 --save

p5.js is installed locally and ready to be used. It's time to write some bindings!

There are several approaches which we can take here. We can open node_modules/p5/lib/p5.js and try to decipher functions and types, but I personally don't recommend to do it (only if you have no other choice) if you have access to the source code. Because what's stored in node_modules could be packed/abridged/minified version of the library.

First approach is to check the source code of the project. Like p5 constructor and createCanvas.

Next approach would be to check TypeScript type definition files by DefinitellyTyped. I highly recommend to check it for other libraries too. Taking p5.js as an example we have p5 constructor type definition and createCanvas type definition. The cool thing about this apporach, is that we already have types which we can convert directly (not always but still) to F# types, but we should do it manually.

Final approach would be to use ts2fable tool to convert TypeScript definition files into F# itself! It's like the second approach but automatic. You just supply *.ts file and it outputs F# types! How cool is that? But be aware, this output is not always what you want to include in your final bindings. You need to check if its what you really need. There are some differences between TypeScript and F# type systems, which must be checked manually.

Armed with all this knowledge we can finally write some p5.js bindings with Fable.

open Fable.Core

// p5.js interface
[<StringEnum>]
type Renderer =
    | [<CompiledName("p2d")>] P2D
    | [<CompiledName("webgl")>] WebGL

type [<Import("*", "p5/lib/p5.js")>] p5(?sketch: p5 -> unit, ?id: string) =    
    member __.setup with set(v: unit -> unit): unit = jsNative
    member __.draw with set(v: unit -> unit): unit = jsNative
    member __.createCanvas(w: float, h: float, ?renderer: Renderer): unit = jsNative
    member __.background(value: int): unit = jsNative
    member __.millis(): float = jsNative
    member __.rotateX(angle: float): unit = jsNative
    member __.box(): unit = jsNative

// client code
let sketch (it: p5) =
    it.setup <- fun () -> it.createCanvas(300., 300., WebGL)
    it.draw <- fun () ->
        it.background(255)
        it.rotateX(it.millis() / 1000.)
        it.box()

// draw    
p5(sketch) |> ignore

Lo and behold!

Alt Text

That's it for this tutorial. The final project can be found here.

Thanks for reading, I hope it was helpful!

Additional resources

Discussion (4)

pic
Editor guide
Collapse
fjanon profile image
Fred Janon

Interesting. Thanks for taking the time to write this article.

It would be interesting to see the JS code generated and the minimum runtime code added to a simple console.log. In some instances, we get 30-60KB just doing nothing.

Keep writing!

Collapse
tunaxor profile image
Angel D. Munoz

That's pretty cool actually!

I've used Elmish with javascript, but I never thought of using Fable alone> I might go dive into more stuff like this

Collapse
pintset profile image
pintset

Thanks for the article. In case you did not know there is a package nuget.org/packages/Fable.Browser.Dom/ where you can find document, window etc., including window.alert. The namespace to open is "Browser.Dom", not "Fable.Browser.Dom"

Collapse
semuserable profile image
semuserable Author

Hello!

My article already contains a link to Fable.Browser.Dom repository and I specifically mentioned that you don't need to recreate it :)