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)

side - , , static content ( , etc) Fable side - F# project

- public

| index.html

- src

| App.fs

| App.fsproj

| package.json

| package-lock.json

| webpack.config.js

public - static content

- static content src - F# project ( Fable ) itself

- project ( ) itself package.json , package-lock.json - npm dependencies

, - 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 in your 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 does 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 have a process running after previous npm start just save and reload the page. You should see two sequential alert popups!

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 )

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 from JavaScript

// 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.

document.createElement() — returns Element which is Node

document.createTextNode() — returns Text which is Node

Node.appendChild() — returns Node

document.getElementById() — returns Element which is Node

Node.insertBefore() — returns Node

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.

process by pressing . in folder is NOT auto-reloading. run npm start

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

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!

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

Thanks for reading, I hope it was helpful!

Additional resources