CocoaFlow

Turning Glue Code into Graphs with CocoaFlow

paulyoung

Application developers today have a tremendous amount of software at there disposal. Whether it's that great networking library, JSON parsing framework, or an in-house project, the ability to delegate so much logic to tried-and-tested solutions allows for an improved software development process and results in better apps.

In spite of this, the introduction of some amount "glue code" seems inevitable in tying everything together. This "glue" is the most read, re-read, and refactored portion of an application and is therefore the most susceptible to bugs, inconsistency, and misunderstanding.

CocoaFlow is an implementation of Flow-based Programming, written in Swift, that provides a mechanism for defining applications as graphs that can be easily composed, reconfigured, and understood.

Flow-Based Programming (FBP) allows these graphs to not only be visualized, but created, inspected, and rewired with ease. For example, this is the graph of an FBP implementation of Jekyll, a popular static site generator:

Image title

What is Flow-Based Programming?

Flow-Based Programming was invented in the early 70s by J. Paul Morrison, who provides the following explanation on his website:

FBP defines applications as networks of "black box" processes, which exchange data across predefined connections by message passing, where the connections are specified externally to the processes. These black box processes can be reconnected endlessly to form different applications without having to be changed internally. FBP is thus naturally component-oriented.

FBP is a particular form of dataflow programming based on bounded buffers, information packets with defined lifetimes, named ports, and separate definition of connections.

Thus, an application defined using FBP is essentially comprised of:

  • Networks – running instances of a graph; a structural representation of an application.
  • Processes – instances of a component being executed on a network.
  • Ports – the connection points belonging to a component through which it sends or receives data.
  • Packets – units of data exchanged across components via ports.

There a many other aspects of FBP in general, of which further information can be found on the Flow-Based Programming wiki.

CocoaFlow

The simplest way to get started with CocoaFlow is to author a component. Typically, components follow the single responsibility principle and are named accordingly using verbs.

Components encapsulate logic in such a way that any style of programming may be used to define how packets are handled when sent or received on its ports. Whether the internals are object-oriented, functional, or something else is an implementation detail. This trait makes it easy to wrap existing code as components in addition to writing them from scratch.

PassThrough Component

The following example demonstrates a component that receives a packet on its input port and sends it to its output port via the network.


import CocoaFlow

final public class PassThrough: Component {
    public let network: Network

    init(_ network: Network) {
        self.network = network
    }

    // Mark: - Ports
    lazy var input: InPort = InPort(self) { packet in
        self.network.send(self.output, packet)
    }

    lazy var output: OutPort = OutPort(self)
}

Print Component

In this example, Swift's print function has simply been wrapped up into a component.

import CocoaFlow

final public class Print: Component {
    public let network: Network

    init(_ network: Network) {
        self.network = network
    }

    // Mark: - Ports
    lazy var items: InPort = InPort(self) { packet in
        print(packet)
    }
}

This could be improved by adding separator and terminator ports of type InPort for the other function parameters, although it would introduce complexity since the function shouldn't be called until all packets are received.

Some other FBP implementations provide mechanisms to address this, and something like GCD's Dispatch Groups could be a candidate for CocoaFlow.

In the case of print some parameters are optional, meaning that those ports may not be attached or ever receive data, which makes it difficult to determine when to call the function.

Hello World

Given a button and an alertController (a UIButton and UIAlertController, respectively) and two new components (ActOnControlEvents and PresentViewController), a network can be defined so that tapping the button presents the view controller, like so:

// Mark: - Networks
let network = Network()

// Mark - Processes
let actor = ActOnControlEvents(network)
let presenter = PresentViewController(network)

// Mark: - Packets
actor.control.receive(button)
actor.events.receive(.TouchUpInside)

presenter.presentingViewController.receive(self)
presenter.viewControllerToPresent.receive(alertController)
presenter.animated.receive(true)

// Mark: - Connections
network.addConnection(actor.actionPort, presenter.presentPort)

Connections determine how information flows out of one port and into another, and are represented statically as edges on a graph.

Swift's type system ensures that two ports can never be connected unless the type of the packet they deal with matches:

public func addConnection(fromPort: OutPort, _ toPort: InPort)

With the network configured, running the app and tapping the button presents the alert view controller as expected:

 Image titleImage title

Now that these two components exist it would be trivial to re-use them for different purposes without writing any additional code by simply reconfiguring the network. Additionally, this configuration could itself be treated as a component and instantiated multiple times in a larger graph.

Flowhub

While writing graphs programmatically is all well and good, it only replaces one form of "glue code" with another. There are still benefits to building applications this way; codebases become more standardized, and components are often easier to test.

However, greater benefits can by seen by treating Swift as an intermediate language. Networks may be serialized to graphs as JSON (and vice-versa) for manipulation by more specialized tools such as Flowhub.

By implementing the FBP Network Protocol, graphs can be edited or created from scratch using Flowhub, providing a new perspective for developing and debugging applications. This includes the ability to inspect the connections between components and monitor data packets as they are sent, in addition to easily rewiring the connections themselves.

A simplified version of a graph for the Hello World application might look something like this:

Image title

While some FBP implementations opt to dynamically create instances of components by interpreting graphs at run-time, CocoaFlow prefers to leverage Swift's type safety as much as possible. This results in generating valid Swift code in order to identify issues at compile-time.

Roadmap

At present, CocoaFlow is still in an experimental phase and is as much an exploration into Flow-Based Programming as it is into Swift itself. So far the focus has been on the API design of the core engine, although work on implementing the FBP Network Protocol and Flowhub integration is underway.

CocoaFlow has the potential to be classified as "Classical FBP" but work still remains to introduce multithreading in order to realize this.

Conclusion

CocoaFlow aims to offer the benefits of Flow-Based Programming to those who want them without detracting from all that Swift offers or compromising on type safety.

Visual aids for programming are a polarizing subject, so Flowhub integration is entirely opt-in. However, once complete, components based on something like Chris Eidhof's Functional View Controllers could be a great motivation to trade storyboards and segues for ports and packets.

The full source code for CocoaFlow and the Hello World project is available on GitHub.

Paul Young

Image title

Paul is an software engineer at The Grid in San Francisco. Find him on GitHub @paulyoung and on Twitter @py.