DEV Community

Cover image for Functional Programming Face-Off: Python vs JavaScript vs Go!
Mihai Farcas
Mihai Farcas

Posted on

Functional Programming Face-Off: Python vs JavaScript vs Go!

Functional programming has been gaining popularity in recent years, and for good reason. It offers a number of benefits over traditional imperative programming, such as improved code readability, testability, and maintainability.

In this article, we'll compare and contrast how three popular programming languages - Python, JavaScript, and Go - handle functional programming concepts. We'll explore how each language supports features like higher-order functions, immutability, and function composition, and discuss their strengths and weaknesses.

If you'd rather watch than read:

Check out the accompanying YouTube video!

1. Defining and Calling Functions

At the heart of functional programming lies, unsurprisingly, the function. The way functions are defined and invoked can significantly impact a language's suitability for functional programming paradigms. We'll examine how Python, JavaScript, and Go handle function definitions, looking at syntax, typing, and overall structure, setting the stage for more advanced functional concepts. Specifically how similar (or different) the syntax is, and the implications of different approaches to type systems (static, dynamic, type hints).

Python

def square(i: int) -> int:
    return i * i

result = square(5)
print(result)
Enter fullscreen mode Exit fullscreen mode

In Python, functions are defined using the def keyword followed by the function name, parentheses for parameters, and a colon. The code block within the function is indented. Type hints can be optionally added to specify the expected types of parameters and return values.

JavaScript

const square = i => i * i;

const result = square(5);
console.log(result);

Enter fullscreen mode Exit fullscreen mode

JavaScript offers a more compact syntax for defining functions, particularly with arrow functions. Arrow functions use the => operator, providing a concise way to express a function's logic.

Go

package main

import (
    "fmt"
)

func square(i int) int {
    return i * i
}

func main() {
    result := square(5)
    fmt.Println(result)
}
Enter fullscreen mode Exit fullscreen mode

Go, a statically-typed language, requires explicit type declarations for both function parameters and return values. Functions are defined using the func keyword, followed by the function name, a parenthesized list of parameters (with their types), and the return type. The function body is enclosed in curly braces {}. Go's syntax is very similar to python in that it uses a keyword, followed by the name, followed by parameters. However, Go's strict typing and the need for explicit imports, even for basic operations like printing to the console (using the fmt package), introduce a slightly higher level of formality compared to Python and JavaScript. The Go example defines a function that, like the Python and JavaScript counterparts, takes an integer and returns its square.

However, Go differs significantly in its overall structure. Every Go file belongs to a package, and the package main declaration is special. It signifies that the file is part of the main package, which is the entry point for executable programs. Without package main and the func main() within it, Go wouldn't know where to begin executing your code. It's a fundamental difference from Python and JavaScript, where you don't need such explicit entry point declarations for simple scripts. Another key distinction is Go's requirement for explicit imports, even for basic functionalities like printing to the console. In the provided example, fmt.Println is used, requiring an import of the fmt (format) package.

2. Higher-Order Functions

This section delves into a cornerstone of functional programming: treating functions as first-class citizens. This means functions can be passed as arguments to other functions, returned as values from functions, and generally treated like any other variable. A direct consequence of this is the ability to create higher-order functions – functions that either take other functions as arguments or return them (or both). This allows for highly flexible and reusable code. We will also be touching on Closures, though the dedicated section for them comes later.

Python

def applyOperation(operation, a: int, b: int) -> int:
    return operation(a, b)


def add(a: int, b: int) -> int:
    return a + b


def subtract(a: int, b: int) -> int:
    return a - b


result = applyOperation(add, 10, 5)
print(result)  # Output: 15

result = applyOperation(subtract, 10, 5)
print(result)  # Output: 5
Enter fullscreen mode Exit fullscreen mode

