Hacker News new | past | comments | ask | show | jobs | submit login
Implementing the Elm Architecture in Swift (medium.com/design-x-code)
93 points by rheeseyb on Feb 2, 2016 | hide | past | favorite | 20 comments



As someone who writes some pretty hardcore UIs in Cocoa, this reactive style stuff seems applicable to a pretty narrow class of application.

The more you fight Cocoa, which I believe this stuff does, the bigger the mess you'll create for yourself once you view hierarchy becomes large and filled with custom views that don't really fit what 99% of these tutorials cover.

Furthermore, trying emulate "dom" diffing by replacing views, instead of updating them, as they seem to be illustrating towards the end of the article (could be reading this wrong though) is pretty inefficient way of doing things in Cocoa.


This might be somewhat true for this particular article, but please do not confuse that with functional-reactive-programming-like systems in general. I don't know RxSwift so will write in terms of ReactiveCocoa.

1. Apps are more than views.

2. ReactiveCocoa and RxSwift are just asynchronous data over time, represented as Just Another Data Structure (cough monad). Even if you just use them instead of callback blocks, it's an improvement because they can be composed, stored in data structures, and used in ways that methods taking a callback function and returning Void cannot. Cancellation is then implicit on deallocation, instead of needing to reference a "cancellation token" or other icky state like that.

3. Complex UIs are exactly where you want this type of system, because it provides type safety and compiler verification, instead of hoping that your target/selectors and KVO work and never break when you edit something and the compiler doesn't complain.

I think that a popular JavaScript UI library being named "React" has damaged the perception of FRP-like systems. Generic FRP-like systems are not related to UI, other than that they can be used for it. People are confusing "DOM diffing" for having anything to do with signals/streams.

In UIs, FRP-like systems allow you to take some evil imperative state (a slider was moved!), lift it into a happy pure-functional world, process the inputs, and drop out of evil imperative state at the end ("update the color"). For example, an RGB slider (using a bit of shorthand in places):

    combineLatest(redSlider.value, greenSlider.value, blueSlider.value)
        .throttle(0.5, onScheduler: UIScheduler())
        .map({ r, g, b in UIColor(r, g, b) })
        .startWithNext({ color in colorView.color = color })
