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)
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);
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)
}
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
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
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
}
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())
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());
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())
}
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"))
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"));
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"))
}
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
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
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
}
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}]
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 topoints2
affects only the copy because append creates a new array, but modifyingpoints2[0]["x"]
modifies the underlying object referenced by bothpoints1
andpoints2
.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, modifyingpoints3[0]["x"]
only affectspoints3
, leavingpoints1
unchanged. You need to explicitly usecopy.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 }]
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 topoints2
doesn't modifypoints1
, as a new array instance is created, but changingpoints2[0].x
does modify the corresponding object inpoints1
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 useJSON.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}]
}
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
: Theappend
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 thatappend
often returns a new slice, leaving the original slice untouched. As demonstrated, points2 = append(points2, Point{5, 6})
creates a new slice forpoints2
, leavingpoints1
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, modifyingpoints2
, does not affectpoints1
.
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)
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 (alambda
) that squares a number. map applies this function to each element of the numbers list. The result ofmap
is aniterator
, so we uselist()
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, returningTrue
for even numbers andFalse
otherwise.filter
returns an iterator containing only the elements for which the predicate returnsTrue
. Again, we uselist()
to convert it to a list.reduce:
reduce
is not directly built-in; it's part of thefunctools
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);
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 thesum
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)
}
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 typeT
, a function that transforms aT
to aU
, and it returns a slice of typeU
. The typesT
andU
can be any types. It iterates through the input slice, applies the provided functionf
to each element, and stores the result in a new slice.Filter: The
Filter
function takes a slice of typeT
and a predicate function (a function that returns a boolean). It iterates through the slice, and for each element where the predicate function returnstrue
, it appends the element to a new slice.Reduce: The
Reduce
function takes a slice of typeT
, an initial value of typeU
, and a function which takes a value of typeU
and a value of typeT
, returning anotherU
.
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)
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);
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)
}
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!
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)