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.
- nodejs with npm
- dotnet core SDK
-
IDE
- personally I use JetBrains Rider, but you can choose Visual Studio Code with ionide plugin or Visual Studio
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 inFable
.Latest
Fable
version is3
, so you can think thatfable2-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 openConsole
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
.
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!
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
// 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 anoptions
parameter which is optional in docs. That's totally fine.
- close the
npm
process by pressingCtrl-Z
.index.html
inpublic
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
- fable.io - official site
- Fable REPL - ready to use projects with REPL style
- Fable community - libs, projects, templates
-
awesome Fable - curated list of useful
Fable
goodness, inspired by awesome
Top comments (4)
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!
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
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"
Hello!
My article already contains a link to
Fable.Browser.Dom
repository and I specifically mentioned that you don't need to recreate it :)