In four lines, we accept input from three different controls, wait for the changes to "settle" (let's say updating colorView.color is expensive for some reason), and update the view! It's very easy. Let's say we want to make a change, and only update colorView when the user taps a button:

    combineLatest(redSlider.value, greenSlider.value, blueSlider.value)
        .sampleOn(button.tapped)
        .map({ r, g, b in UIColor(r, g, b) })
        .startWithNext({ color in colorView.color = color })
Only one change necessary. In plain-old-Cocoa, this would require another instance method to be defined.


So, this is the first time I actually understand what the FRP people are talking about. So, thanks for that. It has been greek to me for some time.

However, now that I do understand it, I agree with parent. I don't want to use this for complex UIs. Point by point:

> Cancellation is then implicit on deallocation

In practice, you're going to have retain cycles, no? I mean I don't know where these four lines "live", but if we write them in a closure and ship them off to a sync/diff/runloop engine, unless we are quite careful, that sync engine is going to hold a great many strong references. Unless you intend this to be an IBAction definition, in which case...

> because it provides type safety and compiler verification, instead of hoping that your target/selectors and KVO work and never break when you edit something and the compiler doesn't complain.

As long as the underlying Cocoa uses target-action, true compiler verification is impossible. Compiler verification checks something (these four lines) but it doesn't check that these lines actually run when the slider is moved in any way.

> FRP-like systems allow you to take some evil imperative state (a slider was moved!), lift it into a happy pure-functional world,

It's not immediately clear to me how e.g. throttle is implemented, but it must accumulate state inside it somehow in order to replay the event after the timer.

> Complex UIs are exactly where you want this type of system, because it provides type safety and compiler verification, instead of hoping that your target/selectors and KVO work and never break when you edit something and the compiler doesn't complain.

Compiler verification is good; tests are better. And I do not understand how you would even begin to write unit tests for this.

Now we get to:

1. Stopping in the debugger and trying to reason about these signal chains is complicated, because what we have here is a datastructure in memory, not lines of code I can step through

2. This example does not account for threading, and in any nontrivial example you want to move between background and foreground a few times. It also does not deal with "splitting/merging" (multiple observers, etc.) and I suspect the intersection of those two features is a sharp edge.

3. Finally, let's compare against a slightly more traditional syntax:

    @IBAction func valueChanged() {
        dispatch_throttle(0.5, onQueue: dispatch_get_main_queue()) {
            colorView.color = colorToValue(red: sliderA.value, green: sliderB.value, blue: sliderC.value)
        }
    }
This syntax is also four lines of code, including the context of where the lines live. This example resolves all the problems I listed with the FRP example. In addition, it also collates which slider goes with which color component in a single line, rather than breaking that relationship apart across a (potentially long) signal path.

To evolve from your first example to your second example we would just change

    slider.addTarget(self, action: "valueChanged", forControlEvents: .ValueChanged)
to

    button.addTarget(self, action: "valueChanged", forControlEvents: .TouchUpInside)
While I readily concede this aspect is not quite as elegant as your example, to me having a slightly more complicated 1-line diff is a very low price to pay for all the other benefits I listed.


I just wanted to respond to this:

> It's not immediately clear to me how e.g. throttle is implemented, but it must accumulate state inside it somehow in order to replay the event after the timer.

Certainly, a lot of FRP (and functional programming in general!) concepts involve state. I would never argue that there is no state in functional programming, just that the state is more explicit, localized, and easier to reason about.

In fact, in pure FRP, there are two fundamental building blocks: Behavior and Event. A Behavior is simply a value over time, and an Event is a value at an instant in time. Therefore, Behaviors are where you store state. However, what makes them "easier to reason about" is that oftentimes the API forces you to construct them in a way that makes all of the possible ways to change the Behavior are clear and present at once.

In summary, all inherently stateful concepts require stateful code, at lease somewhere under the hood. I would say that FRP and functional programming just make the state explicit.


> In practice, you're going to have retain cycles, no? I mean I don't know where these four lines "live", but if we write them in a closure and ship them off to a sync/diff/runloop engine, unless we are quite careful, that sync engine is going to hold a great many strong references. Unless you intend this to be an IBAction definition, in which case...

There's a special structure that deals with IBAction called Action, for example:

        let press = Action<UIButton, UIColor, NoError> { button in
            return combineLatest(redSlider.producer, greenSlider.producer, blueSlider.producer)
                .throttle(0.5, onScheduler: QueueScheduler())
                .map({ r, g, b in UIColor(red: r, green: g, blue: b, alpha: 1.0) })
        }
        
        button.addTarget(press.unsafeCocoaAction, action: CocoaAction.selector, forControlEvents: UIControlEvents.TouchUpInside)
As to where you'd put the code: since this code snippet deals with declaring how a button should react to touch event, putting it in viewDidLoad will do. Now in my production app, most of the signal code are in viewDidLoad or viewWillAppear which doesn't have the retain cycle problem. If you want to "forward" the signal result to a sync/diff engine, then you'd extend the engine with ReactiveCocoa structure like `MutableProperty`, `SignalProducer`. Those structures would be responsible for feeding the data to the engine. After that its only a matter of binding you data signal to the extended strucutre.

In other words, coding in FRP is like trying to construct pipelines. Water (data) flows inside the pipelines, but you don't move the pipelines themsevles around, instead you build them in a way to direct the water to the intended destination. This is why most of the signal declaration is in the viewDidLoad and viewWillAppear.

> As long as the underlying Cocoa uses target-action, true compiler verification is impossible. Compiler verification checks something (these four lines) but it doesn't check that these lines actually run when the slider is moved in any way.

But it is possible to abstract away the dangerous code that rely on un-verifiable string declaration. For example in production app you'd have a lot image assets bundle with the app. In your view controller, you could naively load the image using string:

         imageView.image = UIImage(named: "cloud")
But anyone with decent experience with Cocoa should know that littering such code all over the codebase is not a good idea. Instead you could abstract the string portion away and put it in a struct and have the string as static member. When you load the image, it would be like so:

         imageView.image = UIImage(named: ImageAssets.cloud)
I'd say this code is way better than directly involving string. Although it is true that at the end of the day we are still using string thus true compiler verification is impossible, we reduce the points of failure to just one. As long as the string declaration in the struct is correct then everywhere else should be too.

From the above code example, ReactiveCocoa does abstract away the need to enter string characters into addTarget function. It is basically the same idea as the image example.

> Stopping in the debugger and trying to reason about these signal chains is complicated, because what we have here is a datastructure in memory, not lines of code I can step through

I would say the data structure explanation is not great. To me its more like constructing pipelines and water flow through it. There's operators that allow you to peek into the pipelines. It is true, however, that it's slightly more difficult, comparing to imperative, to debug for first timers as where you'd put the breakpoint is not immediately clear, but with any technologies there's always a trade off.

> This example does not account for threading, and in any nontrivial example you want to move between background and foreground a few times. It also does not deal with "splitting/merging" (multiple observers, etc.) and I suspect the intersection of those two features is a sharp edge.

Moving between threads is incredably easy:

          combineLatest(redSlider.producer, greenSlider.producer, blueSlider.producer)
                .delay(5, onScheduler: QueueScheduler())
                .map({ r, g, b in UIColor(red: r, green: g, blue: b, alpha: 1.0) })
                .observeOn(UISchedular())
In this example, the signal is delayed for 5 seconds off thread and performed "expansive" mapping off thread then brought back to main thread.


Hmmm, sorry if it's not clear but we aren't replacing views after the diffing, we are updating them (that's what we use RxSwift for). The diffing is used to determine which views to add, which to remove, and which to update.


Actually your last point was the aspect of this article I liked the most. Most examples I come across of applying non-imperative patterns to fundamentally stateful applications (UI, games) cheat at the last hurdle.

It's usually "we take a model, apply a transformation, creating a new model, and display it - no side effects, we're heroes, now everyone agree to never use imperative languages ever again!" but then forget that they've now created a copy of their entire system, which may cause havoc with anything from memory budgets to publish-subscribe setups or asynchronous update/display loops.

At least by not sidestepping this thorny part of the problem the authors are showing a sensible solution which doesn't try to pretend a UI composed from NSViews is a stateless system. Instead they show a reasonable way to maintain its state through diffing it against an idealised model, localising the changes (additions, deletions, updates) to the minimum necessary.


> ...but then forget that they've now created a copy of their entire system

With persistent data structures you don't have a whole copy of the entire system. The two views of your system share memory where information didn't change. Sort of like how Git works.


You're absolutely right, and persistent datastructures are exactly the right solution for this problem domain. But that doesn't get round the fact that they are a hell of a lot harder to implement and to understand in comparison to what you had before in an imperative, mutable world (as a UI programmer I now deal with diffing and versioning whereas before I simply mutated variables).


This is what I mean by hardcore UI: http://imgur.com/a/XBfNY

Sorry for the marketing shots.


We're actually aiming to build UIs that would match this level of complexity (if not even more complex), so it will be interesting to see how this pans out. I'm not massively concerned about it being an issue tbh (though thanks for flagging it) - the real issue that I'm concerned might swallow a lot of our time will be how we implement animations using this architecture (though we're currently looking to React JS for inspiration there).


That's what i was about to say : i'd be curious to see how this approach works when bringing animation into the mix, as well as complex uikit components ( such as tableview and collection views) which have their own optimized refresh cycles.


Interesting times for experimental Swift UI libraries coming out, with tcp-json-swift [1] mentioned last week and now this.

This is an excellent synthesis and transfer of related technology. Nice work! Some of the Swift- and NS-isms are tricky to follow without knowing Mac dev, and I'm still wrapping my head around how it all wires together, but especially the update loop is very clear and small and I think a unique use of Rx to blend into React/virtual-dom/Elm ideas.

My perspective is I am trying to bring the Elm Architecture to F# but started out porting virtual-dom.js on the bottom. It's early and ugly and just at the point of having to deal with patching children nodes, so parts of this are quite relevant. One avenue I intend to check out is performing set operations on a flat node list and using something like protocol methods on parent views to add and remove children, so that part of the code is definitely going to trigger some thoughts.

The dictionary of observables indexed by node threw me at first. AFAICT the virtual views, action types, and updates all compose, and then once it's time to do anything with real views (the patch operation in diff/patch terms) just that part is handled independently per node? (EDIT: Ah, I see the context dispatch is composed as in Elm, and only pushing down model updates is separate.) It's dead easy compared to the work virtual-dom.js has to do to index patches, index DOM nodes, and walk the DOM applying updates. This seems to handle all that with much less code, with a couple caveats:

1) The ability to reorder children is missing, right?

