Differentiating Web Assembly with F#

Explore how F# differentiates WebAssembly development. Compare WebSharper, FunScript, and Fable for browser-based F# programming.

8 min read 1,430 words

This post is part of F# Advent calendar. Thanks Sergey Tihon for arranging this and giving me the chance.

JavaScript is a different kind of beast when it comes to a programming language. It is easy to learn but too hard to master. I still get nightmares from interview questions about JavaScript code. One needs to select the correct output for a given code snippet.

For all those reasons, I always go for languages that transpile to JavaScript. Be it CoffeeScript, TypeScript, FunScript, Fable etc. They all provide different types of features to deal with JavaScript issues.

As this article is part of FSAdvent 2020, we will talk about F# and its relationship with JavaScript or web programming in general. It is mostly about executing F# in your favorite browser (mostly all latest browsers - IE obviously).

There have been multiple attempts to make F# work in browsers. WebSharper and FunScript were a few of the first attempts to run F# on web browsers. Both transpile to JavaScript frameworks.

WebSharper is still around, under MIT license. It supports C# and F# as programming languages. WebSharper also supports full stack C# / F# development.

FunScript is not actively maintained. But we have another choice called Fable, a logical successor of FunScript. Fable is also an F# to JavaScript tool. You can do full stack development with Fable as well, with the support of some friends from the zoo. (Read Giraffe and Zebras here.)

The world is pretty much balanced. We have two good choices to write kind of error-free code in F# for front end development if I haven't missed any. I like to put a disclaimer here: I am an active maintainer of the awesome fable project.

There is one more choice, FSBolero. It is an F# wrapper around Blazor. Blazor is a Web Assembly framework to run .NET in a web browser. If we go to the FSBolero docs, everything is pretty much similar. We can write F# on a client, we can have a full stack application in F# as well. We can run mix mode using SignalR. Let's not go into the details about that. MSFT people are already trying hard to market Blazor + SignalR.

So, the question arises: where does one use FSBolero? It has Elmish, so does Fable. It can run F# on a client, so does Fable. So, why bother?

FSBolero comes with the loaded power of Web Assembly. If we are making a typical Elmish application with it, then we might not be justifying all the power we get. There is a whole Unreal Engine that works in the web browser with the help of Web Assembly. No, I'm not going to run anything unreal with it.

But we can surely try some mathematics in the browser. Something that JavaScript can't do or can't do in a performant way. I am learning PyTorch in my free time. So, my first attempt was to run TorchSharp in FSBolero. But sadly I couldn't even run it on my machine, in a full project. Thanks to something called native dependencies. I was a little disappointed that even this time I have to write about my failed attempt. But but but there is one more little-known framework there. Something called DiffSharp. DiffSharp is a tensor library with advanced support for differentiable programming.

Guess what, I did manage to make it work with FSBolero. Yes, differentiation working in a browser. Here is the project, if you'd like to give it a try. Below is a code snippet.

type Model = { currentTensor: Tensor }

let initModel = { currentTensor = Tensor.Zero }

type Message =
    | Zero
    | One
    | Scalar
    | Vector

let init arg =
    let t1 = Tensor.Zero
    { currentTensor = t1 }

let update message model =
    match message with
    | Zero ->
        { model with
              currentTensor = Tensor.Zero }
    | One ->
        { model with
              currentTensor = Tensor.One }
    | Scalar ->
        { model with
              currentTensor = dsharp.tensor 1.2 }
    | Vector ->
        { model with
              currentTensor = (dsharp.tensor [ 0.0; 0.3; 0.1 ]) }

// Define a scalar-to-scalar function
let f (x: Tensor) = sin (sqrt x)

// Get its derivative
let df = dsharp.diff f

// Now define a vector-to-scalar function
let g (x: Tensor) = exp (x.[0] * x.[1]) + x.[2]

// Now compute the gradient of g
let gg = dsharp.grad g

// Compute the hessian of g
let hg = dsharp.hessian g

let view (model: Model) dispatch =
    div [ attr.classes [ "container" ] ] [
        div [ attr.classes [ "columns" ] ] [
            div [ attr.classes [ "column" ] ] []
            div [ attr.classes [ "column" ] ] [
                text (sprintf "Current Tensor %A, and Shape of it %A" model.currentTensor model.currentTensor.shape)
            ]
            div [ attr.classes [ "column" ] ] []
        ]

        if (model.currentTensor.shape.Length = 0) then
            div [ attr.classes [ "columns" ] ] [
                div [ attr.classes [ "column" ] ] []
                div [ attr.classes [ "column" ] ] [
                    text (sprintf "sin (sqrt x) of scalar: %A" (f (model.currentTensor)))
                ]
                div [ attr.classes [ "column" ] ] []
            ]

            div [ attr.classes [ "columns" ] ] [
                div [ attr.classes [ "column" ] ] []
                div [ attr.classes [ "column" ] ] [
                    text (sprintf "diff of sin (sqrt x) scalar: %A" (df (model.currentTensor)))
                ]
                div [ attr.classes [ "column" ] ] []
            ]

        if (model.currentTensor.shape.Length = 1) then
            div [ attr.classes [ "columns" ] ] [
                div [ attr.classes [ "column" ] ] []
                div [ attr.classes [ "column" ] ] [
                    text (sprintf "exp (x.[0] * x.[1]) + x.[2] vector: %A" (g (model.currentTensor)))
                ]
                div [ attr.classes [ "column" ] ] []
            ]

            div [ attr.classes [ "columns" ] ] [
                div [ attr.classes [ "column" ] ] []
                div [ attr.classes [ "column" ] ] [
                    text (sprintf "gradient of exp (x.[0] * x.[1]) + x.[2] vector: %A" (gg (model.currentTensor)))
                ]
                div [ attr.classes [ "column" ] ] []
            ]

            div [ attr.classes [ "columns" ] ] [
                div [ attr.classes [ "column" ] ] []
                div [ attr.classes [ "column" ] ] [
                    text (sprintf "hessian of exp (x.[0] * x.[1]) + x.[2] vector: %A" (hg (model.currentTensor)))
                ]
                div [ attr.classes [ "column" ] ] []
            ]

        div [ attr.classes [ "columns" ] ] [
            div [ attr.classes [ "column" ] ] []
            div [ attr.classes [ "column" ] ] [
                div [ attr.classes [ "columns" ] ] [
                    div [ attr.classes [ "column" ] ] [
                        button [ attr.classes [ "button" ]
                                 on.click (fun _ -> Zero |> dispatch) ] [
                            text "Zero"
                        ]
                    ]
                    div [ attr.classes [ "column" ] ] [
                        button [ attr.classes [ "button" ]
                                 on.click (fun _ -> One |> dispatch) ] [
                            text "One"
                        ]
                    ]
                    div [ attr.classes [ "column" ] ] [
                        button [ attr.classes [ "button" ]
                                 on.click (fun _ -> Scalar |> dispatch) ] [
                            text "Scalar 1.2"
                        ]
                    ]

                    div [ attr.classes [ "column" ] ] [
                        button [ attr.classes [ "button" ]
                                 on.click (fun _ -> Vector |> dispatch) ] [
                            text "Vector [ 0.0; 0.3; 0.1 ]"
                        ]
                    ]
                ]
            ]
            div [ attr.classes [ "column" ] ] []
        ]
    ]

Nothing fancy in there. Just took the example of DiffSharp and put it out there with a little bit of Elmish on top.

It might seem boring but possibilities are infinite. I really love to have TorchSharp working with FSBolero. So, one can train machine learning and neural network models on the client-side. Prediction can also happen on the client-side as well.

Why? The dumb answer is for fun and profit. (Hi, Sir Scott Wlaschin). But we are having a good amount of computing power on the client-side as well. We can push a good amount of calculations there as well. Also, sometimes we can't save data on a server, but if we still like to provide an ML experience in a client application, then it will be possible with Web Assembly.

There is one downside of Web Assembly in general: the first time load of the application. But you can easily use mix mode or serve the client application wrapped in Electron.js, so one doesn't have to download all the load.

There is surely less F# code here compared to my other articles. But I hope you don't mind a little bit more mathematical fun instead.

PS: Any math expert like to push Web Assembly to its boundary? Please do give a PR with your math example. Happy to have some crazy examples.

Frequently Asked Questions

What are the main options for running F# in web browsers?

The primary options are Fable (an F# to JavaScript transpiler), WebSharper (which supports both C# and F# transpilation), and FSBolero (an F# wrapper around Blazor that uses WebAssembly). Each provides different approaches to front-end development, with Fable being the most actively maintained successor to the older FunScript framework.

How does FSBolero differ from Fable for web development?

While both support full-stack F# development and include Elmish architecture, FSBolero leverages WebAssembly to run compiled .NET code directly in browsers, providing access to more computational power. Fable transpiles F# to JavaScript, which is lighter weight but with different performance characteristics. FSBolero is better suited for computationally intensive applications, while Fable excels for typical web applications.

Why would developers choose to transpile to JavaScript instead of using plain JavaScript?

Languages like F#, TypeScript, and CoffeeScript transpile to JavaScript to avoid JavaScript's complexity and potential pitfalls, making code more type-safe and maintainable. These alternatives provide features that help prevent common JavaScript errors and make code easier to understand and debug compared to writing raw JavaScript.

What is WebAssembly and why does it matter for F# web development?

WebAssembly (W) is a low-level bytecode format that allows compiled code to run efficiently in web browsers. FSBolero uses WebAssembly to execute compiled .NET/F# code directly in browsers, unlocking access to the full computational power of the .NET framework for applications that need intense processing capabilities.

Can you build full-stack applications with F# for web development?

Yes, both Fable and FSBolero support full-stack F# development. Fable works with backend frameworks like Giraffe, while FSBolero integrates with Blazor and can use SignalR for real-time communication. This allows developers to use F# for both frontend and backend components of their web applications.

Share this article