Skip to content

Instantly share code, notes, and snippets.

@enerqi
Last active April 11, 2023 14:41
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save enerqi/b941b953f89f3ac91dddff81b932f2ea to your computer and use it in GitHub Desktop.
Save enerqi/b941b953f89f3ac91dddff81b932f2ea to your computer and use it in GitHub Desktop.
Fable F# Notes

Basic Tooling Setup

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.

F# 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.

F# Windows

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

Javascript Tooling

  • NodeJs - get a recent stable version
  • Yarn - get a recent stable version.

JS Interop Basics

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.

Construct

JS

var blah = new Swagger(swaggerParams)

F#

let blah = createNew Swagger swaggerParams

Note

createNew Swagger?swaggerParams

is not what we want === new Swagger.swaggerParams

Casting Dynamic JS

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

Create Plain Json

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.

Showing JS Data in F# code

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 APIs

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.

Using JS properties with attribute names that are F# keywords

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()

Importing Dynamic Untyped JS Libraries

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

Js Dynamic Typing Docs

Debugging

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

What can be compiled to JS via Fable?

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.

F# libraries that work with Fable

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

What libraries are batteries included with Fable?

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.

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

Notable 3rd Party Libraries

How to use JS libraries in a type secure way?

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!

Library Use Conflicts

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")

Build/Project/Skeleton Tooling

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.

JS Tooling

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.

Running JS "build" commands

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.

Adding new JS libraries

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.

F# Tooling (dotnet)

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 via paket.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 python tests 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 and paket. For the most part when using dotnet core the dotnet cli should correctly delegate dependency management to paket. However, (I could have messed something up), restoring and making available on the CLI a clitool such as dotnet-fable so one can run commands such as dotnet fable ... seemed to require dotnet 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 of paket for reproducibility, but when combined with custom additions using new core sdk cli tooling features paket 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 or Base.fsproj which confusingly has no C#/F# files and nothing in the paket.references except a clitool e.g dotnet-fable. This is so the tool can be run in the root directory without having to cd into src, 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 with npm) and installing nodejs. Hopefully the project templates take care of webpack 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 python egg files which are just zipped up libraries).

Running paket

On windows it will be:

.paket/paket.exe ...

On linux we have to run it with mono:

mono .paket/paket.exe ...

Adding new F# libraries

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

REPL experimentation with JS Libraries

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.

F# REPL

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.

Finding dotnet DLLs to import and play with at the REPL

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 the Storage: NONE directive was not given to Paket)
  • ~/.nuget/packages which is the user nuget cache

Adding new F# files to a project

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.

Elmish (F# Elm Architecture)

How are Promises (fairly common async tool in JS libraries) used with Elmish?

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.

Cookie access

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

Testing

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

Adding Jest testing to the Project

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 and jest-fable-preprocessor + babel-preset-env and save in package.json devdependencies yarn 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 file testURL: '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 to dotnet-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 running dotnet-fable from the project root possible (presumably related to how jest wants to run).

  • Add reference from root project to test project dotnet add Root.fsproj reference tests/Tests.fsproj

Random Links

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

Dll introspection

Introspect exported types

open System.Reflection;; let a = Assembly.LoadFrom "/home/someone/whatever/packages/example/lib/net45/IO.Swagger.dll";; let types = a.GetExportedTypes();;

Fulma/Bulma

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 Devtools Chrome Browser Plugin

React Developer Tools is worth a look at if using React, such as Fable-Elmish-React.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment