Code seams are a way of creating places where you could re-use, test, or modify code. The term comes from clothing and textile production where a seam is a place where something can be taken apart and modified (like to fit a specific body), repaired, or replaced.

Let’s look at an example. Let’s take a problem where we have to get the total of numbers in a string that looks like this:

a, x, f, s, 12, s, 23, 19, 85, s, x, l, j, n, 100

We could solve this in the quickest way by doing this:

function main(){
    const tokens = "a, x, f, s, 12, s, 23, 19, 85, s, x, l, j, n, 100"
    let total = 0;
 
    for (const token of tokens.split(",")){
        const number = parseInt(token);
        if (!isNaN(number)){
            total += number
        }
    }
 
    console.log(total)
}
 
main();

This works! It gets us our answer but it’s also hard to test. We can create our first seam by separating the input from the process of solving.

function solve(input: string): number {
    let total = 0;
 
    for (const token of input.split(",")){
        const number = parseInt(token);
        if (!isNaN(number)){
            total += number
        }
    }
 
    return total
}
 
function main(){
    const tokens = "a, x, f, s, 12, s, 23, 19, 85, s, x, l, j, n, 100"
    let total = solve(tokens)
 
    console.log(total)
}
 
main();

The advantages of doing this are:

  • We have a testable function! We can now try out many different inputs to see if we get the expected output.
  • We can reuse the solve function in other places in our code
  • It’s slightly easier to understand what’s going on (although it’s also slightly longer, this is a constant tension in creating seams)

Let’s create another seam! Let’s imagine that the input string isn’t always separated with , sometimes it uses /. Let’s first refactor our code to pull out a new seam for parsing the input:

function parse(input: string): string[] {
    return input.split(",")
}
 
function solve(input: string): number {
    let total = 0;
 
    for (const token of parse(input)){
        const number = parseInt(token);
        if (!isNaN(number)){
            total += number
        }
    }
 
    return total
}
 
function main(){
    const tokens = "a, x, f, s, 12, s, 23, 19, 85, s, x, l, j, n, 100"
    let total = solve(tokens)
 
    console.log(total)
}
 
main();

Again this is longer but we now have a new place we can test or reuse. Let’s take it a step further, and add a second parsing function:

function parseWithSlash(input: string): string[] {
    return input.split("/")
}

We could write a second version of the function to use the new parseWithSlash function:

function solveWithSlash(input: string): number {
    let total = 0;
 
    for (const token of parseWithSlash(input)){
        const number = parseInt(token);
        if (!isNaN(number)){
            total += number
        }
    }
 
    return total
}

Or we could take advantage of the seam we’ve created and pass in the parse function to our existing solve:

function parseWithComma(input: string): string[] {
    return input.split(",")
}
 
function parseWithSlash(input: string): string[] {
    return input.split("/")
}
 
function solve(input: string, parseInput: (input: string) => string[]): number {
    let total = 0;
 
    for (const token of parseInput(input)){
        const number = parseInt(token);
        if (!isNaN(number)){
            total += number
        }
    }
 
    return total
}
 
 
function main(){
    const tokens = "a, x, f, s, 12, s, 23, 19, 85, s, x, l, j, n, 100"
    let total = solve(tokens, parseWithComma)
 
    const tokens2 = "a/x/f/s/12/s/23/19/85/s/x/l/j/n/100"
    let total2 = solve(tokens2, parseWithSlash)
 
 
    console.log(total)
    console.log(total2)
}
 
main();

We’ve taken advantage of the seam we created by breaking out the parse part of our program. We could even break out the parse function as a generic type (maybe we use that in other places?):

type ParseFn = (input: string) => string[]
 
function solve(input: string, parseInput: ParseFn): number {
    let total = 0;
 
    for (const token of parseInput(input)){
        const number = parseInt(token);
        if (!isNaN(number)){
            total += number
        }
    }
 
    return total
}

Towards Functional Programming

We could take another step and rewrite our solve function like this:

function solve(input: string, parseInput: ParseFn): number {
    return parseInput(input)
    .map((i: string) => {
        const number = parseInt(i);
        if (!isNaN(number)){
            return number
        }
        return null
    })
    .filter(token => token !== null)
    .reduce((total: number, n: number, _: number) => {
        return total + n
    }, 0)
}

“Gross!” you might think. And yes, when it’s written like this it’s kind of gross. It’s harder to look at. What’s going on here is that we’re parsing the input and getting back an array of strings. We then take each string and change it into either a number or null. then in the filter we throw away the nulls. Then finally we sum up the total by taking each value and adding it to a starting value of 0.

What if we start pulling out each of these things to its own function, though?

function sum(total: number | undefined = 0, n: number): number {
    return total + n
}
 
function solve(input: string, parseInput: ParseFn): number {
    return parseInput(input)
    .map((i: string) => {
        const number = parseInt(i);
        if (!isNaN(number)){
            return number
        }
        return null
    })
    .filter(token => token !== null)
    .reduce(sum)

Now we have a new seam at sum. We can test that summing two numbers or a undefined and a number always returns the total. That’s easy! And we might reuse it somewhere else.

The parseInt function is almost doing what we want. What if we just call parseInt and then let filter remove the NaN’s?

function solve(input: string, parseInput: ParseFn): number {
    return parseInput(input)
    .map(parseInt)
    .filter(token => isNaN(token))
    .reduce(sum)
}

This is a kind of function that I really like! I find it easy to understand what’s going on here. We’re:

  • Receiving a function that does parsing and returns an array of strings
  • We take each string and try to turn it into an Int
  • We filter out the ones that didn’t get turned into an int
  • We sum up all of the values by taking each value with the running total

I like this because:

  • Each little piece that this uses is testable somewhere else (except maybe the filter- maybe that should come out too?).
  • Each of the little pieces is so simple that it feels almost silly to write them into a function but by doing this we’re controlling complexity. If we can understand each little thing we can build up our understanding of the whole program in little steps
  • The pipeline reads like I would describe it to a person
  • I can reuse each piece somewhere else

All of this was possible because we created seams at different points in the program.

Full program listing:

function parseWithComma(input: string): string[] {
    return input.split(",")
}
 
function parseWithSlash(input: string): string[] {
    return input.split("/")
}
 
type ParseFn = (input: string) => string[]
 
function sum(total: number | undefined = 0, n: number): number {
    return total + n
}
 
function removeNaN(i: number): boolean {
    return !isNaN(i)
}
 
function solve(input: string, parseInput: ParseFn): number {
    return parseInput(input)
    .map(parseInt)
    .filter(removeNaN)
    .reduce(sum)
}
 
 
function main(){
    const tokens = "a, x, f, s, 12, s, 23, 19, 85, s, x, l, j, n, 100"
    let total = solve(tokens, parseWithComma)
 
    const tokens2 = "a/x/f/s/12/s/23/19/85/s/x/l/j/n/100"
    let total2 = solve(tokens2, parseWithSlash)
 
 
    console.log(total)
    console.log(total2)
}
 
main();