Python supports higher-order functions elegantly. In the Python snippet, applyOperation is a higher-order function because it takes another function (operation) as an argument. add and subtract are regular functions that perform addition and subtraction, respectively. applyOperation then calls the passed-in function (operation) with the provided arguments (a and b). Notice that the type hints are optional in Python. While the example does use type hints for clarity (specifying that operation should be a function taking two integers and returning an integer, and that a and b are integers), they are not enforced at runtime by default. This makes the Python code concise and similar in that aspect to JavaScript.

JavaScript

function applyOperation(operation, a, b) {
  return operation(a, b);
}

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

let result = applyOperation(add, 10, 5);
console.log(result); // Output: 15

result = applyOperation(subtract, 10, 5);
console.log(result); // Output: 5
Enter fullscreen mode Exit fullscreen mode

JavaScript, like Python, treats functions as first-class citizens and readily supports higher-order functions. The JavaScript snippet demonstrates the same applyOperation, add, and subtract structure as the Python example. applyOperation accepts a function (operation) and calls it with arguments a and b. Because JavaScript is dynamically typed, there are no type annotations. The code is very concise and mirrors the Python example in its functional approach.

Go

package main

import "fmt"

func applyOperation(operation func(int, int) int, a int, b int) int {
    return operation(a, b)
}

func add(a, b int) int {
    return a + b
}

func subtract(a, b int) int {
    return a - b
}

func main() {
    result := applyOperation(add, 10, 5)
    fmt.Println(result) // Output: 15

    result = applyOperation(subtract, 10, 5)
    fmt.Println(result) // Output: 5
}
Enter fullscreen mode Exit fullscreen mode

Go also supports higher-order functions, although with its characteristic explicitness due to static typing. In the Go snippet, applyOperation is again the higher-order function. However, the type of the operation parameter is explicitly declared: func(int, int) int. This signifies that operation must be a function that takes two int arguments and returns an int. This explicit type declaration is a key difference from Python (with optional type hints) and JavaScript (dynamically typed). The add and subtract functions are defined with their types, and applyOperation calls the provided function just like in the other languages. While all three languages achieve the same result, Go's static typing makes the function signatures more verbose. This reinforces Go's emphasis on explicitness and compile-time type safety.

3. Closures

Closures are a powerful feature often used in conjunction with higher-order functions. A closure is a function that "remembers" its surrounding environment (its lexical scope) even after the outer function that created it has finished executing. This means the inner function can access and modify variables defined in the outer function's scope, even if the outer function has returned. This ability to "capture" state is crucial for many functional programming patterns.

Python

def createCounter():
    count = 0

    def counter():
        nonlocal count  # count is not a local variable,
        # it refers to the count variable in the outer scope
        count += 1
        return count

    return counter


counter = createCounter()
print(counter())
print(counter())
Enter fullscreen mode Exit fullscreen mode

The Python snippet demonstrates a classic closure example with createCounter. createCounter is a function that returns another function, counter. The key here is the nonlocal count declaration within counter. Because Python has stricter scoping rules, you need to explicitly tell the inner function (counter) that count is not a local variable within counter itself, but rather refers to the count variable in the enclosing *scope of createCounter. Without nonlocal, Python would create a new, local count variable inside counter, and the outer count would remain unchanged. The returned counter function *"closes over" the count variable, maintaining its state between calls.

JavaScript

const createCounter = () => {
  let count = 0;
  return () => {
    count++;
    return count;
  };
};

const counter = createCounter();
console.log(counter());
console.log(counter());
Enter fullscreen mode Exit fullscreen mode

JavaScript handles closures very naturally. In the JavaScript snippet, createCounter also returns an anonymous function (the closure). This inner function increments and returns the count variable. Crucially, JavaScript's scoping rules automatically allow the inner function to access and modify the count variable from the createCounter scope, even after createCounter has completed. There's no need for a special keyword like Python's nonlocal; the closure behavior is implicit. This makes the JavaScript code very concise.

Go

package main

import "fmt"

func createCounter() func() int {
    count := 0
    return func() int {
            count++
            return count
    }
}

func main() {
    counter := createCounter()
    fmt.Println(counter())
    fmt.Println(counter())
}
Enter fullscreen mode Exit fullscreen mode

