Hackathon Superweapon: F# and Fabulous

Larry O'Brien

Recently, I participated in Hack for the Sea, a weekend devoted to applying tech to marine conservation. One of our local challenges was a “cross-platform mobile app for reporting marine debris.” A perfect opportunity to explore the Fabulous  project, which brings the Elmish Model-View-Update (MVU) architecture to Xamarin and F#.

Hackathons are great short and intense challenges that (hopefully) turn a concept into, if not a complete prototype, at least an initial iteration. The camaraderie, short deadline, and free pizza make hackathons an ideal place to explore new technologies.

F# has become a favorite programming language for production work. It has a combination of concise syntax for writing new code and strong types for working with older or unfamiliar code. However, would it be up for the controlled chaos of a hackathon?

MVU Architecture

The MVU architecture is an increasingly popular architecture that combines “UI in code,” with a simple-to-use separation between:

  • A view function which describes the user-interface appropriate to the current state of the application.
  • An update function that modifies that state.

MVU is not specific to any language, but it fits well with the functional programming mindset. Both view and update are expected to receive all the information they need from their arguments rather than reading and maintaining instance data.

The MVU architecture and F#’s concise syntax allow one to rapidly create a reporting app that is very similar to a lot of Line of Business (LOB) apps. The app has a combination of data-entry pages and device-generated data (default location and time info, photos, etc.).

Creating Xamarin.Forms UI in Code

Creating a Xamarin.Forms UI in code is very straightforward. Define each complex element or page in a local function. A simple one like header just contains a static label. A more complex one like locationPage can have a message depending on the value (or non-existence) of model.Report.Location. As you can see, using the powerful FlexLayout capability of Xamarin.Forms defines a user interface that lays out properly on any sized device:

let view model dispatch =

    let header = 
        View.Label(text = "Report Marine Debris", fontSize = 24, horizontalTextAlignment = TextAlignment.Center)

    let locationPage = 
        let locMsg = match model.Report |> Option.bind (fun r -> r.Location) with
                     | Some loc -> sprintf "Location: %.3f, %.3f" loc.Latitude loc.Longitude
                     | None -> "Location Unknown"

        View.FlexLayout(direction = FlexDirection.Column, alignItems = FlexAlignItems.Center, justifyContent = FlexJustify.SpaceEvenly, 
            children = [
                View.Map(requestedRegion = model.MapRegion, minimumWidthRequest = 200.0, widthRequest = 400.0) |> flexGrow 1.0
                View.Label(text = locMsg, verticalTextAlignment = TextAlignment.Center) |> flexGrow 1.0
            ])

    // ... more building up of elements and pages ... 

    View.ContentPage(
         content = View.FlexLayout(direction = FlexDirection.Column, alignItems = FlexAlignItems.Center, justifyContent = FlexJustify.SpaceEvenly, 
            children = [
                header
                content
                footer
            ]))

F# reads quite a bit like Python (quite often when writing Python I accidentally type an F#-ism!). One thing possibly surprising to people who’ve heard of “strongly typed functional languages” is the lack of explicit type declarations. While developers may  add explicit declarations, mostly you rely on the compiler to correctly infer the type and use IntelliSense to give you the precise signature. Other things worth pointing out :

  • In F# “everything is an expression.” Both functions and values are declared using let;
  • The |> operator is similar to a Unix or PowerShell pipe. It passes the left-hand side value to the right-hand side as an argument;
  • The match ... with ... pattern-matching syntax that generates a compiler error if you miss a case;
  • The function Option.bind which is something like the null-conditional operator.

Using F# to Write a Function

Once you learn the basics of F#, it’s very readable and concise. Since local functions are trivial to write, you end up refactoring the boilerplate code into local functions. For instance, this “progress panel” uses different icons to indicate whether or not the user has entered a particular type of data (e.g., “what_some.png” vs “what_none.png”). Rather than write a bunch of near-identical if...then... blocks, it’s natural in F# to write a function such as imageFor that checks if the data has a particular field (the Some case). Then it returns the results of the check and the name of the particular icon to load :

