The second case for function-tree

Talking about new concepts is difficult. Especially when those concepts aim to solve complexity. The initial article on function-tree was a very direct approach. It might not have made much sense, cause there was nothing to compare it to. Well, in this article we are going to go from a single promise to a complete function-tree, talking about what problems it solves a long the way.

A promise

If you are not familiar with promises it is explained as “a future value”. Think of it as a wrapper for a value you can only access with a callback:

So this is a typical way to create a promise flow. You start with a promise and add to it. At the end you catch any possible errors (if you bother). Even though promises are a great concept, it is very low level and it is difficult to discipline yourself to write good code. For example with the code above I show off some typical problems you can get yourself into:

The flow accesses variables in its outer scope. applesUrl. With promises you easily get into a mess of pointing to outer scope variables, making your code harder to understand and reason about. Basically it is more difficult to write declarative code

With promises you typically return only one value, meaning that a concept for “passing on values” from previous steps is not opinionated. The example above creates a variable that is assigned later. This is not ideal

Our side effect (ajax) is also accessed in the outer scope. This makes promise flows harder to test. Even though promises hints to be a great concept for declarative code, where each .then just references a function, it is difficult to do in practice

Maybe you already have ideas to make this code better, that is great! Maybe you feel provoked as “this is not the way to write promises”, great! We have something in common :) My point with this example is to show that Promises are low level and gives a lot of freedom, freedom that can easily move you down the wrong path.

So how can we make this flow better?

Improving the flow

Injecting a payload

First of all we want to prevent ourselves from pointing to the outer scope. This will allow us to write our code in a declarative way. Starting our flow by resolving the input to it, our urls, we are able to make our first “bananaGet” declarative:

Passing the payload

The second thing we need to do is make it possible for us to rather extend the existing payload with new properties. This will make sure that whatever payload we start with and add, will be available all the way through the flow:

Side effects

One last thing we want to improve here is the side effect that is happening. Ideally we want to separate the side effects and the flow completely. We can do that by passing in the side effect as part of the flow:

Summary

We have made our flow declarative. It is very easy to understand what this flow is doing, as we can just read the function names

We have solved the passing of data. By having an argument we in this example call context, we can pass in a payload that is added to, instead of replaced for every step in the flow

We also pass in the side effect itself to the flow, meaning that we have completely decoupled execution from side effects

What we gain by this is:

Reading a step by step reference to named functions gives an instant understanding of what the flow does. This allows you to understand code without the distraction of implementation details and you can also plan implementation as a flow first, then implement

We have a consistent way of passing data through our flow. The props property is where initial and added data is contained

We have increased testability. We can test each function in the flow individually by mocking the context passed in. We can even test the whole flow in isolation by using a mocked context

If you identify yourself with the promise issues mentioned so far and you see the benefits of the pattern explained here you will have a blast reading the rest of the article. If not, I suggest you keep reading anyways as these ideas gives benefits far beyond readability and testability.

Defining a context

So in the example above we separated our side effects from the execution:

Function tree analyses the whole tree before it executes, meaning that it will identify execution paths and make those available to the functions that can execute them… like hasBananasUrl in this example. Being able to also declaratively express conditional paths improves readability even more, especially in complex flows.

Static analysis

So promises is a low level tool. We used it to create a concept of flow, but as with all low level tools, abstractions can be made to make them more powerful. Also because it is low level there is no way to statically analyse how things will run, but function-tree can. A static analysis means that we get a serializable representation of the flow. For example the flow above:

With a static representation like this we are able to create developer tools. When you use function-tree, either in the browser or on the server you will be able to use a developer tool that gives you all the information about the executions. You will see what executions are made, the props, side effects run and their arguments, sequence execution, parallel execution… even how you compose together the functions and sequences of functions. This is not something you would be able to do with pure promises.

This is an example of the function-tree debugger:

Async / Await

But you might say… why even try to define this flow with promises? You can just use the new async/await. Even though async await does make it easier to define flows compared to promises it does not encourage declarative code, it encourages imperative code. Let us convert the example above:

Async / await allows us to more naturally access shared variables, as the flow is defined in one function, but it is not declarative. In this example we need to read all the implementation details to understand what it does conceptually. Declarative by definition helps readability and it allows us to create tools like the debugger.

It is also difficult to test this code, as we are likely to point to “outside side effects”.

Summary

So this article was not about saying that promises and async/await are bad. They are great! But they are also a bit too low level when we want to handle complex flows and write code that is readable and maintainable. With function-tree you get some opinions and guarantees:

Your side effects are separated from the execution, meaning that testing is easier

You never have to break out of declarative code, as all you need is on the context. Even when diverging execution you use declarative paths

High degree of composability. You can safely create a function that operates on its context without thinking about “the stuff around it”. You can compose this function into any tree definition, making flows truly feel like putting together lego blocks

Debugging flows can be difficult when your only reference is the code itself. With function-tree you get a debugger that understands the flow and gives you insight on a higher abstraction level

The function-tree project was not just some idea or theory. After the first iteration of the Cerebral project it was obvious that this implementation could power more than the framework itself, it could be published as a standalone abstraction to handle flows. During development of Cerebral 2 there has been many iterations, testing function-tree in many different scenarios and it has proven itself to be a practical way to structure your code.

If you want to try function-tree you can head over to the Cerebral website or check out the repo to get going quickly.