Fable is an F# language to javascript compiler powered by Babel.
F# is a language that runs on a Microsoft CLR. Using the .NET Core CLR implementation it works on Windows, Mac or Linux.
- .NET Core SDK - download the latest 2.1 SDK
- Mono -
mono-complete
package - F# compiler -
fsharp
package from the mono repository
Checkout Visual Studio Code + Ionide or the non-free Jetbrain's Rider IDE.
fsharpi
(fsharp interactive REPL should be available), fsharpc
(f# compiler) and dotnet
(.net core CLI tooling) should all be available on the command line.
- .NET Core SDK - download the latest 2.1 SDK
- .NET Framework - note should come with visual studio anyway
- Visual Studio Community Edition as a convenient way to get the F# compiler
See F# on Windows
fsi
(fsharp interactive REPL should be available), fsc
(f# compiler) and dotnet
(.net core CLI tooling) should all be available on the command line.
Note, if your code compiles and does something odd you can probably find what it has compiled to by using source maps and breakpointing in the browser.
JS
var blah = new Swagger(swaggerParams)
F#
let blah = createNew Swagger swaggerParams
Note
createNew Swagger?swaggerParams
is not what we want === new Swagger.swaggerParams
When using dynamic untyped JS the obj
type returned often needs casting to something sensible using the (!!)
function. Ideally we avoid dynamic JS as much as possible and use typed binding to JS libraries or F# code only.
!!createNew Swagger swaggerParams
var swaggerParams = {
url: "https://costing-api.int.gigaclear.net/swagger.json",
requestInterceptor: function(request){ request.headers.Authorization = authHeaderValue }
}
In F# becomes:
let swaggerParams = createObj [
"url" ==> "https://costing-api.int.gigaclear.net/swagger.json"
"requestInterceptor" ==> (fun request -> request?headers?Authorization <- authHeaderValue)
]
The double arrow is a helper function (==>)
for upcasting the data on the right to an object. Note the usual F#
syntax of <-
being a mutating assigment. =
would be a comparison.
Printing to console:
open Fable.Import.Browser
Fable.Import.Browser.console.log("Hello World")
Dump JS type:
[<Emit("typeof $0")>]
let jsType (x: obj) = jsNative
jsType someObject
Promise based APIs, such as used in the Swagger Client JS library use the type in Fable.Import.JS.Promise which is a JS binding and used, for example, in the 3rd party fable-powerpack library.
If we have an object client
in F# code that references an untyped javascript library object, as we might with e.g.
the swagger-client JS library, then we may access some API in JS code with client.apis.default.healthcheck()
that has a keyword.
Using dynamic untyped JS objects we can access properties using the ?
function:
new Swagger.swaggerParamsclient?apis
As default
is an F# keyword (but not a JS keyword) we can access the default keyword by string quoting it:
client?apis?("default")?healthcheck()
Assuming we have fetched the library with yarn/npm into the node_modules directory after adding it to the
package.json
file:
let Swagger: obj = importDefault "swagger-client"
is equivalent to the javascript:
var Swagger = require('swagger-client')
or in ES2015 import syntax import Swagger from swagger-client
"Source maps" are available -> so can Ctrl+P
search for .fs
files of interest in Chrome devtools and set breakpoints. In chrome devtools -> sources
see webpack://
-> src
.
The dotnet core and .net framework platforms now (seem) to provide parity of F# features/functionality. Type Providers are the most obviously complex F# feature that was initially unsupported on dotnet core. TypeProviders on .net core/Linux indicates that some of the type provider libraries may need updating but the support is there for them to work on dotnet core.
Fable, not dotnet core, does not support type providers. This was a nuisance in one case - that of using a swagger (open api) code generator. F# does not have a swagger codegen as it has it's own extra special/featureful SwaggerProvider typeprovider to do that. Fable cannot currently compile typeprovider code though - Fable support issue is Open.
C# libraries cannot be compiled by Fable. A C# swagger generated library was tried. Fable requires source code access to compile 3rd party libraries.
So, provide F# source code. When the fable compiler is running you can see the source code for 3rd party libraries
dumped into the .fable
directory.
Note, that fable libraries which are only JS bindings, that is type signatures for Javascript libraries can be packaged
as dotnet dlls - we don't need access to the source code for those. Fable can read metadata from .dll assemblies (like signatures and attributes) but not executable code, for that it needs the F# sources.
When looking for Fable libraries on nuget JS bindings are conventionally in the Fable.Import
namespace:
- Nuget javascript search for
Fable.Import.*
on nuget - Nuget F#/Fable non JS-binding search for
Fable.*
on nuget
See Fable Library Template details if you ever want to write a standalone fable library.
These libraries seem to have something in common - the F# source is exposed in a special way to the Fable compiler. E.g. for Fable.Elmish.React
<ItemGroup>
<Content Include="*.fsproj; *.fs" PackagePath="fable\" />
</ItemGroup>
The Cvdm.ErrorHandling library works with fable and has the same addition.
In contrast FsToolkit.ErrorHandling doesn't include it and currently doesn't compile with fable - you get error messages like:
error FABLE: Cannot resolve FsToolkit.ErrorHandling.CE.Result.Result.ResultBuilder.Return
even if added to paket, installed and the editors intellisense is working with it fine.
The fsproj file copied into the fable/
folder should be the same name as the Nuget package name (which is usually the natural thing to do anyway).
If fable requires raw source code to compile everything, then why are there libraries I know that I am using that do
not appear in the .fable
directory? Well somethings are batteries included.
- "Fable provides support for some classes of .NET BCL (Base Class Library) and most of FSharp.Core library"
- Fable compilers JS Bindings
The selection of JS bindings at fable-compiler/fable-import are somewhat arbitrary. Other Fable.Import.Something JS binding libraries are 3rd party available as source code and need to be added to your project with Paket - similar to Pipenv in Python.
See NuGet, similar to PyPi in Python, for 3rd party libraries.
When using the libraries be careful of case. E.g. Fable.Import.Browser.Document
is a type but Fable.Import.Browser.document
is the global top window.document
(in the JS console).
- Fable.PowerPack - Fetch, Json, Promise
- Elmish
- Fable.React + [Random Blog](Fable React Article)
Given that we don't have the source code for all F# libraries there are times when we will want to pull the functionality from the JS ecosystem (when we don't want to provide the type signatures ourselves - e.g. typescript definition files are not available and/or ts2fable
does not work).
The main links for learning about this:
The Typescript to Fable compiler can convert typescript declaration files into Fable bindings for JS code.
How reliable is this? Unsure, on the swagger codegenerated typescript files it found some typescript type definitions that it refused to compile!
Small point, if you get type confusion/conflicts you may have imported ("opened") libraries with similar module names.
For example, if using the Elm architecture then you are probably using the Elmish library.
Fable.Import.Browser
lets you use the browser JS/DOM API and will probably show up in many projects.
open Elmish
open Elmish.Browser.Navigation
open Fable.Import.Browser
.NET languages have a habit of opening/importing too many names, like import * from foo
in Python. At this point
Browser.foo
could be Fable.Import.Browser
or Elmish.Browser.foo
. F# doesn't support namespace aliases, but it does support module aliases and being more specific is required in this case:
module FableBrowser = Fable.Import.Browser
...
FableBrowser.console.log("Hello World")
There are two sets of tooling to be concerned with. Tooling for the F# (dotnet really) ecosystem and tooling for Javascript libraries. There are templates that help you get started with Fable, e.g:
The templates come with instructions on building/running your application.
Other templates for dotnet in general, including ones not for fable.
WebPack is the current (IMO) defacto JS build tool. Examine webpack.config.js
. Hopefully you will not have to change it. To try out the Fable 2.0 beta compiler you will have to (somewhat painfully) mess with it - but it's probably best to stick with Fable 1 and whatever the Fable templates give you for now.
The hot module reloading feature of the webpack devserver should be setup by the Fable templates. You may need to change the port number it is running on. The devserver rebuilds/reloads your code everytime you save a file you are working on. The production webpack build, on the other hand, minifies/optimizes and bundles your code ready and does not use a dev server.
Documentation may tell you to use Yarn or NPM as part of the JS ecosystem build. It probably doesn't matter which, yarn is just a better NPM. Regardless you will need NodeJS installed.
We don't have to run webpack
(for example) directly. See the "scripts"
section inside package.json
. This defines
commands that can be run by yarn/npm. E.g. the fable elmish template has a script "start" that calls "webpack-dev-server" and can be run via npm start
or yarn start
.
The dotnet-fable
CLI tool can defer to these commands aswell, for example:
dotnet fable yarn-(build|start|test)
=== dotnet fable yarn-run (build|start|test)
and naturally enough yarn-run
just delegates to running a script defined in package.json
.
Yarn and npm have commands for adding new libraries. It's also easy to edit your package.json
files (similar to Pipfile
in Python) and then run yarn install
. A dependency implies the library will in someway be bundled into your compiled javascript, whereas devDependency is some library used to build/compile your JS from F# with Fable.
The package-lock.json
file or yarn.lock
(if using yarn) is equivalent to Pipfile.lock
in Python projects.
Paket is the package manager used for F# dependencies. It doesn't need a global installation, the binary is included in the .paket
folder. Paket functions like Pipenv for Python. Project management in dotnet is more complicated than Python.
So, there is some cognitive burden when coming from a python structured project. Some of these are not a big deal if using point and click IDE user interfaces such as visual studio. The open source cross platform projects seem more likely to be CLI tool focused.
The templates for dotnet core installable via the CLI (dotnet new -i ...
) help to reduce boiler plate when setting up new projects. These templates rarely compose perfectly, if at all sensibly, with one another, so it's hard to take a bit of one template and some of another without doing the work of the templates yourself (learning dotnet
commands etc).
In the wider F# ecosystem (not just Fable projects) there are a bunch of f# project skeletons around, many of which don't compose perfectly or have in the past only been for the .net framework and not .net core. Some exist as "new project" options in visual studio, different ones exist in visual studio code + Ionide plugin via the
Forge clitool. Project scaffold is on github and the dotnet new -i ...
templates are
listed here.
Thankfully, the fable elmish react template is pretty solid by itself.
So, some detailed points on the tooling (and some comparisons with Python).
- Dotnet users (C# and F#) who use visual studio (Windows only) may not even bother with paket and just use the Nuget tool, even though it is not a good dependency tracker. See Paket for why. Open source and typically cross-platform F# projects prefer to use Paket.
- Paket has the distinction between the specification of everything that is to be downloaded (perhaps into groups such as Main, Tools, Tests etc.) via
paket.dependencies
vs. each project specifying what it needs out of those downloaded dependencies viapaket.references
. In python one repository is basically one project (library or application) so the pipenv dependencies are implicitly for that project only. Pipenv simplifies the groups into normal dependencies vs dev-dependencies -> you cannot come up with random names for your dependency groups the way you can with paket. - F# repositories often have a solution file plus one or more project files, whereas python is always one project. Even adding tests to a project means creating a separate test application project with its own
paket.references
. One doesn't just run something like pytest that discovers the test files (which can be messy in python with having to add init.py files to the pythontests
directory and adding the tests directory to the python sys path). All this means we have to remember to add references between projects (e.g. the tests project references/depends on the main project). When also using solution files we have to add the projects into the solution. - The source code files have to be added to the relevant project, the fact that a .fsproj exists doesn't implicitly tell the comipler to put all .fs files in certain subtree(s) into the project. In F# the listed order matters.
- There is some crossover between the
dotnet
cli andpaket
. For the most part when using dotnet core the dotnet cli should correctly delegate dependency management topaket
. However, (I could have messed something up), restoring and making available on the CLI a clitool such asdotnet-fable
so one can run commands such asdotnet fable ...
seemed to requiredotnet restore
. - Sometimes frustrating errors come up in the CLI usage of paket combined with the dotnet core sdk, which seems to be due to recent features in the user addable clitools (e.g.
dotnet-fable
) and the core sdk being new. The templates often use a fixed version ofpaket
for reproducibility, but when combined with custom additions using new core sdk cli tooling featurespaket
may need updating (which basically means nuking the.paket/
directory contents and downloading a new paket bootstrapper) - Some dotnet core project templates create a
Root.fsproj
orBase.fsproj
which confusingly has no C#/F# files and nothing in thepaket.references
except aclitool
e.gdotnet-fable
. This is so the tool can be run in the root directory without having tocd
intosrc
, for example. - Fable is pretty lightweight and doesn't hide much of the Javascript ecosystem, you will have to familiarise yourself with
yarn
(faster version of and compatible withnpm
) and installingnodejs
. Hopefully the project templates take care ofwebpack
setup. - Focus on just compiling fsharp to javascript and using javascript ecosystem tools as that is where the code will ultimately run. E.g. use Jest for compiled javascript testing and the Fable-Jest bindings. There is unlikely to be a super-polished test runner written in F# that is meant to work with JS compiled code.
- If you want to host your own fsharp library artifact repository (a PyPI instance equivalent), including libraries that are just bindings between f# and javascript (fable bindings) then you probably need your own nuget installation, though it is possible to make Paket checkout a git repository and run some build command that will produce the
nupkg
file (similar to pythonegg
files which are just zipped up libraries).
On windows it will be:
.paket/paket.exe ...
On linux we have to run it with mono:
mono .paket/paket.exe ...
-
add the dependency to
paket.dependencies
(similar to Pipfile in Python). -
add a reference to the
paket.references
file for all projects that need to use the depedency -
Run the
paket install
command You can also use paket add to encompass these steps. -
cd to project directory
-
dotnet restore
- is this needed? This might not be needed all the time, I think some of the dotnet cli tools require it though. Certainly the IDE/editor like VSCode seems to be clueless until this is run.
Assuming you have nodejs installed and you have installed the JS modules for your project with Yarn or NPM then you can
run node
at the root of your project in a terminal/shell. Libraries that you have "installed"/fetched for use in your project (like pipenv install
for Python projects) appear in the node_modules
directory. From the node repl you can import things as normal e.g. var Swagger = require('swagger-client')
will import the swagger-client
library in node_modules/swagger-client
.
You can use the F# repl (fsi
/fsharpi
) when developing and working with F# code. Where using a JS binding F# library there is little utility beyond type signatures in the library though.
After F# code is compiled to JS you can use the repl/console in the browser along with browser source maps to experiment.
After making Paket download your dependencies for use in a project, you will find the libraries in one or both of:
./packages
in your project directory (if theStorage: NONE
directive was not given to Paket)~/.nuget/packages
which is the user nuget cache
Your editor will likely have something like VSCode's "Ionide: add to project" to add an f# file to an fsproj project. In F# the compile order matters, so you will probably need to re-order the file in the project (which Ionide can also do). F# cyclic dependency avoidance.
Fable.Import.JS.Promise<_>
which is used in F# libraries such as Fable PowerPack
is just a binding to JS Promises. How do we use it Elm without blocking the UI?
In Elmish a "Task is any code that will execute when a command is evalutated, like reading a database, defined in Elmish as async or promise blocks or just a plain function." These operations may return immediately but complete (or fail) at some later time. To feed the results back into the dispatch loop, instead of executing the operations directly, we instruct Elmish to do it for us by wrapping the instruction in a command.
So, see Elmish Cmd.ofPromise. Pay close attention to the type signatures - most of the parameters are functions e.g:
let costDesignTask = (fun _ ->
let costParams = createObj [
"design_uuid" ==> "a3f2abec-12c0-4a13-bd79-95764b088218"
"rate_card" ==> "complete-utilities-fastershire"
"rate_card_type" ==> "nec-v1"
]
!! jsSwaggerClient?apis?("default")?get_cost(costParams)
)
let costDesignSuccessF: (obj -> Services.Types.Msg) = // function to call on success taking the response and returning a message
let costDesignErrF: (obj -> Services.Types.Msg) = // function to call on error taking the response and returning a message
let costDesignCmd = Cmd.ofPromise costDesignTask () costDesignSuccessF costDesignErrF
The 2nd parameter to Cmd.ofPromise is the argument to costDesignTask, which is a function taking one argument. In this case the argument is unused.
Assuming you can access the cookies in normal JS with document.cookie
, which requires cookies to be visible to JS by
not being an 'httpOnly' cookie, you can pull out a cookie from the browser using this function (the normal browser API
for cookies is painfully primitive):
open Fable.Import.Browser
module FableBrowser = Fable.Import.Browser
let findCookieValue (name: string): string option =
let kvArrToPair (kvArr: string []): string * string =
match kvArr with
| [|k; v|] -> (k, v)
| _ -> ("", "")
let rawCookies: string = FableBrowser.document.cookie
rawCookies.Split ';'
|> Array.map (fun (s: string) -> s.Trim().Split '=' |> kvArrToPair)
|> Map.ofArray
|> Map.tryFind name
Fable logic/libraries code can possibly be tested with F# library code, but testing with JS libs under the hood makes sense in terms of where we will run the code. JS Test Runners have "better" reporters in the browser etc. with auto reloading.
These are some F# test (runner) libraries that have been used to test generated Javascript code:
- The Fable.NUnit plugin converts expecto code into mocha/jest. Seems inflexible.
- Fable.XUnit is can use expecto or Jest/Mocha.
Given the state of testing in pure F#/Fable for generated JS code (Open Issue to create Test Runner F# to JS Tests) it seems better to stick with mature well used libraries from the Javascript ecosystem and use bindings to them if they exist, at least for now.
Some F# templates such as the Fable-Suave SAFE stack example do use
F# code (e.g. Expecto
and canopy
) for testing the UI code. It at least makes more sense there because there is
server/API code written in F# and running on dotnet core aswell as F# Fable code UI code that compiles to Javascript. It is reducing the number of test tools involved even if the JS test runners would be "better" for the JS code.
The Fable-Elmish-React template does not come with a Test project. I have manually integrated Fable-Jest to use Jest for testing - which is IMO the current defacto testing library for Javascript (2018 overview). There is a fable jest template but you have to manually take the relevant bits of it and put it into the Fable-Elmish-React template.
For pure UI testing, which means testing as though you are a user clicking on elements in the browser we don't need
to use anything specific to Javascript or F#. This sort of testing never looks at the library/source code for a WebUI.
There is Canopy which is a layer over Selenium Webdriver
made in F# and here is a Canopy start page. Note it is different to canopyjs
which is some Javascript parsing library.
Testers writing UI automation tests probably have a different language they want to use. Selenium/Selenium Webdriver is a very mature project that a lot of QA people have experience with and can be tested with Python, C#, Java and lots of languages. It controls the entire browser application on the desktop and doesn't just run "inside" the browser.
TestCafe injects itself into JS instead of controlling the browsers like selenium does, so it can work on mobile devices. Languages used are JS or Typescript. It seems to be the best alternative to Selenium, is new and has some useful distinguishing features like strong async support. As it injects itself into the page Javascript it's not good for testing page transitions (from what I could tell, though the docs may suggest otherwise).
These notes probably need more verification. They should cover the manual alterations to add Jest testing to the Fable-Elmish-React template project.
- Change the
package.json
"scripts":"test" to call"jest"
- Optionally add extra "scripts": "coverage": "jest --coverage", "test-watch": "jest --watchAll"
- Install npm packages
jest
andjest-fable-preprocessor
+babel-preset-env
and save in package.json devdependenciesyarn add jest jest-fable-preprocessor babel-preset-env --dev
- Add to the the
paket.dependencies
file if missing these dependencies:
clitool dotnet-fable
nuget Fable.Core
nuget Fable.Import.Browser
nuget Fable.Import.Jest
Doing one of those with paket
commands: [mono] .paket/paket.exe add Fable.Import.Jest
Then add to the fsproj as a reference.
- Add new test project (probably required) and reference the main project you are testing
dotnet new classlib --language "F#" --output tests --name Tests
dotnet sln add tests/Tests.fsproj
dotnet add tests/Tests.fsproj reference src/Blah.fsproj
-
add starter test file
touch tests/TestMain.fs
...and then add it to the test fsproj -
add paket references to a new
tests/paket.references
file dotnet-fable Fable.Core Fable.Import.Jest ... -
add
.babelrc
file to the root{ "presets": [ [ "env", { "targets": { "node": "current" } } ] ] }
-
add jest config file
jest.config.js
Beware JSDom bug: jestjs/jest#6766 which meant we added to the config filetestURL: 'http://localhost'
Here is th entire file
jest.config.js
:module.exports = { moduleFileExtensions: ['js', 'fs'], transform: { '^.+\\.(fs)$': 'jest-fable-preprocessor', '^.+\\.js$': 'jest-fable-preprocessor/source/babel-jest.js' }, testMatch: ['**/**/*.(Test.fs)'], coveragePathIgnorePatterns: ['/packages/', 'test/'], // Workaround for JSDom bug https://github.com/facebook/jest/issues/6766 testURL: 'http://localhost' };
-
Ensure we have a top level project - Jest seems to need a root project(?)
dotnet new classlib --language "F#" --output . --name Root
It has a
paket.references
file in the root folder todotnet-fable
. Note, no other implementation or test project references the root project and the root project does not have any source files. The root project exists to make runningdotnet-fable
from the project root possible (presumably related to howjest
wants to run). -
Add reference from root project to test project
dotnet add Root.fsproj reference tests/Tests.fsproj
Comparison with Elm lang
Probably got some of this wrong.
- Elm is more pure-functional - pure until the JS ports boundary. F# can do mutation and classes without effort: CPU performance vs safety trade-offs.
- Elm has a property based testing library. F# libraries exist but do they work with fable? Or do we add the complication of running some tests with dotnet tools and not JS tools?
- F# Minimal fable-powerpack Result handling library. Elm does Result errorhandling style but nothing like computation expressions.
- Editors: Ellie for Elm. Visual studio code for F# and Elm. Visual Studio for F#.
- F# easy parallel with JS promises, no ports challenges.
- F# computation expressions powerful vs simpler throughout Elm type system. Both have strong types though.
- Elm quicker to learn for newbies, one way to do most everything
- Elm tooling is simpler that dotnet ecosystem
open System.Reflection;; let a = Assembly.LoadFrom "/home/someone/whatever/packages/example/lib/net45/IO.Swagger.dll";; let types = a.GetExportedTypes();;
Bulma is a decent responsive CSS framework (think Bootstrap or Foundation for comparison).
Look at Fulma for documentation on F# bindings for this CSS library.
React Developer Tools is worth a look at if using React, such as Fable-Elmish-React.