Take

Functional UI

I'm writing this article leaning against some nameless architectural mistake, and I am not writing the article on a Mac. I would, but my PowerBook is fresh out of power (funny notion, to name the thing after its only major shortcoming it's rather like Greenland in that respect.) [emphasis editor's]

Douglas Adams, The Little Computer that Could, 1998

Linked in popular places today is Marcel Weiher's UIs Are Not Pure Functions of the Model - React.js and Cocoa Side by Side from 2018, which delves into why React's functional state muckery appears useless.

This is a subject that I give a lot of thought from many angles, so there are many things to note:

  • The post appears snarky, but bases its attitude on the notion that Cocoa is a thought-out, capable, battle-tested, baked user interface framework. For example, for the point where the React side focuses on being able to provide fit-for-purpose lists, the Cocoa side highlights that nothing special needs to be done since, roughly, we know how to use our lists and we know how to design model classes to hook into them.

  • The functional nature of React, according to the origin story - retold many times, but here citing Pete Hunt in 2014's Rethinking Web App Development at Facebook beginning at around 24:00, is that of all the collective state being changed boiling down to one type of update, and if we could just handle that well we could stop the complexity from being spread out across 500 minor pokes to the DOM from across as many JavaScript files. Pete says "we built a user interface library called React, designed to solve the hardest problem in this space, which is that is that data changing over time is the root of all evil".

  • The major, primordial feature of SwiftUI, as explained by Kyle Macomber in WWDC 2019's Introducing SwiftUI session beginning at around 24:00 is collapsing the number of reasons things can happen to views down to 1. Kyle says "UI programming is hard - like 'lock free concurrency' hard" and refers to an "explosion" of possible ordering of events. These may not be identical arguments, but they are at least parallel arguments within their respective ecosystem.

  • The notion that Cocoa's views are baked and done and ready has also taken a hit in the last decade or so. As an example, when Mac table views stopped being primarily NSCell-based, they became NSView-based, and responding to selection and picking a contrasting color to the background is now a halfway magical process, when it used to be entirely managed for you. And of course, the desire to go to more custom and higher production value presentation requires even more code to stray from the default. (Even if collection views, for example, are excellent tools to get a grip on this complexity and focus on the behavior you want.)

View models and functional UI look like solutions, and they are indeed effective ways of managing complexity by making all the constituent state visible and enumerated. But in my experience they also encourage a way of programming where you bind as much as possible, and the problem with that is that, as the title of the linked post notes, UIs are not pure functions of the models.

If you go from one place in your UI to another, you may want to be stopped because there are things that don't validate or don't fly. You may have pending changes that should neither automagically apply nor be lost. Both SwiftUI and React have state as a first-class concept and are theoretically well-equipped to handle this; what's worse is that we don't have a handle on it.

We don't know how to think in state. For the edited-but-not-saved text of a text field, sure. For the in-progress-but-not-committed-changes in what is at least partially modal, somewhere in the UI? Hm, well, that sounds like a tree of Redux reducers - and therefore model data - or a bunch of nested view models to me. The SwiftUI talk mentions "sources of truth" a lot. Here, the source of truth for the hitherto unsaved data is nebulous. Living in 107 state variables? Living in provisionally updated properties of an ObservableObject that is kept uncommited from the database or real source of truth?

The key to great user interfaces is that they work the way the person expects. That constantly - not always, but constantly - requires making the mental model of the user interface richer. When an item is dragged around in a list, there should be indications about where the new item would land if you dropped it and you should be able to cancel it. If the list is hierarchial, you should be able to have things open for you in the middle of the drag. When you type blindly into the list, the list should select the next best item depending on what you typed. When you have a list of items and the ability to close one of them, you should also be able to close all others. When you can select one item and edit some of its information, you should be able to select multiple items, see where the information agrees and edit the information en masse, applying it to all of them.

Great user interfaces take time and effort and forethought and respect for the user. Not everyone may wish to always create a great user interface for all things at all times. There's a time and place for all ends of the spectrum and all levels of ambition. But UI frameworks should think about how it can, to quote the most unintuitive thing I know, make simple things simple and hard things possible.

Because no matter how much support the framework provides, hard things remain. Thinking back to the not-yet-committed changes, before anything is saved, hitting Undo should Undo whatever is possible to Undo. After the save operation has happened, Undo should magically switch to either doing nothing or Undoing the entire change. Figuring that out requires thinking through what the model of the user interface needs to be to be closest to the user's mental model. After you have done that, the last thing you need is the framework fighting you because it did not anticipate your need for this type of control.

Frameworks should be built by and for the people who want to create the great user interfaces, the ones that anticipate your needs by assuming that you want to have as much and as expedient control as possible over whatever the user interface is about. Where a list means being able to copy and paste the selected items, drag and drop the items to rearrange them, drag and drop the items holding a key to copy them, hold a key to check/uncheck all checkboxes, resize all columns to fit the width of their respective contents, sort columns by clicking the headers, drag and drop the columns to reorder them and, yes, type into the list blindly to jump to the item with the text prefix.

It is the case that many things are presented better on the web when they use, say, a multiple-row, "layouted" form of presentation. Where for good reason, things should not just look like a list. The chat messages example from the React talk should not look like a list. But the big failure of the web's style of user interfaces is that everything is custom. On the web you have to make a gargantuan effort to build everything from the ground up and work well on every device. On the desktop (and in some ways on the tablet or on the phone), you just have to make a concerted effort to use the features that are already there.

So, wrapping around. I don't think functional is the right idea. I don't think mutation from fifty angles is the right idea. I don't think controls or MVC guide you sufficiently towards how your code should be structured to best serve the user interface, without bugs and allowing interactivity and reactivity. My answer is that I have no answer, but to look out for seemingly perfect answers. Making everything look like the web is not a good idea. Making everything look like not the web, when you have people expecting the web, and who have never clicked to sort a table column in their life, is probably also not a good idea.

A pure function transforms an input value to an output value. If the idea was for a reader to go into this post with one idea and come out with another, this post is as impure as it gets. But maybe it, and 500 other random bits from other random ideas in other random places, will end up collectively giving you a clearer, more nuanced picture over time.

I consider that an imperative.

Previous post: Don't Overextend Yourself