Go's closure mechanism is similar to JavaScript's. The Go snippet defines createCounter, which returns an anonymous function that increments and returns the count variable. Like JavaScript, and unlike Python, there's no need for any special declaration to indicate that count is from the outer scope. The inner function automatically has access to and can modify count. The returned function "closes over" the count variable, preserving its state between invocations. Go's approach, like JavaScript's, is straightforward and doesn't require extra keywords for closure behavior. The syntax, returning a func() int, clearly expresses that createCounter returns a function that takes no arguments and returns an integer.

4. Partial Application

Partial application is a technique for creating new functions from existing ones by pre-filling some of their arguments. It's a way to specialize a general function into a more specific one. Instead of calling a function with all its arguments at once, you "fix" some of the arguments, creating a new function that expects only the remaining arguments. This can lead to more reusable and readable code.

Python

def createGreeting(greeting: str):
    def greetingFn(name: str) -> str:
        return f"{greeting} {name}"

    return greetingFn


firstGreeting = createGreeting("Well, hello there ")
secondGreeting = createGreeting("Hola ")
print(firstGreeting("Remi"))
print(secondGreeting("Ana"))
Enter fullscreen mode Exit fullscreen mode

In the Python snippet, createGreeting is a function that demonstrates partial application. It takes a greeting string as input and returns a new function (greetingFn). This returned function takes a name string and combines it with the pre-filled greeting. When you call createGreeting("Well, hello there "), you're not getting the final greeting string; you're getting a new function (firstGreeting) that has the "Well, hello there " greeting already "baked in." You can then call firstGreeting with different names to get the complete greeting. The same principle applies to secondGreeting, prefilling a different initial greeting. The type hints help show the flow: createGreeting takes a string and returns a function that takes a string and returns a string.

JavaScript

const createGreeting = (greeting) => {
  return (name) => {
    return `${greeting} ${name}`;
  };
};

const firstGreeting = createGreeting("Well, hello there ");
const secondGreeting = createGreeting("Hola ");
console.log(firstGreeting("Remi"));
console.log(secondGreeting("Ana"));
Enter fullscreen mode Exit fullscreen mode

The JavaScript snippet achieves the same result using a concise arrow function syntax. createGreeting takes a greeting and returns another arrow function that takes a name. This inner function uses template literals (backticks) to create the final greeting string. Like the Python example, calling createGreeting with a greeting creates a new, specialized function (firstGreeting, secondGreeting) with that greeting pre-applied. The nested arrow functions clearly illustrate the process of creating a function that returns another function.

Go

package main

import "fmt"


func createGreeting(greeting string) func(string) string {
    return func(name string) string {
            return fmt.Sprintf("%s %s", greeting, name)
    }
}

func main() {
    firstGreeting := createGreeting("Well, hello there ")
    secondGreeting := createGreeting("Hola ")
    fmt.Println(firstGreeting("Remi"))
    fmt.Println(secondGreeting("Ana"))
}
Enter fullscreen mode Exit fullscreen mode

The Go snippet implements partial application in a similar manner. createGreeting takes a greeting string and returns a function that takes a name string and returns the combined string. Go's explicit type signature, func(string) string, clearly indicates that createGreeting returns a function. The use of fmt.Sprintf is Go's way of formatting strings, analogous to Python's f-strings and JavaScript's template literals. The result is the same: firstGreeting and secondGreeting become specialized functions with their respective greetings pre-filled. Go's syntax, while slightly more verbose due to type declarations, achieves the same functional goal.

5. Currying

Currying is a technique closely related to partial application, but with a subtle, yet important, difference. While partial application allows you to pre-fill any number of arguments, currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument. Each function in the sequence returns the next function in the sequence, until all arguments have been supplied, at which point the final result is returned. Currying isn't built-in to any of these languages; we need to implement a curry function ourselves.

Python

def curry(fn):
    def curried(*args):
        if len(args) >= fn.__code__.co_argcount:
            return fn(*args)
        else:
            return lambda *args2: curried(*(args + args2))

    return curried


