Callbacks in the synchronous world (Typescript)

Abdul Jeelani
6 min readMar 8, 2019

Ever wonder why we need callbacks when dealing with the synchronous code?

Let's try to solve a nontrivial problem! Dealing with database connection and transactions.

Problem statement: “We need to access the database with a transaction.”

doSomething with the database using transaction

Let's define a function to add an order in DB

In both doSomething and insertOrder, we can see a pattern

  • Setup (DB connection and transaction)
  • Work (add order)
  • Tear down (clean up things).

Oh! It just reminds us the AAA — Arrange Act Assert, Yup! But we can call this Arrange, Act, Abolish if you want to remember in this way.

This way of programming is called direct style. Let's try and abstract the redundant things by creating a function to perform any arbitrary operation using the database connection

Above execute function takes 2 inputs

  1. connection string — containing the information to connect to DB
  2. a “process function” (fn) — a function that uses the created connection and does some process then returns a value of type R.

When we call the execute function it

  • Calls the getConnection and acquire the connection
  • Execute the process function
  • Cleanup the resources
  • Returns the result of the process function.

Note that the return type of the execute function is the same as the return type of the process function.

The functions that can take other functions as an input or produce functions as output are called Higher Order Functions (HOF)

Let's define, Another HOF that works with transaction

When we call the execute function it

  • Calls the createTransaction and begin the transaction
  • Execute the process function
  • Cleanup
  • Return the result of the process function.

With these 2 functions in place, Our insert order code would look like

Above code uses closures to capture the value of connection and transaction.

Milestone

Realize the need for passing in callbacks in synchronous programming to solve the redundant AAA problem. We solved our problem using higher order functions! This style of programming is called continuation passing style.

But this does not come for free! Let's discover the issue here with our

Updated problem statement “We need to access database with a transaction, as well as log and time the transaction.”

Let's go ahead and define a few more HOF for

  • timing — that measure the time taken by a process
  • logging — that logs the before and after the state of a process,

We will now write one more function that takes a connection string and works on the transaction. The function shall log before and after State of the transaction and also compute the time taken by the transaction

Do you see the problem now? Oh Yeah! Welcome back Callback Hell a.k.a. pyramid of doom.

Let's go ahead and solve this too. Caution! Jargons ahead

Let's start by creating some type alias for better readability

Unit Type and a Function alias

Here we defined a type called unit to represent nothing. But why? Well! In FP while creating functions if we encounter a situation to deal with functions that do not require any inputs then we take a unit as input. Likewise, if a function is not supposed to returning anything we return a unit value. This technique is extremely useful in function composition.

Note: Functions that do not have any input are most likely to cause side effects.

Func<A, B> is a type alias to represent a function that takes a value of type A and returns a value of type B A ->B

In all our HOF’s we take a process function as input. These input process functions take a resource to work with and produce a result. (resource in the above examples are DbConnection and DbTransaction)

Process function = Func<T,R>

where T is the type of resource and R is the type of result produced.

Now, let us look at the signature our HOF’s

They all are functions (Func) that takes some parameter and a process function as input and outputs the output of the process function. i.e. if the process function returns a string our HOF returns a string and so on.

HOF can be written as,

HOF = Func<Process function, R>

Substituting the process function

HOF = Func<Func<T,R>, R>

where T is the type of resource and R is the type of result produced by the process function (i.e. the function that uses the resource)

In literature, it is called Continuation Monad. Let's not worry about literature now and give our HOF a name Cont<T, R>

With this in place, let's rewrite our execute HOF using a process called currying. Now instead of taking a function as an input we will return the continuation function as an output.

Instead of taking a function as a parameter, we now return a function. (It is still Higher Order Function)

executeCps is a function that takes a connection string as a parameter and returns a continuation function that takes a DbConnection and returns a value of type R.

Why are we using a Generic type <R> here? At the time of defining this function, we are not sure about the return type of process function. It is up to the caller of the HOF to decide what it will return, But what we know from the above signature is both the return types of the continuation function and the executeCps are same.

One major advantage of this technique is that we have reversed the control here. Hence, the caller forms the chain / workflow / pipeline instead of the function being called !!!

What does it mean?

If a function A takes callback B, then the function A is responsible for calling B. Incase if function A returns a function B, the responsibility of calling function B goes to the caller of function A.

Let's proceed and rewrite our transaction function, and call it

As you can see in executeWithTransactionCps function we

  • call the executeCps (returns a continuation)
  • and then call the returned continuation

Similarly, we call the executeWithTransactionCps with

  • connection string parameter
  • which now returns a continuation that takes a transaction resource
  • we call the continuation with the actual process to do

Let’s finish the other two functions as well,

As you see in line number 18 to 21, we have somewhat mitigated the nested callback / callback hell.

If we use to rewrite the above function (as monadic composition) using the bind function we defined earlier,

For now, we are timing and logging the transaction phase alone.

What change would it take if we need to include the connection?

Well swapping the lines would do it!!

If in the case of callbacks swapping is not possible, rather we need to change all the function calls !!!

Milestone

We have now solved our call back hell problem. Though due to the limitation of language support this still looks redundant calls to bind function. In proper functional languages it would look like (f# pseudo code below),

The advantage this brings us if we need to change the order of timing or logging its just about changing lines, i.e. any change in the chain will not affect the functions.

This is due to the reason that now caller has the control to chain the functions not the function being called.

Okay, All good. Nightmare!

Working with libraries that exhaustively use callbacks. Now we cannot modify the source, What can we do now? Is there any way we can get around this. Yup!

Let's define a middleware instead of having the result type as a generic, we open up to contain anything, and then use the middleware to describe our workflow / pipeline,

This is extremely useful when we have a AAA scenario.

Do we need to do all by hand? Nope! You can still use promises with their finally feature as below

Continuation using promises (ES8 finally)

In the finally, block we get the reference of the connection/transaction through a closure.

That being said, Promise is also an implementation of continuation monad.

If you have made up till here! Here is my salute!

Thank you!

See you again with more Monadic stuff. After all, it is just a monoid in the category of endofunctors.

--

--