DEV Community

Yusuf Yalım
Yusuf Yalım

Posted on

Functional Hello World in Go: A Journey from First Class Functions to Function Composition

Picture this: You're a friendly person who loves greeting everyone around you. But as a programmer, you also love efficiency and elegance. What if I told you that Go's treatment of functions as first-class citizens could help you build the perfect greeting system that evolves from simple hellos to sophisticated function composition? Let's embark on this journey together!

Welcome in if you understand Go basics - like creating variables, typing strings, invoking functions, how to loop and use generics. If you don't, you want to take A Tour of Go and comeback here.

What Does "First-Class Citizen" Even Mean?

In programming languages, when we say something is a "first-class citizen," we mean it has all the capabilities available to other entities. For functions, this means they can be:

  • Assigned to variables (your greeting becomes a reusable tool)
  • Passed as arguments to other functions (automation magic)
  • Returned as values from functions (function factories)
  • Created at runtime (dynamic behavior)
  • Composed together to create new functions (the mathematical f(g(x)) pattern)

Go treats functions exactly like this, making them incredibly powerful and flexible. By the end of our journey, you'll see how these capabilities combine to let you solve the real-world - like greeting pets loudly enough for them to hear from a block away, while keeping family greetings respectfully quiet - and the imaginary-world - apply same greeting problem to unicorns and gnomes - problems all through the elegant art of function composition!

Chapter 1: The Simple Salutation - Functions as Variables

You start with a basic need: greeting the world. Being the smart programmer you are, you realize you can store a function in a variable just like any other value:

package main
import "fmt"

var helloWorld = func() {
    fmt.Print("HELLO WORLD\n")
}

func main() {
    helloWorld()
}
Enter fullscreen mode Exit fullscreen mode

This prints our cheerful hello world message. But wait - you're too smart to stop here! Why limit yourself to just "hello" when you could be more versatile?

package main
import "fmt"

var salute = func(salutation string) {
    fmt.Printf("%v, WORLD\n", salutation)
}

func main() {
    salute("HELLO")
    salute("HI") 
    salute("GOOD MORNING")
}
Enter fullscreen mode Exit fullscreen mode

Now you can greet the world in multiple ways! The function is stored in a variable and accepts parameters, making it reusable and flexible.

Chapter 2: The Realization - Functions as Return Values

But then reality hits you. Screaming "HELLO WORLD!" at the top of your lungs isn't exactly practical. People might think you've lost your mind! Plus, wouldn't it be better to greet specific people by name?

This is where your programming wisdom shines. Instead of just adding another parameter, you discover the power of higher-order functions - functions that can return other functions:

package main
import "fmt"

var salute = func(salutation string) func(string) {
    return func(name string) {
        fmt.Printf("%v, %v!\n", salutation, name)
    }
}

func main() {
    hello := salute("Hello")
    hello("Cat")
    hello("Dog")

    hi := salute("Hi")
    hi("Mom")
    hi("Dad")

    goodMorning := salute("Good morning")
    goodMorning("Vietnam")
}
Enter fullscreen mode Exit fullscreen mode

This produces:

Hello, Cat!
Hello, Dog!
Hi, Mom!
Hi, Dad!
Good morning, Vietnam!
Enter fullscreen mode Exit fullscreen mode

Beautiful! You've created a function factory. The salute function remembers your chosen salutation style and returns a personalized greeting function. This is closure in action - the returned function "closes over" the salutation parameter, keeping it alive even after the outer function returns.

Chapter 3: The Automation Dream - Functions as Parameters

As a thoughtful person, you realize that greeting everyone individually every day is time-consuming, yet everyone you care about deserves attention. Being the efficiency-loving programmer you are, you think: "Why not automate this?"

Enter functions as parameters - the final piece of the first-class citizen puzzle:

package main
import "fmt"

var salute = func(saluting string) func(string) {
    return func(name string) {
        fmt.Printf("%v, %v!\n", saluting, name)
    }
}

