Introducing Quick Reviews

Disillusioned

I’m introducing a new type of blog post under the label of quick reviews. My goal is to write down brief thoughts about the media that I consume, short enough that the entire contents of each post can appear on the main blog page. Keeping it short incentivizes me to do it more often, because I don’t have the pressure to write a lot.

The contents are what I would post on my Storygraph and Letterboxd profiles, but as I become increasingly disillusioned against the world of tech capitalism, I’m trying to own my data as much as I can. I’ll still use those platforms, but I want my words to belong to me and not live on someone else’s servers.

I’ve always preferred the more “mainstream” parts of the MCU over the offbeat energy of the Guardians series. This third one is my favourite of the trilogy. Rocket’s backstory is effectively dark and tragic, and especially hard to watch if you’re an animal lover, like me.

letterboxd link

A collection filled with the type of true crime article that I regularly save to Pocket. I probably wouldn’t recommend reading the book cover-to-cover, but I had to because it was a library loan. In my opinion, it would be better enjoyed by cherry-picking an article from time to time, when you’re in the mood for some moral indignation.

But beyond the morbid point-and-judge tone that’s typical of true crime, I do appreciate that Keefe offers empathy and compassion towards his so-called “rogues.” This is especially evident in my favourite article.

“Loaded Gun” explores the past of a mass shooter. Twenty years before the crime, she shot and killed her brother, in what may or may not have been an accident. I especially liked this article because Keefe inserts his first-person ruminations on the ambiguity of what happened, and what the ambiguity means.

storygraph link

Paper Girls

Brian K. Vaughn & Cliff Chiang

-

The Star Trek vision of time travel usually involved restoring the “correct” timeline after some mishap, and preserving the way that things are “supposed” to be. In the time travel story of Paper Girls, the forces that support timeline preservation are painted as villains, and the act of changing history is seen as a charitable act, a sort of temporal wealth redistribution where the advances of the future are shared with the past. I don’t think I agree with the ethics of this position, but it was an interesting inversion of what I’ve seen before.

My favourite parts of the story involved the teenage girls meeting adult versions of themselves. Because of my age, I identified more with the “guest stars” than the main cast, and found myself wondering what I would say to my younger self. I kind of wish that Paper Girls had offered more answers to this question, beyond “Keep in touch with your friends.”

storygraph link

I was a fan of the TV show The Good Place, and this book dives deeper into the philosophical concepts behind that show’s exploration of morality. The subject matter is pretty dry and basic (I was familiar with much of it from taking a 100-level philosophy course in university), but the author, who also created and ran the TV show, imbues his sitcom-honed sense of humour into it. A bonus if you’re listening to the audiobook: the Good Place cast comes along for the ride; a highlight is Ted Danson’s dramatic reading of chapter titles.

storygraph link

Is it bad form to criticize a novel for its font?1 Don’t answer that, because I’m going to do it anyway. If you open and flip through Interior Chinatown by Charles Yu, you’ll see that the entire book is set in a Courier-like typewriter font. This choice supposedly makes the text appear like a screenplay, because the story is about a character, Willis Wu, who is… uh… a character in a TV show? Let me come back to that.

As you continue to flip through the pages of the book, you will next see that there are indeed sections of dialogue which are formatted like a screenplay, where the character name (in uppercase), and the lines that they speak (in lowercase), are centred on the page.

					SPECIAL GUEST STAR

Right. I do.

						LEE

Well, what are we waiting for? Let's go.

Footnotes

  1. One of my favourite series of recent years, the Outline trilogy by Rachel Cusk, is typeset in Optima, a sans-serif which, while unusual and frankly not my favourite, did not detract from the experience of reading the prose.

Recently, I’ve been working on some social and gamification features in a web application. I decided to use a backend service called brainCloud instead of implementing all of the database stuff myself. The brainCloud service stores things like player statistics, leaderboards and social relationships (i.e. who’s friends with who?), and provides an HTTP API to read and write the data. They also provide a Javascript client library to make it easier to work with those APIs.

Without getting into too many specifics, the library follows a callback style for handling asynchronous behaviour. When you call one of their functions, it sends an HTTP request; when the server responds, it executes the code in the callback function that you pass as a parameter. For example,

postScoreToLeaderboard(leaderboardId, score, (resp) => {
	if (resp.successful) {
		// do something with the data
		console.log('We got data!', resp.data);
	} else {
		console.log('We got an error!', resp.error);
	}
});

Personally, I prefer to handle asynchronous code using promises in the await/async style.

