It was observables all this time

Alfonso García-CaroDecember 17, 2020

Disclaimer I: This post is part of the F# Advent Calendar in Japanese (the original one), but it's not in Japanese and doesn't contain any F# code either so... well, sorry for that. 今度日本語で書くように頑張ります! Thanks a lot to Midoliy btw for organizing it and to Sergey for the English version!

Disclaimer II: I will be reviewing recent developments in the way we build user interfaces, particularly in web apps. But this is done from my own experience and doesn't try to be an accurate report of Software History, please excuse the many inaccuracies that are about to come.

And with the disclaimers out of the way, let's start!

In the beginning it was Windows Forms, and everything was good. We could just drag some buttons, checkboxes and inputs to the designer, link a couple of events in the Properties panel and voilà, we had an application. The designer would just generate the code to build the UI, and when we had change something on the go, we could just name the element to get a reference and mutate some of its properties at runtime. The web had a similar experience with tools generating jQuery code, and it was also easy to get references to elements through query selectors. To avoid messing things too much, we started to separate the code in three parts: code to deal with data, code to render the view, and code to propagate changes from the data to the view and vice versa. This was called the Model-View-Controller architecture, MVC for short.

Unfortunately, at some point someone decided (I’d like to blame users but most likely it was us, developers, making our life more difficult as usual) that applications, now apps, should be more dynamic and that they couldn't all look alike. It also happened that computing devices got smaller and it wasn’t possible any more to fit everything on screen, so we had to start showing and hiding controls continuously. We all know that managing state is difficult and when the view was changing constantly at runtime, MVC was just not enough for the task.

In the Windows world the response to this was XAML: instead of using “standard” code for the views we could design views declaratively (that was the magic word) with an XML-like language similar to HTML. But the designer could still generate code for us so we kept the best of two worlds: drag-and-drop controls and manual edition when needed. Now, this XML-like language was static, so how could we make it dynamic without a bloated controller? Here comes another magic word: “bindings”. This language could declare some bindings to properties in a C# class in a way that whenever the property changed the view would change too. And it also worked the other way around: if we had an input or a checkbox, changes made by the user to these controls would be automatically reflected in our class! Now instead of a complicated controller we just needed a version of the model adapted to how the view looked and served as a bridge between them. This was called the view-model, and a palindromic architecture was born: MVVM.

Bindings were a great solution to build dynamic UIs and made their way to the web, with a plethora of libraries that could generate bindings (like Backbone.js) and template tools to add dynamic capabilities to HTML. However, bindings depended on one thing functional programmers have been warning against for a long time: mutability. This means that when the app grew in complexity, it was still hard sometimes to reconcile the state of the model and the view or even different parts of the view.

After a while, and precisely under the influence of functional programming, a project from Facebook engineers showed the world a new way of building UIs. The React team asked themselves how it would look like if instead of a template language you could use a pure function for the view. The response was the Virtual DOM. Programmers didn't need to worry about manually changing properties of the UI elements. This was similar to the bindings but more powerful because you could just fully re-render the view every time there was a change in the model and let the Virtual DOM would take care of checking what had actually changed and only make the necessary changes in the actual DOM (labelled as the bottle-neck of the UI). React hit many keys at once: came from a big company, introduced functional programming to fronted devs, was a great implementation of the web component concept, it made for the ultimate templating engine by just using JS code, useful tooling, etc. It was (still is) a great success.

React components included the capability to manage state from the very beginning, and in fact you had to update the component state in order to trigger a new render. However, for some reason React was promoted as a render-only library. “React is the V in your MV*” their site used to say. At the same time Facebook said the right way of writing React apps was to use the Flux architecture, but Flux only lived in our hearts. “Flux is just an idea, not an implementation”, they said. At the time nobody understood Flux (by nobody I mean me) and now it looks like the most obvious thing, so this may be a sign of its success. Basically it meant having something, most of the times called “the store”, holding the state of your app, let the view components send messages to it, update the state and then trigger a new render. Does it look familiar? Yes, this is what we’ve been all doing with different names (Redux, Elm architecture) and small differences.