def add(a: int, b: int, c: int) -> int:
    return a + b + c


result = curry(add)(1)(2)(3)
print(result)  # Output: 6
Enter fullscreen mode Exit fullscreen mode

The Python snippet implements a general-purpose curry function. This function is more complex than the previous examples because it needs to handle functions with an arbitrary number of arguments. It uses fn.__code__.co_argcount to determine the number of arguments the original function (fn) expects. The inner curried function uses *args to collect arguments. If enough arguments have been collected (the length of args is greater or equal to the number of arguments that fn expects), it calls the original function fn with those arguments. Otherwise, it returns a new lambda function that takes more arguments (*args2), combines them with the existing arguments (args + args2), and recursively calls curried again. This recursive process continues until all arguments are provided. The add function, which takes three arguments, is then curried and called with one argument at a time: curry(add)(1)(2)(3).

JavaScript

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return (...args2) => curried.apply(this, args.concat(args2));
    }
  };
}

function add(a, b, c) {
  return a + b + c;
}

const result = curry(add)(1)(2)(3);
console.log(result); // Output: 6
Enter fullscreen mode Exit fullscreen mode

The JavaScript snippet provides a similar curry function implementation. It uses fn.length to get the number of arguments the original function expects. The curried function uses the rest parameter syntax (...args) to gather arguments. If enough arguments are present, it calls the original function fn using apply (to handle the this context correctly). Otherwise, it returns a new function that takes more arguments (...args2), concatenates them with the existing arguments using concat, and recursively calls curried. Like the Python version, this allows us to call a curried function with one argument at a time, as shown with curry(add)(1)(2)(3). The Javascript and Python versions are strikingly similar.

Go

The Go snippet presents a significantly more complex implementation of curry. This is where the differences between Go's static typing and the dynamic typing of Python and JavaScript become most apparent. Because Go is statically typed, creating a generic curry function that works with functions of any arity (number of arguments) and any type requires using the reflect package. Reflection allows us to inspect and manipulate types at runtime, which is necessary because the curry function doesn't know the types of the function it's currying beforehand.

package main

import (
    "fmt"
    "reflect"
)


func curry(f interface{}) interface{} {
    ft := reflect.TypeOf(f)
    if ft.Kind() != reflect.Func {
        panic("curry: argument must be a function")
    }

    numArgs := ft.NumIn()
    args := make([]reflect.Value, 0, numArgs)

    var curried func(x reflect.Value) interface{}
    curried = func(x reflect.Value) interface{} {
        args = append(args, x)
        if len(args) == numArgs {
            return reflect.ValueOf(f).Call(args)[0].Interface()
        } else {
            return curried
        }
    }

    return curried
}

func add(a, b, c int) int {
    return a + b + c
}

func main() {
    curriedAdd := curry(add)

    add1 := curriedAdd.(func(reflect.Value) interface{})(reflect.ValueOf(1))
    add12 := add1.(func(reflect.Value) interface{})(reflect.ValueOf(2))
    result := add12.(func(reflect.Value) interface{})(reflect.ValueOf(3)).(int)

    fmt.Println(result) // Output: 6
}
Enter fullscreen mode Exit fullscreen mode

The curry function first checks if the input f is actually a function using reflect.TypeOf(f).Kind() != reflect.Func. It then determines the number of expected arguments using ft.NumIn(). It uses a slice args (of type reflect.Value) to store the accumulated arguments. The curried function (a closure) is defined recursively. When called with an argument x, it appends x to args. If enough arguments have been collected, it calls the original function using reflect.ValueOf(f).Call(args)[0].Interface(). This line is crucial: it converts the function f to a reflect.Value, calls it with the collected arguments, extracts the result (which is also a reflect.Value), and then converts it back to a regular Go interface{} using .Interface(). If not enough arguments have been collected, it returns itself (curried), continuing the currying process.