A general pattern to follow to convert a callback interface to a promise-based one is to build a Promise object and return it.

(N.B.: For the purposes of this post, I’ll follow the convention of prefixing a function name with an underscore to indicate that it’s a “wrapper” around a brainCloud API function.)

const _postScoreToLeaderboard = (leaderboardId, score) => {
	return new Promise(resolve, reject) => {
		postScoreToLeaderboard(leaderboardId, score, (resp) => {
			if (resp.successful) {
				resolve(resp.data);
			} else {
				reject(resp.error);
			}
		});
	});
};

Now, we can use this function without a callback:

try {
	const data = await _postScoreToLeaderboard(leaderboardId, score);
	// do something with the data
	console.log('We got data!', data);
} catch (e) {
	console.log('We got an error!', e);
}

This works, but there are many functions in the brainCloud API. How can we extend this technique in a generic way?

To illustrate, here’s a couple of other brainCloud methods for gamification:

getLeaderboardTop(leaderboardId, (resp) => {
	// handle response
});

incrementUserStats(statId, value, (resp) => {
	// handle response
});

It would be a lot of repetitive code to wrap each of these in a function:

// TOO MUCH CODE!

const _getLeaderboardTop = (leaderboardId) => {
	return new Promise(resolve, reject) => {
		getLeaderboardTop(leaderboardId, (resp) => {
			// handle response
		})
	});
};

const _incrementUserStats = (statId, value) => {
	return new Promise(resolve, reject) => {
		incrementUserStats(statId, value, (resp) => {
			// handle response
		})
	});
};

To reduce duplication, we can curry the functions.

A brief introduction to currying

If you’re not familiar with the term, currying a function creates a new function with parameters that can be called one by one.

The classic example is a function that adds three numbers together.

const add = (a, b, c) => a + b + c;

// create a curried function
const curriedAdd = curry(add);

const addOne = curriedAdd(1);

// this is equivalent to calling add(1, 2, 3)
const x = addOne(2, 3);

// x is 6

Note that this only returns the final result of 6 if we “use up” all three parameters of the original function. If, instead, you leave the final parameter “unused,” it will return yet another function.

// we have "used up" two out of three parameters
const addSeven = addOne(6);

// this is equivalent to calling add(1, 6, 10)
const y = addSeven(10);

// y is 17

Back to our regularly scheduled programming

With this approach, we can curry the functions from the brainCloud API. For example,

const _incrementUserStats = curry(incrementUserStats);

This particular function has two input parameters, and a third for a callback. If we call the curried version with the two input parameters, it will return a function that accepts a callback and executes the original function once it receives the callback.

const fn = _incrementUserStats('statId', 15);

const handleResponse = (resp) => {
	// do something
};

// this is equivalent to calling 
// incrementUserStats('statId', 15, handleResponse)
fn(handleResponse);

I named the final function fn because this part can be generalized to any API call that takes a callback as its final parameter.

If we create a function that takes a function like fn and calls it, we can then use a Promise-based pattern for making these API calls.

const makeAPICall = (fn) => {
	return new Promise((resolve, reject) => {
		fn((resp) => {
			if (resp.successful) {
				resolve(resp.data);
			} else {
				reject(resp.error);
			}
		});
	});
};

To use this, we call one of the curried functions with all of its parameters except for the final callback parameter, and pass the result to the makeAPICall function.

try {
	const data = await makeAPICall(_incrementUserStats('statId', 15));
	// do something with the result
	console.log('We got data!', data);
} catch (e) {
	console.log('We got an error!', e)
}

Putting it all together:

// curry all of the functions that we want to use
const _postScoreToLeaderboard = curry(postScoreToLeaderboard);
const _getLeaderboardTop = curry(getLeaderboardTop);
const _incrementUserStats = curry(incrementUserStats);

// define the function that receives a callback 
// and executes the original function
const makeAPICall = (fn) => {
	return new Promise((resolve, reject) => {
		fn((resp) => {
			if (resp.successful) {
				resolve(resp);
			} else {
				reject(resp);
			}
		});
	});
};

// now we can make API calls like this
const result1 = await makeAPICall(_postScoreToLeaderboard('id', 100));
const result2 = await makeAPICall(_getLeaderboardTop('id'));
const result3 = await makeAPICall(_incrementUserStats('statId', 15));

As a final note, the curry function itself is available in the popular lodash library of Javascript utilities.

Albert

About Me

Hi! Albert here. Canadian. Chinese.

Writing software since 2001. “Blogging” since 2004. Reading since forever.

You can find me on socials with the links below, or contact me here.