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