let imageFor f imagePrefix =
    match model.Report |> Option.bind f with
    | Some _ -> (true, sprintf "%s_some.png" imagePrefix)
    | None -> (false, sprintf "%s_none.png" imagePrefix)

let (hasLoc, locImage) = imageFor (fun r -> r.Location) "where"
let (hasDebris, debrisImage) = imageFor (fun r -> r.Material) "what"
// ... etc. for each type of data and icon

 

Updating to Update

To be honest, I love the Model-View-Controller architecture. However, MVU has some clear advantages, particularly in the earliest iterations of a project. Also, it has the potential for “time-travel debugging” in which you can “run the program backward and forwards” rather than just freezing at a breakpoint.

Key to MVU is the update method, which has a signature Msg -> Model -> Model * Cmd<Msg> (which would be expressed in C# as Func<Msg,Model,Tuple<Model,Cmd<Msg>>). This shows the common functional pattern of “Take a request (Msg) and an argument representing the state (Model). Act on it, and then return a new version of the state with a new request to tackle the next step in responding to the input.”

For instance, when the GPS gets a reading, the handler creates a LocationFound Msg with a value of type Xamarin.Essentials.Location. In response, the update method has this snippet :

| LocationFound loc ->
    let report = { oldReport with Location = Some loc }
    let newRegion = new MapSpan(new Position(loc.Latitude, loc.Longitude), 0.025, 0.025)
    { model with MapRegion = newRegion; Report = Some report }, Cmd.none

 

Reports

A new report (the data that is ultimately uploaded to Azure) is created by copying the oldReport with the new Location value. I then create a new model that contains both my new report and the MapRegion value used in the view method as discussed previously.

And that’s it! The handler for LocationFound is actually the longest one in the update function. If a message requires complex handling, handling it should be done in a separate function. This is particularly nice for async processing, as you can see in the below snippet, that stores the photo to an Azure blob and the data to table storage:

let SubmitReportAsync reportOption =
    async {
        match reportOption with
        | Some report ->
            let photoName = sprintf "%s.jpg" ( report.Timestamp.ToString("o") )
            let csa = CloudStorageAccount.Parse connectionString
            let! photoResult = photoSubmissionAsync csa "image/jpeg" report.Photo photoName
            let! submissionResult = reportSubmissionAsync csa report photoResult
            return SubmissionResult submissionResult
        | None ->
            return Error "Choose data to make report"
} |> Cmd.ofAsyncMsg

 

Unlike the synchronous LocationFound handler, this function does some asynchronous work and then fires off a new Cmd with a Msg that’s either an Error message or a SubmissionResult message. Rather than a function that tries to do all the business of coordinating async network requests, displaying the results or errors, etc., the MVU architecture facilitates creating clear, discrete single-task functions. In a rapidly-iterating situation such as a hackathon, this is a blast: “OK, what’s next?” … type a few lines … run it … change it … run it … “OK, what’s next?”

Scaling Applications

There is a trade-off. An update function that has to handle lots of Msg types whose abstraction levels can vary (for example, SubmitReport vs. TakePhoto). It’s pretty jarring to be preaching the functional “lots of small functions” and have a multi-hundred line update function. I recommend watching Zaid Ajaj’s talk “Scaling Elmish Applications” for a good discussion of the issue.

Wrapping Up

In the end, I made my final commit a few minutes before midnight on Saturday, having created from scratch a cross-platform data-entry mobile application, two wrappers for custom controls, and a pile of soda cans to be recycled (Ocean conservation protip: aluminum cans are efficiently recycled compared to plastic bottles, despite their relatively poor packaging-to-content ratio). You can see the interim result (and proof that UX design is not my forte!) at my Github repo.

The pressure-cooker environment of a hackathon demands tools that are both high in productivity and fun: exactly how I would describe F# and Fabulous!

0 comments

Discussion is closed.

Feedback usabilla icon