The main function shows how to use the curried function. Because the curry function returns the most generic type interface{}, we need multiple type assertions (e.g., .(func(reflect.Value) interface{})) to convert the intermediate curried functions to their correct types before we can call them. This is much more verbose than the Python and JavaScript examples, highlighting the added complexity of implementing currying in a statically-typed language without generics (prior to Go 1.18). The final result is retrieved and printed, demonstrating that the currying works, but at the cost of significantly more complex code. This example clearly shows that functional programming patterns like currying, while possible in Go, are often more natural and concise in dynamically typed languages.

6. Immutability

Immutability is a core principle of functional programming. An immutable object is one whose state cannot be changed after it's created. This contrasts with mutable objects, which can be modified in place. Immutability simplifies reasoning about code, prevents unintended side effects, and is crucial for concurrent programming (as immutable data is inherently thread-safe). Different languages have varying levels of built-in support for immutability.

Python

import copy

points1 = [{"x": 1, "y": 2}, {"x": 3, "y": 4}]

# Shallow Copy
points2 = copy.copy(points1)
points2.append({"x": 5, "y": 6})  # Modifies only points2
points2[0]["x"] = 10  # Modifies the object in both lists

print(points1)  # Output: [{'x': 10, 'y': 2}, {'x': 3, 'y': 4}] (affected!)
print(points2)  # Output: [{'x': 10, 'y': 2}, {'x': 3, 'y': 4}, {'x': 5, 'y': 6}]

# Deep Copy
points3 = copy.deepcopy(points1)
points3[0]["x"] = 20  # Modifies only points3

print(points1)  # Output: [{'x': 10, 'y': 2}, {'x': 3, 'y': 4}] (unaffected)
print(points3)  # Output: [{'x': 20, 'y': 2}, {'x': 3, 'y': 4}]
Enter fullscreen mode Exit fullscreen mode

Python's built-in data structures, like lists and dictionaries, are mutable. This means you can modify them directly after creation. The Python snippet highlights the importance of understanding the difference between shallow and deep copies when working with nested mutable objects.

  • Shallow Copy (copy.copy()): A shallow copy creates a new top-level object, but the elements within that object are still references to the original objects. So, if you have a list of dictionaries, a shallow copy will create a new list, but the dictionaries inside the new list will be the same dictionaries as in the original list. Modifying a nested object in the shallow copy will affect the original object as well. As shown, appending to points2 affects only the copy because append creates a new array, but modifying points2[0]["x"] modifies the underlying object referenced by both points1 and points2.

  • Deep Copy (copy.deepcopy()): A deep copy recursively creates new copies of all objects, including nested ones. This means modifying a nested object in a deep copy will not affect the original object. As shown, after a deep copy, modifying points3[0]["x"] only affects points3, leaving points1 unchanged. You need to explicitly use copy.deepcopy() to achieve true immutability with nested mutable objects in Python.

JavaScript

const points1 = [{ x: 1, y: 2 }, { x: 3, y: 4 }];
const points2 = [...points1]; // Creates a shallow copy

points2.push({ x: 5, y: 6 }); // Modifies only points2
points2[0].x = 10; // Modifies the object in both arrays

console.log(points1); // Output: [{ x: 10, y: 2 }, { x: 3, y: 4 }] (affected!)
console.log(points2); // Output: [{ x: 10, y: 2 }, { x: 3, y: 4 }, { x: 5, y: 6 }]
Enter fullscreen mode Exit fullscreen mode

JavaScript, like Python, has mutable objects (arrays and objects) by default. The JavaScript snippet uses the spread operator (...) to create a shallow copy of an array of objects.

  • Spread Operator for Arrays: The spread operator, when used with an array, creates a shallow copy. Similar to Python's copy.copy(), it creates a new array, but the objects within the array are still references to the same objects in the original array. Appending to points2 doesn't modify points1, as a new array instance is created, but changing points2[0].x does modify the corresponding object in points1 because it's a shared reference.

  • Deep Copying in JavaScript: JavaScript doesn't have a built-in deep copy function like Python's copy.deepcopy(). You typically need to use a custom function or a library (like Lodash's _.cloneDeep) to perform a deep copy. One common (but potentially problematic) approach is to use JSON.parse(JSON.stringify(object)), but this only works for objects that can be safely serialized to JSON (no functions, circular references, etc.). The lack of a standard, built-in deep copy mechanism is a notable weakness of JavaScript in terms of supporting immutability.

Go

package main

import "fmt"

type Point struct {
    X int
    Y int
}

func main() {
    points1 := []Point{{1, 2}, {3, 4}}
    points2 := points1 // Creates a copy of the slice header and underlying array

    // Appending creates a new slice, doesn't modify points1
    points2 = append(points2, Point{5, 6})

    // Modifying an element in points2 creates a copy of that element.
    points2[0].X = 10

    fmt.Println(points1) // Output: [{1 2} {3 4}] (original unaffected)
    fmt.Println(points2) // Output: [{10 2} {3 4} {5 6}]
}
Enter fullscreen mode Exit fullscreen mode

Go takes a different approach to immutability. Go's structs are value types. When you assign a struct to a new variable or pass it to a function, a copy of the struct is created. This promotes immutability by default.

  • Value Types: In the Go snippet, Point is a struct. When points2 := points1 is executed, points2 receives a copy of the slice header. Importantly, in this case, the underlying array elements (the Point structs) are also copied because they are value types.

  • append: The append function in Go is designed to work well with immutability. When you append to a slice, Go might create a new underlying array if the existing array doesn't have enough capacity. This means that append often returns a new slice, leaving the original slice untouched. As demonstrated, points2 = append(points2, Point{5, 6}) creates a new slice for points2, leaving points1 unmodified.

  • Modifying Struct Fields: Because of the value semantics of structs, changing a field within the struct inside a slice in Go doesn't change the value of the struct in another.
    In the example provided, modifying points2, does not affect points1.

Go's design, with its emphasis on value types for structs and the behavior of append, encourages immutability. While you can use pointers to create mutable behavior in Go, the language's default behavior for structs promotes immutability, making it easier to write code with fewer side effects. This is a significant advantage of Go for functional programming.

7. Map, Filter, Reduce

Map, filter, and reduce are fundamental higher-order functions in functional programming that operate on collections of data (like lists or arrays). They provide a concise and declarative way to transform and process data without using explicit loops.

  • Map: Applies a given function to each element of a collection, producing a new collection with the transformed elements. The original collection remains unchanged.
  • Filter: Creates a new collection containing only the elements from the original collection that satisfy a given condition (a predicate function).
  • Reduce: Combines all elements of a collection into a single value using a specified function. This function takes an accumulator and the current element, updating the accumulator with each iteration.

Python

from functools import reduce

# Map: Square each number
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda n: n * n, numbers))
print("Squares:", squares)

# Filter: Get even numbers
evens = list(filter(lambda n: n % 2 == 0, numbers))
print("Evens:", evens)

# Reduce: Sum all numbers
sum_of_numbers = reduce(lambda acc, n: acc + n, numbers, 0)  # 0 is the initial value
print("Sum:", sum_of_numbers)
Enter fullscreen mode Exit fullscreen mode

Python has built-in support for map, filter, and reduce.

  • map: The map function takes a function and an iterable (like a list) as arguments. In the example, lambda n: n * n is an anonymous function (a lambda) that squares a number. map applies this function to each element of the numbers list. The result of map is an iterator, so we use list() to convert it to a list for printing.

  • filter: The filter function also takes a function and an iterable. The function (lambda n: n % 2 == 0) acts as a predicate, returning True for even numbers and False otherwise. filter returns an iterator containing only the elements for which the predicate returns True. Again, we use list() to convert it to a list.

  • reduce: reduce is not directly built-in; it's part of the functools module. It takes a function, an iterable, and an optional initial value. The function (lambda acc, n: acc + n) takes the accumulator (acc) and the current element (n), returning the updated accumulator. reduce applies this function cumulatively to the items of the sequence, from left to right, so as to reduce the sequence to a single value. In the snippet, it calculates the sum of all numbers in the list.

