BattlefyBlogHistoryOpen menu
Close menuHistory

How to fix inside-out functional programming with pipe

Ronald Chen June 13th 2022

Img

As one initially starts practising functional programming with tiny single purpose pure functions, one gets to a point where the composition of those functions seems awkward.

Let's say we are extracting the results from a League of Legends match from the Riot API. In a functional programming style, we could break this down into a series of functions.

function extractResult(match: GetMatchResponseDto): RawResult {...

// converts things like mapId: 1 to mapName: "Summoner's Rift"
function humanizeGameConstants(result: RawResult): HumanizedResult {...

// calculates bonus points based off of first Dragon/Baron kill
function calculateBonusPoints(result: HumanizedResult): FinalResult {...

The actual code that composes these functions together would look like.

const {data: match} = await riotApi.getMatch(...)
const result = calculateBonusPoints(humanizeGameConstants(extractResult(match)))

But isn't it strange that the data flows right to left, which is backwards when reading the code from left to right? This is what I mean by inside-out functional programming. This consumes mental energy by having the reader start from the end.

We can attempt to solve this problem by introducing intermediate variables.

const {data: match} = await riotApi.getMatch(...)
const result1 = extractResult(match)
const result2 = humanizeGameConstants(result1)
const result = calculateBonusPoints(result2)

But then this introduces a new problem. result1/result2 are clearly poorly named and we could make up better ones. However, this is just noise. We don't care about the names for result1/result2, we only wanted the data flow to match the code flow.

What we really want is something that looks like,

const {data: match} = await riotApi.getMatch(...)
const result = match → extractResult → humanizeGameConstants → calculateBonusPoints

One might recognize this if rewritten in a different way.

curl https://riot.api.../matches/... |  ./extractResult | ./humanizeGameConstants | ./calculateBonusPoints > result

It's the pipe operator!

There are a few ways to use the pipe operator in our code today.

ts-belt pipe

import { pipe } from '@mobily/ts-belt'
...
const {data: match} = await riotApi.getMatch(...)
const result = pipe(
  match,
  extractResult,
  humanizeGameConstants,
  calculateBonusPoints
)

ramda pipe

import * as R from 'ramda'
...
const {data: match} = await riotApi.getMatch(...)
const result = R.pipe(
  extractResult,
  humanizeGameConstants,
  calculateBonusPoints
)(match)

Notice how ramda takes the approach of composing functions using pipe, whereas ts-belt takes a data-first approach where match is the first argument into pipe.

There is a proposed pipe operator for JavaScript.

const {data: match} = await riotApi.getMatch(...)
const result = match
  |> extractResult(%)
  |> humanizeGameConstants(%)
  |> calculateBonusPoints(%)

The work-in-progress syntax is definitely controversial, but it does solve our original problem having the data flow match the code flow.

Do you want to get more things done by saving your mental energy for more important things, like esports? You’re in luck, Battlefy is hiring.

2022

Powered by
BATTLEFY