Functions-First

Functions-first is my name for an approach to coding that emphasizes finding the functions in what you are building first. Once all the functions have been identified you can move on to procedures. The idea is that all of your code lives in a function or procedure from the beginning — with emphasis on true functions — so that any of them can move anywhere without breaking any couplings at all.

📖 It’s worth a reminder at this point that a function is not a procedure even though this rampant misunderstanding is so wide-spread it has become codified in several modern languages. This is why I use the term procedural functions (an oxymoron to those in the know but it works), and true functions.

Summary

  1. Write everything as functions first if possible.
  2. Write the rest as procedures.
  3. Centralize data as separate simple structs in a model package.
  4. Only use methods for computed values.
  5. Use variadic function parameters sparingly.
  6. User interfaces extensively for compatibility.
  7. Favor strict typing over any run-time type evaluation.
  8. Factor and package early.

A Functions-First Hello World in Bash

The simplest Go code for hello world might be something like this:

package main

import "fmt"

func main() { fmt.Println("Hello world.") }

(The function main() doesn’t count.)

That’s all well and good. But almost no one would ever write that. Functions first builds on the idea that if something is worth capturing in code — not the command line — that it is worth putting into a function or procedure so that it can be reused and moved to other packages and libraries.

So this simple hello world would be something like this following functions-first:

package main

// main.go

import "fmt"

func main() { PrintHello() }
func PrintHello() { fmt.Println(HelloWorld()) }
func HelloWorld() string { return "Hello world." }

It is slight more code, but the value of approaching all your code design this way will inevitably save you hundreds of hours of refactoring and rewriting crusty code with massive technical debt later.

This is also ridiculously easy to test. To test the first you would need a complicated testing framework to see the output of command after it had been compiled. While it is possible, it is far more complicated than using the built in testable example of Go (and similar things in other languages).

Here’s were we really get to see Go outshine every other language by using the test framework that is built into the language.

package main

// main_test.go

import "fmt"

func ExampleHelloWorld() {
  fmt.Println(HelloWorld())

  // Output:
  //
  // Hello world.
}

💬 I’m using private testing from the same package rather than main_test to avoid having to figure out where this package would live on the Internet at the moment.

This brings out another major principle of functions-first: factor and package early.

You can see from our revised functions-first hello world that it is now almost begging to be refactored into a separate file and possibly package.

For this we will use the common Go practice of creating a cmd/ directory to hold our commands and separate them from the base library that is forming.

mkdir -p cmd/hello
cp main.go cmd/hello
mv main.go lib.go
mv main_test.go lib_test.go

Now we can clean up these files.

The command is tucked away and ultra simplified.


package main

// cmd/hello/main.go

func main() { PrintHello() }

The functions have been moved to a simple lib library package.

This is the holy grail of the functions-first approach. When done well you will have almost nothing in any of your package main commands because all the functionality has been put into functions that can be used by anything, anywhere (so long as you make them public). This flexibility is the perfect balance between over-abstraction and over-engineering and inflexible “write-once” programming.

package lib

// lib.go

import "fmt"

func PrintHello()        { fmt.Println(HelloWorld()) }
func HelloWorld() string { return "Hello world." }

And the test functions moved as well (but are still using the localized testing instead of forcing an import from a yet unknown repo URI).

package lib

// lib_test.go

import "fmt"

func ExampleHelloWorld() {
    fmt.Println(HelloWorld())

    // Output:
    //
    // Hello world.
}