2) Any plan to support laziness to curb rerendering sections of the virtual tree? It's probably not needed for many apps, but for some reason the feature gets air time in discussions around React and Elm. (Looks like all observables get called unconditionally once per update, too?)

[1] - https://github.com/chriseidhof/tcp-json-swift


Thanks for the feedback! To answer your questions: 1) Currently, yes, though that is something that will definitely be implemented as we work on this further 2) Laziness there is something that we're going to look into. We need to first assess how much of an impact it has on performance - if that's big enough to warrant taking further (which it likely will be for more complex apps) then we need to figure out the best way to perform a deeper diff of the model Good luck with the F# port - I'd be very interested to hear more!


Great, I will keep an eye on your repo and look forward to seeing how you solve those problems, if it comes to it.

There's not much to say on the F# front yet except that the goal is to get a taste of functional rendering from an app state atom for Windows GUIs and F# and the Elm Architecture seem like the most approachable way to get there for me. So far the work consists of porting the node, thunk, and widget designs from virtual-dom.js to F# types and functions and using .NET reflection to create and patch live views. Work continues on the patch function, but seeing your code is definitely creating an itch to move on to the architecture.



As someone who's currently learning Swift and Cocoa and have no idea what Elm is. Is this something actually useful that helps building apps?


Elm is a language that compiles to JavaScript that is designed to be somewhat accessible to people already using JavaScript...one could call it a flavor of JavaScript that's influenced by languages like Haskell and not get an argument from me.

On the other hand, this project, though interesting, isn't something that the process of learning Swift and Cocoa would really benefit from. If it seems kind of interesting, starting with Elm as a separate project is probably a better way to get context for evaluating using the Elm architecture in Swift.


Elm is a language for writing web applications. There are a lot of good ideas in the framework and it's a fairly big influence on up-and-coming UI frameworks/approaches but it won't help you write Swift/Cocoa apps directly.

[1] http://elm-lang.org/


This is still more or less at a proof of concept stage, so you won't be able to use it to build apps, but it will introduce you to some functional reactive programming concepts




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: