Interactive code runner with FSCS and SAFE stack

December 15, 2021

This is an entry for the F# Advent Calendar 2021, please consider checking out some of the other awesome blog posts.

FSharp.Compiler.Services is a nuget package that contains tools for implementing F# language bindings. It is mostly used for developer tools but also contains APIs for dynamic execution of F# code.

SAFE Stack is a dotnet new template that allows web applications to be written in F# and run on the server and browser. Server side code runs with the dotnet runtime and client side code is transpiled into Javascript with fable.

Interactive code runners like try.dot.net are an easy way to start learning a programming language. They speed up the process by executing code in a web browser, removing the need to install any software onto your computer. Combining FSCS and the SAFE stack we can easily create a web application to interactively run F# code.


FSharp.Compiler.Services

There are two main ways to run dynamic F# code with the package, either using the fsc executable to compile the code and import the created .dll file, or creating an F# interactive session with the fsi executable and passing it commands to evaluate.

The code runner app is going to use the fsi executable on the server and pass results back to the browser. The documentation describes how to create an interactive session with FSCS and the dotnet executable can be used instead by prefixing the initial arguments with fsi.

To use the FSCS library, add a package reference in the .fsproj file:

<PackageReference Include="FSharp.Compiler.Service" Version="40.0.0" />

An fsi evaluation session needs a TextReader and couple of TextWriter objects for the input and output streams. These can be encapsulated in a FsiExpressionEvaluator type:

open System
open System.IO
open System.Text
open FSharp.Compiler.Interactive.Shell

type FsiExpressionEvaluator() =
    // Initialize output and input streams
    let sbOut = new StringBuilder()
    let sbErr = new StringBuilder()
    let inStream = new StringReader("")
    let outStream = new StringWriter(sbOut)
    let errStream = new StringWriter(sbErr)
    ...

Still within the type, create an fsi session with FsiEvaluationSession.Create(), the streams, default configuration and other arguments (fsi, --noninteractive):

    // differs across platforms
    let dotnetLocation = Environment.GetEnvironmentVariable("DOTNET_ROOT")

    // Build command line arguments & start FSI session
    let allArgs = [| dotnetLocation; "fsi"; "--noninteractive" |]
    let fsiConfig = FsiEvaluationSession.GetDefaultConfiguration()
    
    let fsiSession =
        FsiEvaluationSession.Create(fsiConfig, allArgs, inStream, outStream, errStream)
    ...

With the session initialised, add an Evaluate method that takes a string expression and returns a Result<string, string> of either the compilation error(s) or evaluation result.

    member this.Evaluate(expression: string) =
        let result, warnings = fsiSession.EvalExpressionNonThrowing(expression)
        match result with
        // successful execution
        | Choice1Of2 valueOrUnit ->
            let expressionValue = 
                match valueOrUnit with
                // return value as string
                | Some fsharpValue -> sprintf "%A" fsharpValue.ReflectionValue
                // unit, return blank string
                | None -> ""
            Ok expressionValue
        // execution failed
        | Choice2Of2 _ ->
            // join list of error messages into string
            let messages =
                warnings
                |> Array.map (fun x -> x.Message)
                |> fun arr -> String.Join(", ", arr)

            sprintf "Failed: %s" messages |> Error

The FsiExpressionEvaluator type allows the execution of F# code dynamically via an fsi session. The code runner app will use this type to evaluate user generated expressions and display the output or compiler errors.


Web app

Creating the web application involves installing the SAFE stack project template and creating a new project. This creates the files needed to build the Client and Server components with the application code files stored in the src directory.

FableTree

The template creates three F# projects for the Shared, Server and Client code with the latter two depending on the Shared.fsproj. The project is setup to build the server code with dotnet, transpile the F# to Javascript with fable and bundle the client side code with webpack.

ProjectStructure

Shared code

The shared code project is a dependency of both the client and server projects and can be used for shared types and validation code. Client-Server communication is achieved with the Fable.Remoting RPC library with the route and contract defined in the shared project and the library providing helper functions to implement the client and server setup.

The client sends an expression in the form of a string and the server either returns the result of the expression if the code evaluates, or returns the compiler errors if the code does not compile. The Shared.fs file shows the required code to achieve this in an eval procedure:

namespace Shared

module Route =
    let builder typeName methodName = sprintf "/api/%s/%s" typeName methodName

// type aliases for clarity/documentation
type Expression = string
type EvalValue = string
type EvalError = string
type EvalResult = Result<EvalValue, EvalError>

// used by Fable.Remoting to define communication
type IRunnerApi =
    {
        eval: Expression -> Async<EvalResult>
    }

Client

The client side browser application is written in F#. The code is transpiled into Javascript as part of the build process using Fable. The SAFE stack template includes a simple application written in the MVU architectural pattern using the Elmish library which can be tweaked to make the code runner.

