Home Pipelines
Post
Cancel

Pipelines

The Problem

1
2
3
4
5
6
7
8
9
Write JavaScript code using the setTimeout function to print 3 lines asynchronously.

The output should do the following:
  1. Wait 2 seconds
  2. Print out “First task done!”
  3. Wait another 2 seconds
  4. Print out “Second task done!”
  5. Wait another 2 seconds
  6. Print out “Third task done!”

So, I was given this problem a little while ago. It’s a deceptively simple problem (for beginners), and I think it gets at core of a common design problem.

Naive Solution

In my head, setTimeout worked like this: Give it some code and a time to wait, and it’ll run the code after said time has passed.

After thinking it over for a bit, this was the first thing that popped in my head:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// code to run
function firstTask() {
	console.log("First task done!")
}
function secondTask() {
	console.log("Second task done!")
}
function thirdTask() {
	console.log("Third task done!")
}

// time to wait
setTimeout(firstTask, 2000)
setTimeout(secondTask, 4000)
setTimeout(thirdTask, 6000)

Since secondTask runs two seconds after firstTask, that means it occurs four seconds after running the program!!! :DDD

Yeah, no.

Though it technically works, it doesn’t solve the problem at all: the second task doesn’t run two seconds after the first one, it just so happens to. If I wanted to make secondTask run three seconds after firstTask instead of two, I’d have to do this:

1
2
3
setTimeout(firstTask, 2000)
setTimeout(secondTask, 5000) // modified
setTimeout(thirdTask, 7000) // ALSO modified!

So, no good. What I really needed to do was make each task run after the previous one.

Callback Chaining

Okay, secondTask runs after firstTask right?

1
2
3
4
function firstTask() {
	console.log("First task done!")
	setTimeout(secondTask, 2000)
}

Wait.

If I just wanted to run firstTask, it would also run secondTask. I need the sequencing to happen outside of either task.

1
2
3
4
setTimeout(() => {
	firstTask()
	setTimeout(secondTask, 2000)
}, 2000)

Adding thirdTask in…

1
2
3
4
5
6
7
setTimeout(() => {
	firstTask()
	setTimeout(() => {
		secondTask()
		setTimeout(thirdTask, 2000)
	}, 2000)
}, 2000)

Now, when I change secondTask to three seconds I only do this:

1
2
3
4
5
6
7
setTimeout(() => {
	firstTask()
	setTimeout(() => {
		secondTask()
		setTimeout(thirdTask, 2000)
	}, 3000) // modified
}, 2000)

…And it works!


This solved my previous problem, but I still wasn’t completely happy with it.

For one, it’s hard to read. Every other line is stuffed with boilerplate. It’s clearer when you extend the pattern out like this:

1
2
3
4
5
6
7
8
9
setTimeout(() => { // boilerplate
	firstTask()
	setTimeout(() => { // boilerplate
		secondTask()
		setTimeout(() => { // boilerplate
			thirdTask()
		}, 2000)
	}, 2000)
}, 2000) 

This is hell.

From here, it’s easy to see the problem. I’ve got some repetitive code using setTimeout that I have to write before every task.

Contrast that with the naive solution from earlier:

1
2
3
setTimeout(firstTask, 2000)
setTimeout(secondTask, 4000)
setTimeout(thirdTask, 6000)

It’d be nice if I could chain these tasks together without all the boilerplate.

But, that’s not possible, is it?

Pipelines

Introducing the JS Promise – your one-stop shop to call-back for all your callback chaining needs! Call now, and it’ll call back as many times as you need!

Allow me to demonstrate.


Recipe: How to Cook a Call-Forward

Ingredients

  • A stack of boilerplates (covered in callbacks)
  • A properly prepared promise

Steps

1. Set your boilerplates down on the table

1
2
3
4
5
6
7
8
9
setTimeout(() => { // boilerplate
	firstTask()
	setTimeout(() => { // boilerplate
		secondTask()
		setTimeout(() => { // boilerplate
			thirdTask()
		}, 2000)
	}, 2000)
}, 2000) 

2. Flatten them out, one by one

1
2
3
4
5
6
7
8
9
setTimeout(() => { // boilerplate
	firstTask()
}, 2000)
setTimeout(() => { // boilerplate
	secondTask()
}, 2000)
setTimeout(() => { // boilerplate
	thirdTask()
}, 2000)

3. Promise each one you’ll have the resolve to call-back

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise((resolve, reject) => {
	setTimeout(() => { // boilerplate
		resolve(firstTask())
	}, 2000)
})

new Promise((resolve, reject) => {
	setTimeout(() => { // boilerplate
		resolve(secondTask())
	}, 2000)
})

new Promise((resolve, reject) => {
	setTimeout(() => { // boilerplate
		resolve(thirdTask())
	}, 2000)
})

4. Then enjoy your newly cooked call-forward

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new Promise((resolve, reject) => {
	setTimeout(() => { // boilerplate
		resolve(firstTask())
	}, 2000)
})
.then(() =>
	new Promise((resolve, reject) => {
		setTimeout(() => { // boilerplate
			resolve(secondTask())
		}, 2000)
	})
)
.then(() =>
	new Promise((resolve, reject) => {
		setTimeout(() => { // boilerplate
			resolve(thirdTask())
		}, 2000)
	})
)

Refactoring

Jokes aside, it’s now much easier to fix the boilerplate. Instead of writing each promise by hand, let’s put it in a function!

1
2
3
4
5
6
7
function setTimeoutPromise(func, ms) {
	return new Promise((resolve, reject) => {
		setTimeout(() => { // boilerplate (now in one place)
			resolve(func())
		}, ms)
	})
}

Now my call-forward *cough* pipeline *cough* looks like this:

1
2
3
setTimeoutPromise(firstTask, 2000)
.then(() => setTimeoutPromise(secondTask, 2000))
.then(() => setTimeoutPromise(thirdTask, 2000))

Modifying secondTask is easy:

1
2
3
setTimeoutPromise(firstTask, 2000)
.then(() => setTimeoutPromise(secondTask, 3000)) // modified
.then(() => setTimeoutPromise(thirdTask, 2000))

And adding fourthTask is too:

1
2
3
4
setTimeoutPromise(firstTask, 2000)
.then(() => setTimeoutPromise(secondTask, 3000))
.then(() => setTimeoutPromise(thirdTask, 2000))
.then(() => setTimeoutPromise(fourthTask, 2000)) // modified

The Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function firstTask() {
	console.log("Completed the first task")
}
function secondTask() {
	console.log("Completed the second task")
}
function thirdTask() {
	console.log("Completed the third task")
}

function setTimeoutPromise(func, ms) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve(func())
		}, ms)
	})
}

setTimeoutPromise(firstTask, 2000)
.then(() => setTimeoutPromise(secondTask, 2000))
.then(() => setTimeoutPromise(thirdTask, 2000))

My final solution. Beatiful, isn’t it?


So, yeah. Pipelines. Use them when you need to chain anything.

Wait, you still don’t understand Promises?

A Promise is just a monoid in the category of callbacks, what’s the problem?

This post is licensed under CC BY 4.0 by the author.
Contents