func salutingFactory(saluteFunction func(string) func(string)) {
    hello := saluteFunction("Hello")
    hello("Cat")
    hello("Dog")

    hi := saluteFunction("Hi")
    hi("Mom")
    hi("Dad")

    goodMorning := saluteFunction("Good morning")
    goodMorning("Vietnam")
}

func main() {
    salutingFactory(salute)
}
Enter fullscreen mode Exit fullscreen mode

Now you've passed your entire saluting system as a parameter to another function! The salutingFactory doesn't need to know the specific implementation details of how saluting works - it just knows it received a function that can create greeting functions.

Chapter 4: The Distance Problem - Function Composition

A few days later, disaster strikes! Your dog and cat get annoyed by your constant saluting and move a block away. Now you need to SHOUT so they can hear you, but you can't shout at your mom, dad and people in Vietnam - that would be rude!

This is where function composition comes to the rescue. You realize you can handle this by composing a shouting function with your greeting function, but first you need to refactor your saluting function to return strings instead of printing directly:

package main
import "fmt"

var salute = func(saluting string) func(string) string {
    return func(name string) string {
        return fmt.Sprintf("%v, %v", saluting, name)
    }
}

func print(str string) {
    fmt.Println(str)
}

func salutingFactory(saluteFunction func(string) func(string) string) {
    hello := salute("Hello")
    print(hello("Cat"))
    print(hello("Dog"))

    hi := salute("Hi")
    print(hi("Mom"))
    print(hi("Dad"))

    goodMorning := salute("Good morning")
    print(goodMorning("Vietnam"))
}

func main() {
    salutingFactory(salute)
}
Enter fullscreen mode Exit fullscreen mode

Did you notice what we just did? We're doing the trick from high school math like f(g(x))! While we're moving toward function composition, we need something more powerful to truly compose functions.

Enter the magical compose function - the Swiss Army knife of functional programming:

func compose[A any, B any, C any](f func(B) C, g func(A) B) func(A) C {
    return func(a A) C {
        return f(g(a)) // f(g(x))
    }
}
Enter fullscreen mode Exit fullscreen mode

This generic compose function takes two functions and creates a new function that applies them in sequence: first g, then f on the result. It's like a mathematical pipeline: data flows from right to left, just like in math notation f(g(x)).

Now we can create a shout function and compose it with our greeting functions:

package main
import (
    "fmt"
    "strings"
)

var salute = func(saluting string) func(string) string {
    return func(name string) string {
        return fmt.Sprintf("%v, %v", saluting, name)
    }
}

func print(str string) {
    fmt.Println(str)
}

func shout(str string) string {
    return strings.ToUpper(str)
}

func compose[A any, B any, C any](f func(B) C, g func(A) B) func(A) C {
    return func(a A) C {
        return f(g(a)) // f(g(x))
    }
}

func salutingFactory(saluteFunction func(string) func(string) string) {
    hello := salute("Hello")
    shoutHello := compose(shout, hello)  // Creates a shouting greeting function!
    print(shoutHello("Cat"))
    print(shoutHello("Dog"))

    hi := salute("Hi")
    print(hi("Mom"))
    print(hi("Dad"))

    goodMorning := salute("Good morning")
    print(goodMorning("Vietnam"))
}

func main() {
    salutingFactory(salute)
}
Enter fullscreen mode Exit fullscreen mode

This outputs:

HELLO, CAT
HELLO, DOG
Hi, Mom
Hi, Dad
Good morning, Vietnam
Enter fullscreen mode Exit fullscreen mode

Perfect! Now your cats and dogs can hear you from a block away with the shouted greetings, while your family gets the respectful, normal-volume treatment. Function composition lets you mix and match behaviors like building blocks, creating exactly the right function for each situation.

Chapter 5: The Efficiency Wizards - Advanced Function Composition

This is perfect! But then you realize your saluting factory isn't efficient enough. What if the government decides to implement a "printing tax" - charging you every time you typed print? You need a more powerful compose function that can handle multiple functions at once!