JavaScript

// Map: Square each number
const numbers = [1, 2, 3, 4, 5];
const squares = numbers.map(n => n * n);
console.log("Squares:", squares);

// Filter: Get even numbers
const evens = numbers.filter(n => n % 2 === 0);
console.log("Evens:", evens);

// Reduce: Sum all numbers
const sum = numbers.reduce((acc, n) => acc + n, 0); // 0 is the initial value
console.log("Sum:", sum);
Enter fullscreen mode Exit fullscreen mode

JavaScript also provides map, filter, and reduce as built-in methods on arrays. This makes the syntax very concise and readable.

  • map: The map method is called directly on the numbers array. It takes a callback function (n => n * n) that's applied to each element, creating a new array (squares) with the results.

  • filter: The filter method, similarly, is called on the array. It takes a predicate function (n => n % 2 === 0) and returns a new array (evens) containing only the elements that satisfy the predicate.

  • reduce: The reduce method is also called on the array. It takes a callback function ((acc, n) => acc + n) and an initial value (0). The callback function takes the accumulator (acc) and the current element (n), returning the updated accumulator. reduce iterates through the array, accumulating the result into the sum variable. The JavaScript syntax for these operations is very clean and directly integrated into the array object.

Go

package main

import (
    "fmt"
)

func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

func Filter[T any](slice []T, f func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if f(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](slice []T, initial U, f func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = f(result, v)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}

    // Map: Square each number
    squares := Map(numbers, func(n int) int { return n * n })
    fmt.Println("Squares:", squares)

    // Filter: Get even numbers
    evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
    fmt.Println("Evens:", evens)

    // Reduce: Sum all numbers
    sum := Reduce(numbers, 0, func(acc int, n int) int { return acc + n })
    fmt.Println("Sum:", sum)
}
Enter fullscreen mode Exit fullscreen mode

Go, unlike Python and JavaScript, does not have built-in map, filter, and reduce functions. However, you can easily implement them using Go's for...range loops and functions. The Go snippet shows generic implementations of Map, Filter, and Reduce using Go 1.18+ generics.

  • Map: The provided Map function takes a slice of type T, a function that transforms a T to a U, and it returns a slice of type U. The types T and U can be any types. It iterates through the input slice, applies the provided function f to each element, and stores the result in a new slice.

  • Filter: The Filter function takes a slice of type T and a predicate function (a function that returns a boolean). It iterates through the slice, and for each element where the predicate function returns true, it appends the element to a new slice.

  • Reduce: The Reduce function takes a slice of type T, an initial value of type U, and a function which takes a value of type U and a value of type T, returning another U.

While Go requires more code to achieve the same functionality as Python and JavaScript's built-ins, the generic implementations provide flexibility and type safety. The use of anonymous functions (like func(n int) int { return n * n }) makes the code resemble the lambda expressions used in Python and JavaScript.

8. Function Composition

Function composition is the process of combining two or more functions to create a new function. The output of one function becomes the input of the next, forming a pipeline of operations. This is a powerful way to build complex logic from simpler, reusable building blocks, enhancing code modularity and readability. It avoids creating intermediate variables and nested function calls.

Python

from functools import reduce


def compose(*funcs):
    return lambda x: reduce(lambda acc, f: f(acc), reversed(funcs), x)


def add_one(x):
    return x + 1


def square(x):
    return x * x


def multiply_by_two(x):
    return x * 2


composed = compose(square, multiply_by_two, add_one)  # Order is correct
result = composed(5)  # ((5 + 1) * 2)^2 = 144
print("Composed result:", result)
Enter fullscreen mode Exit fullscreen mode