MVU stands for Model View Update, the model contains all of the possible states of the application as a data structure. The view is a function that takes the model as an argument and returns the UI to be rendered to the user who can then perform actions that dispatch messages. update is a function that takes messages from the UI and returns an updated model to be rendered.

mvu pattern

The app needs to store the expression to be evaluated and the response from the api and will encounter three different events: changing the expression text, run the expression and update the model from the response of the api. These can be codified with:

open Shared

type Model = {
    Expression: string
    Response: EvalResult
}

type Msg =
    // Run button is pressed
    | EvaluateExpression
    // Expression text is changed
    | UpdateExpression of Expression
    // Received a response from api
    | GotResponse of EvalResult

The Model and Msg are used by the view and update functions and specify the possible application states and events. The update function takes the Msg and current Model and returns a tuple of the new Model and a command object for Elmish to run.

open Fable.Remoting.Client

// use Fable.Remoting and Shared.fs code to create object to call api
let runnerApi =
    Remoting.createApi ()
    |> Remoting.withRouteBuilder Route.builder
    |> Remoting.buildProxy<IRunnerApi>

let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    match msg with
    | EvaluateExpression ->
        // create command to run after update function executed
        let evaluateExpressionOnServer =
            Cmd.OfAsync.perform  // create command to perform:
                runnerApi.eval   //   call runner api eval function
                model.Expression //   with currently editing Expression from model
                GotResponse      //   create GotResponse message from eval return  
        model, evaluateExpressionOnServer
    | GotResponse res ->
        // update the model with api response
        { model with Response = res }, Cmd.none
    | UpdateExpression expression ->
        // text area changed, update the model with new value
        { model with Expression = expression }, Cmd.none

The Msg type allows the compiler to check the completeness of the pattern matching on the msg value to ensure that all of the events are handled.

The view function takes the Model and a function to dispatch new Msg objects from the UI. The return value is a reactjs component that is defined with the Feliz library, and Feliz.Bulma is used to create Bulma UI components.

The app needs to display the expression, a “Run” button and the result from eval in a box that is either coloured red or black depending on response.

// model is type Model, dispatch is function (Msg -> unit)

let resultBox =
    // text and text colour from the EvalResult
    let (result, colour) =
        match model.Response with
        | Ok ok -> ok, "black"
        | Error err -> err, "red"

    // add a p element with border
    Html.div [
        prop.style [
            style.padding 10
            style.border (1, borderStyle.double, colour)
            style.borderRadius 5
        ]
        prop.children [
            Html.p [
                prop.style [ style.color colour ]
                prop.text result
            ]
        ]
    ]

// editable textarea that dispatches UpdateExpression message when changed
let expressionTextArea =
    Bulma.textarea [
        prop.value model.Expression
        prop.onChange (fun x -> dispatch (UpdateExpression x))
    ]

// button that sends expression to api when clicked
let runButton =
    Bulma.button.a [
        color.isPrimary
        prop.onClick (fun _ -> dispatch EvaluateExpression)
        prop.text "Run"
    ]

The values resultBox, expressionTextArea and runButton are F# values of type ReactElement and can be put within other layout components:

View

Server side

The SAFE template uses the Saturn framework to create the API. Saturn is built on top of ASP.NET Core and provides useful abstractions and utilities for creating modern web APIs.

The server side code needs to create a record of type IRunbookApi and then Fable.Remoting can generate an app for Saturn to run.

let evaluator = FsiExpressionEvaluator()

open Shared

// implement shared api contract
let runnerApi: IRunnerApi =
    { 
        eval = fun expression ->
            async {
                return evaluator.Evaluate expression
            }
    }

open Fable.Remoting.Server
open Fable.Remoting.Giraffe

// create Fable.Remoting api from IRunnerApi and shared route builder
let fableRemotingApi =
    Remoting.createApi ()
    |> Remoting.withRouteBuilder Route.builder
    |> Remoting.fromValue runnerApi
    |> Remoting.buildHttpHandler

open Saturn

// create saturn app and run
let app =
    application {
        url "http://0.0.0.0:8085"
        use_router fableRemotingApi
        memory_cache
        use_static "public"
        use_gzip
    }

run app

With the server setup it’s possible to see Fable.Remoting in action and the request and response if the expression 1 + 1 is evaluated:

ApiOk

And a compiler error if the expression 1 + "1" is sent:

ApiError

Conclusion

This post has shown how to use FSCS and SAFE stack to create an interactive code runner web app. The code can be seen on my github for anyone who is interested.

Using an interactive session with fsi is one of the ways to use FSCS, but it is probably not the best solution for embedding user defined code in an application. There are a couple of blog posts (here and here) that describe how to use fsc to run dynamic F# code in an application which might be more appropriate.

The SAFE Stack is a great combination of technologies that reduces the time that it takes to start a new project. It’s very useful to be able to use the same code on the client and server and also check the application correctness with the F# compiler.


Profile picture

Website and blog of Chester Burbidge