Writing the logic code for your full app in a single place is still complicated, so in the different “Flux implementations” there was a way to split the code in multiple parts that usually corresponded to the UI components. I’m not familiar with Redux, but if I’m not mistaken it just composed the logic parts using a JS dictionary object where the keys are the messages (string literals) and the values are actions that transform the state. This way of composition was called “reducing”, if I’m getting the terminology correctly. Elmish works in a similar way but takes advantage of the F# type system for hierarchical composition that is checked by the compiler, besides using union types and pattern matching to emit and handle messages.

Elmish is the port of the Elm architecture to F# done by Eugene Tomachev, Maxime Mangel and the rest of the Fable community. As far as I know it’s fairly identical to the original but there may be some nuances.

These libraries or “architectures” allowed for a simple and clear, yet powerful structure, and having a snapshot of the whole state of your app enabled nice tricks like a message debugger, time traveling, easily saving the state of your app at any point or even transparently sharing the state with your server. In spite of this at one point Redux lost the favor of the React community. I didn’t follow the discussions but I guess the complains were similar to what I sometimes hear about Elmish: that the structure is a bit too rigid and cannot be combined with other approaches, and that it gets cumbersome to write a message with its corresponding action for every little event and to build the whole component hierarchy, instead of using stateful components.

Either because of this or just because the React team was exploring ways to keep state and expose life cycle hooks without using classes, a new feature was introduced aptly named “hooks” (pardon the redundancy). Hooks are a great way of composing behavior in function components at the cost of introducing implicit arguments, which makes things less obvious unless you know how React does its magic. Fortunately you can encapsulate that magic into a custom hook to make things more manageable, which is what a lot of people are doing. Currently there seems to be a shift from using external libraries for state management to just writing the logic of your component using a variety of hooks... in your view function! Will this last? Well, looking at how developers are desperate to move to an alternative solution as soon as they find a case where the current one doesn’t fit so well, I doubt so.

Is this the whole story? Of course not, it’s just a brief summary based on my experience for which I’ve tried to give some consistency. But there have been other frameworks and tools with interesting proposals. One very important point to mention is bindings haven’t gone away. There are still a great way to make UIs dynamic in many common cases (like form validation) and they’re at the core of widely used frameworks like Vue or Svelte.

But there’s something else missing from the story that is actually crucial to this post. Around the same time (?) as XAML, there was another invention starting in .NET world that has jumped to many other languages: observables. The observables are something very clever and simple, as most clever things. They are just a way to express with a simple interface a tremendously common pattern that so far had mostly been relegated to “native” frameworks, that is, the events. If you think about event handlers for a moment, you quickly realize they are just callback where the code owner of the callback and the code triggering it are not directly related. Instead, the two pieces of code are linked together through a “subscription”. This is extremely useful when the code emitting messages or event runs independently of the code to handle them, as it often happens with views and logic.

So the connection between observables and UIs is clear. In fact, we had already been using events to get user input since forever. But now observables gave us the possibility to do things the other way around: the view code could listen to changes in our model and update accordingly. A term was coined for this: being “reactive”. For a little while, hundreds of tutorials and cool examples taught us that we had to be reactive or vanish miserably. But only for a little while, almost without notice the trend faded away and only the expression remained, we still talk about reactivity in modern UI frameworks. Why did this happen? My take is that this was caused by the confusion between observables and observable streams. The observables came in hand with a set of tools to deal with them as if they were sequences: the Reactive Extensions, and most of the samples using observables with the UI were about observable streams. This is super helpful when dealing with events that happen in a rapid fashion, think of web sockets or user typing, but not so much in more general cases, like button clicks or http requests. I believe the insistence of showcasing observable streams relegated observables to a niche.