The Python snippet defines a compose function that takes a variable number of functions (*funcs) as arguments. It returns a new function (a lambda) that takes an initial value x. Inside the lambda, it uses reduce to apply the functions in sequence. The trick here is reversed(funcs). reduce normally applies functions from left to right, but in function composition, we usually want the rightmost function to be applied first. reversed(funcs) reverses the order of functions, so reduce effectively applies them from right to left. The functions add_one, square, multiply_by_two demonstrates what functions can be composed. The order matters in composition. The comment ((5 + 1) * 2)^2 = 144 shows how the functions are applied step by step, starting with add_one.

JavaScript

const compose = (...funcs) => x => funcs.reduceRight((acc, f) => f(acc), x);

const addOne = x => x + 1;
const square = x => x * x;
const multiplyByTwo = x => x * 2;

const composed = compose(square, multiplyByTwo, addOne); // Order is correct
const result = composed(5); // ((5 + 1) * 2)^2 = 144
console.log("Composed result:", result);
Enter fullscreen mode Exit fullscreen mode

The JavaScript snippet defines a compose function using arrow function syntax. It's very concise. ...funcs collects all function arguments into an array. The returned function takes an initial value x and uses funcs.reduceRight((acc, f) => f(acc), x). The reduceRight method is the key here; it's a built-in array method that applies a function against an accumulator and each element of the array (from right to left), reducing it to a single value. This achieves the correct order of function application directly, without needing to reverse the function list like in Python.

Go

package main

import "fmt"

func compose[T any](funcs ...func(T) T) func(T) T {
    return func(x T) T {
        result := x
        for _, f := range funcs {
            result = f(result)
        }
        return result
    }
}

func addOne(x int) int {
    return x + 1
}

func square(x int) int {
    return x * x
}

func multiplyByTwo(x int) int {
    return x * 2
}

func main() {
    composed := compose(addOne, multiplyByTwo, square)
    result := composed(5) // ((5 + 1) * 2)^2 = 144
    fmt.Println("Composed result:", result)
}
Enter fullscreen mode Exit fullscreen mode

The go snippet defines a compose function takes advantage of Go 1.18+ generics.
It is defined to take a variadic number of functions (funcs ...func(T) T). The T represents any data type. The key difference is in how the functions are applied. It uses a simple for...range loop to iterate through the funcs slice in the order they were provided. Unlike Python and JavaScript, this Go compose function applies functions from left to right. This is a deliberate choice to make the code simpler, and in the provided main() function, this is accounted for. The composed variable receives the composition, but the functions are listed in the reverse order (addOne, multiplyByTwo, square) compared to what would be mathematically expected. The final call to composed(5) produces correct results.

Conclusion

Each of these three languages brings its own unique approach to functional programming. Python and JavaScript, with their dynamic typing and built-in support for many functional features, offer a relatively lower barrier to entry. Go, on the other hand, while requiring a bit more effort to implement some functional concepts, provides strong immutability support and explicitness, which can be advantageous in certain scenarios.

Subscribe for More!

If you enjoyed this comparison, be sure to subscribe to my YouTube channel for more content on programming languages, software development, and other tech topics.

Let me know in the comments which language you prefer for functional programming and why!

Let's Talk Dev - YouTube

I share my insights and tips on software engineering topics such as web development, databases, cloud computing, and more. Hi, I'm Mihai and I'm a full-stack software engineer with a passion for creating innovative and user-friendly solutions using web and cloud technologies. I have a home lab where I like to experiment with Kubernetes and other cutting-edge technologies. I'm also a big fan of open-source software. When I'm not coding, I enjoy photography, reading books, learning about finance and entrepreneurship, and watching movies.

favicon youtube.com

About me

I'm Mihai Farcas, a software engineer with a few years of experience under my belt. I'm passionate about writing code and love sharing knowledge with fellow developers.

My YouTube channel, "Let's Talk Dev," is where I break down complex concepts, share my experiences (both the good and the face-palm moments).

Connect with me:

Website: https://mihai.ltd
YouTube: https://www.youtube.com/@letstalkdev
GitHub: https://github.com/mihailtd
LinkedIn: https://www.linkedin.com/in/mihai-farcas-ltd/

Top comments (0)