Being the wizard you now are (yes, you're no longer just a smart guy. By discovering secret composition techniques you've earned your Wizard status!), you create an even more magical compose function:

func compose[T any](fns ...func(T) T) func(T) T {
    return func(input T) T {
        result := input
        // Execute from right to left: last function first
        for i := len(fns) - 1; i >= 0; i-- {
            result = fns[i](result)
        }
        return result
    }
}
Enter fullscreen mode Exit fullscreen mode

This variadic compose function can take any number of functions and chain them together! It executes from right to left (just like mathematical composition), so the last function in the list runs first.

To make this work with our printing workflow, we need to modify the print function to return the string it prints:

func print(str string) string {
    fmt.Println(str)
    return str
}
Enter fullscreen mode Exit fullscreen mode

Now behold the final, most elegant version of our greeting system:

package main
import (
    "fmt"
    "strings"
)

var salute = func(saluting string) func(string) string {
    return func(name string) string {
        return fmt.Sprintf("%v, %v", saluting, name)
    }
}

func print(str string) string {
    fmt.Println(str)
    return str
}

func shout(str string) string {
    return strings.ToUpper(str)
}

func compose[T any](fns ...func(T) T) func(T) T {
    return func(input T) T {
        result := input
        // Execute from right to left: last function first
        for i := len(fns) - 1; i >= 0; i-- {
            result = fns[i](result)
        }
        return result
    }
}

func salutingFactory(saluteFunction func(string) func(string) string) {
    hello := salute("Hello")
    shoutHello := compose(print, shout, hello)
    shoutHello("Cat")
    shoutHello("Dog")

    hi := salute("Hi")
    sayHi := compose(print, hi)
    sayHi("Mom")
    sayHi("Dad")

    goodMorning := salute("Good morning")
    sayGoodMorning := compose(print, goodMorning)
    sayGoodMorning("Vietnam")
}

func main() {
    salutingFactory(salute)
}
Enter fullscreen mode Exit fullscreen mode

Now you have the ultimate greeting system! You can compose any number of transformations together - print, shout, hello - all in one elegant function call. No more "printing tax" worries, and your code is cleaner and more expressive than ever.

The Beauty of First-Class Functions

Through this complete journey from simple greetings to automated salutation systems and finally to advanced function composition wizardry, we've seen how Go's treatment of functions as first-class citizens enables:

Flexibility: Functions can be stored, passed around, and created dynamically, making your code adaptable to different situations.

Reusability: By parameterizing behavior through functions, you can create highly reusable components.

Abstraction: Higher-order functions let you separate the "what" from the "how," making your code more maintainable.

Composability: Functions can be combined and composed in powerful ways, letting you build complex behavior from simple building blocks - as we saw with our greeting transformations.

Modularity: Each function has a single responsibility, making your code easier to test, debug, and maintain.

Beyond Greetings: Real-World Applications

While our greeting system is charming, first-class functions in Go power many real-world scenarios:

  • Event handlers: Pass different callback functions to handle various events
  • Middleware: Chain functions together to process HTTP requests
  • Functional programming pipelines: Use variadic compose functions for complex data transformations

The Takeaway

Functions as first-class citizens in Go aren't just a fancy language feature - they're a powerful tool for writing elegant, flexible, and maintainable code. Whether you're greeting the world or building complex systems, treating functions as values opens up a world of possibilities.

Next time you find yourself copying and pasting similar code, remember our salutation journey and the wizard powers you've gained. Ask yourself: "Could I pass this behavior as a function instead? Could I compose these operations together?" Often, the answer will lead you to cleaner, more elegant solutions.

After all, in Go, functions aren't just code you call - they're values you can cherish, store, pass around, compose into beautiful pipelines, and use to solve real-world problems efficiently. Just like a perfect greeting delivered at exactly the right volume, they bring everything together with style, grace, and mathematical elegance.

Top comments (0)