I’ve been recently exploring integration between Fable and Svelte, the cool new (in JS these words always go together) “disappearing” frontend framework. Svelte was created by Rich Harris which is also the author of Ractive, the very first HTML renderer I tried to integrate with F# (using Funscript back then). There are many things to like about Svelte, I invite you to visit their website to learn more, but here I want to talk about its API to interact with external stores which is, as you may guess if you’ve been following the plot, observables. Using a programming standard for a public API makes it really easy to interact with it from a non-JS language like F# through Fable. Svelte feels a lot like Vue, but when I tried to combine Fable and Vue the result was not so nice. I have now high hopes that Svelte can become a great choice when building Fable apps in addition to React.

I'm thankful that Svelte has helped rediscover observables, because they do have interesting properties that made them a great fit to serve as the bridge between logic and view code:

  • They can have multiple observers, overcoming the need of a 1:1 relationship between your logic and view components. In a simple app like a TODO list manager, for example, it may be enough to have a single logic component, but you may still want to have multiple UI components.

  • They are inherently disposable. This is very important because modern UI frameworks automatically manage the life cycle of UI components, so you need a way to tell the logic when the component has been unmounted in case you need to make some cleanup. Having this by default makes things much easier. Adding lazy initialization is trivial too, just defer it until you get your first observer.

  • Observables can be synchronous and asynchronous at the same time, same as Elmish “update” functions do with commands.

  • They are easily composable, an observable can just observe another observable.

I am convinced now that observables are the way to go for connecting the view and logic code and I’ll try to apply this when architecting apps from now on. I’m still in the exploratory stage and some patterns may emerge after playing a bit more with this. This is why this post is mostly conceptual and doesn’t contain actual code. Please also note that I’m not trying to introduce a new “UI architecture” here, what interests me about observables is precisely their flexibility to integrate different approaches to solving different problems in client applications. In the case of Fable, as we’re a small community, we’ve tried to focus on a single and clear way to build UIs (Elmish with React) and this has worked great, but I feel a bit sad that we haven’t managed to integrate other interesting proposals that work really well in specific situations, like Dag Brattli's Fable.Reaction or one of my “click” moments when learning F#: async workflows to represent UI state machines by Tomas Petricek. I will still be using Elmish a lot because it’s a great solution for many cases, but I’d like to be able to use other approaches when it makes sense, and I’ll make other adjustments as well:

  • Logic components don’t need to match UI components, as we've seen above.

  • Logic components must be as independent as possible of our rendering framework/engine. Ideally, they should be in a different layer and easily reusable in other environments, like a console app. In a sense, Elmish gets this right because the “update” function is independent of the “view” function. But promoting the architecture as MVU (model-view-update) makes it hard to see it.

  • Given how modern UI frameworks work, it makes more sense for logic components to be activated and deactivated upon calls from UI components, not the other way around. This is akin to how logic modules work in a server, as they are just invoked by whatever routing framework we are using. The main difference is logic modules in the server are short lived, they just stop as soon as the job is done and the response sent to the client. But it’s still the same, the UI component initializes the logic component as soon as it’s mounted in the DOM (it may also happen it’s a shared logic component and it’s already up and running) and gets destroyed, or “disposed”, as soon as the component is dismounted from the DOM.

  • As frameworks like React, Vue or Svelte provide ways to keep state in the UI component (state hooks in React or bindings in Vue and Svelte), it's ok to take advantage of this for things that don't really belong to our logic model, like whether a menu ir open or closed, or even form controls before validation.

  • Finally, we should be able to use a non-Elmish approach in some situations. This is where observables come into play, because their contract can be easily fulfilled with different implementations, you just need to be able to trigger a new event whenever the logic state changes.

Together with other members of the Fable community, I’m currently finishing a package to easily integrate Svelte in your Fable apps. When it is ready, we will talk more about the code specifics. Note this doesn’t mean we all need to move to yet another framework overnight, Elmish and React are still great choices to build web apps. In fact, we want to make it possible to apply this new knowledge to existing apps so we will be also publishing a package to easily build observable stores in Feliz apps.

If you’ve come this far, thanks a lot for reading the post and I hope you found it useful. Stay tuned and follow the @FableCompiler Twitter account for get the latest news. We all have big hopes for 2021 so even if it’s a bit early, Happy New Year to you all!