Polyglot Primer

Chapter 5: Functions

Declaring, calling and passing functions. Go returns multiple values and uses them for errors; JS and Python treat functions as ordinary values with rich argument handling; Java wraps everything in methods and offers overloads instead of default arguments.

Overview

GoJavaJavaScriptPython
Free-standing functionsyesno - always inside a class (static methods)yesyes
Multiple return valuesyes (native)no (use a record/array)no (return array/object, destructure)yes (really a tuple)
Default parametersnono (use overloads)yesyes
Named argumentsnonono (pass an object)yes
Overloadingnoyesnono
Variadic...TT......rest*args
First-class functionsyesvia lambdas / functional interfacesyesyes
Closuresyesyes (captured vars must be effectively final)yesyes
Nested functionsclosures only (anonymous)lambdas / local classesyesyes

Declaring and calling

func add(a int, b int) int {       // or: func add(a, b int) int
    return a + b
}
total := add(2, 3)
public static int add(int a, int b) {
    return a + b;
}
int total = add(2, 3);
function add(a, b) {
  return a + b;
}
const addArrow = (a, b) => a + b;      // arrow function, implicit return
const total = add(2, 3);
def add(a, b):
    return a + b

add_lambda = lambda a, b: a + b         # single expression only
total = add(2, 3)

Type position: Go puts the type after the name; Java before. Python can annotate: def add(a: int, b: int) -> int:.

Missing arguments: Go and Java refuse to compile; JS silently passes undefined and ignores extras; Python raises TypeError.

Multiple return values

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divide by zero")
    }
    return a / b, nil
}

result, err := divide(10, 2)
_, err = divide(1, 0)                   // blank identifier discards a value
record Result(double value, String error) {}      // records: Java 16+
Result divide(double a, double b) {
    return b == 0 ? new Result(0, "divide by zero") : new Result(a / b, null);
}
function divide(a, b) {
  return [a / b, null];                  // or return { value, error }
}
const [result, err] = divide(10, 2);     // destructuring
def divide(a, b):
    return a / b, None                   # a tuple
result, err = divide(10, 2)              # unpacking

Go's (value, error) pair is the backbone of its error handling (see the errors chapter). In the other languages exceptions are used instead.

Named return values (Go)

func minMax(xs []int) (min, max int) {   // named results start as zero values
    min, max = xs[0], xs[0]
    for _, x := range xs {
        if x < min { min = x }
        if x > max { max = x }
    }
    return                                // "naked" return: fine in short functions only
}

Named results also let defer modify the return value, a Go-only trick covered in the Go-specific chapter.

Default, optional and named arguments

// Go has none. Use an options struct or functional options.
type Options struct{ Retries int; Verbose bool }
func run(o Options) {}
run(Options{Retries: 3})
// Java: overloads
void greet(String name) { greet(name, "Hello"); }
void greet(String name, String greeting) { /* ... */ }
function greet(name, greeting = "Hello") {}      // default parameter
function connect({ host, port = 80 } = {}) {}     // destructured "named" args
connect({ host: "x" });
def greet(name, greeting="Hello", *, shout=False):    # `*` forces keyword-only
    ...
greet("Ada", shout=True)                              # named arguments

Python gotcha - mutable default arguments are shared between calls:

def append(x, items=[]):        # BUG: one list for every call
    items.append(x); return items

def append(x, items=None):      # correct
    items = [] if items is None else items

Variadic functions

func sum(nums ...int) int {              // nums is a []int
    t := 0
    for _, n := range nums { t += n }
    return t
}
sum(1, 2, 3)
sum(xs...)                                // spread an existing slice
static int sum(int... nums) {             // nums is an int[]
    int t = 0;
    for (int n : nums) t += n;
    return t;
}
sum(1, 2, 3);
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);
sum(1, 2, 3);
sum(...xs);                                // spread
def total(*nums, **options):               # *args tuple, **kwargs dict
    return sum(nums)
total(1, 2, 3)
total(*xs, **opts)                         # unpack when calling

Functions as values

type Op func(int, int) int
ops := map[string]Op{
    "add": func(a, b int) int { return a + b },
    "mul": func(a, b int) int { return a * b },
}
fmt.Println(ops["add"](2, 3))
import java.util.function.*;
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
Function<Integer, Integer> square = x -> x * x;
Supplier<String> hello = () -> "hi";
add.apply(2, 3);
const ops = { add: (a, b) => a + b, mul: (a, b) => a * b };
ops.add(2, 3);
[1, 2, 3].map((x) => x * 2);
ops = {"add": lambda a, b: a + b, "mul": lambda a, b: a * b}
ops["add"](2, 3)
list(map(lambda x: x * 2, [1, 2, 3]))      # comprehension is more idiomatic

Java has no function types; every lambda targets a functional interface (Function, Predicate, Supplier, Consumer, Runnable, ...). That is why you call .apply() instead of f(x).

Closures

func counter() func() int {
    n := 0
    return func() int { n++; return n }
}
c := counter()
c(); c()          // 1, 2
// captured locals must be final or effectively final
int[] n = {0};
Supplier<Integer> next = () -> ++n[0];       // workaround: a 1-element array or AtomicInteger
function counter() {
  let n = 0;
  return () => ++n;
}
def counter():
    n = 0
    def inc():
        nonlocal n          # required to REBIND an outer variable
        n += 1
        return n
    return inc

Python needs nonlocal; without it n += 1 makes n a local and raises UnboundLocalError. Java forbids reassigning captured variables altogether.

this, receivers and methods

GoJavaJavaScriptPython
Instance referenceexplicit receiver: func (u User) Name()implicit thisthis (depends on how it's called!)explicit first param self
Binding of thisfixedfixeddynamic except in arrow functionsfixed (bound method)

JavaScript quirk: const f = obj.method; f() loses this. Arrow functions capture this lexically, which is why they are used for callbacks. Full coverage in the classes/structs chapter.

Recursion

func fact(n int) int {
    if n <= 1 { return 1 }
    return n * fact(n-1)
}

All four recurse the same way. Limits differ: Go stacks grow dynamically (up to 1 GB by default) so deep recursion is usually fine; Java defaults to a small thread stack (StackOverflowError near 10-20k frames); JS throws RangeError around 10k frames; Python raises RecursionError at 1000 frames by default (sys.setrecursionlimit). None of them guarantees tail-call optimisation.

Passing semantics: value, reference, copy

GoJavaJavaScriptPython
Modelalways by value (a copy)always by value (of the reference for objects)by value of the referenceby "assignment" (object reference)
Mutate caller's dataonly via pointer, slice, mapmutate the objectmutate the objectmutate the object
def f(lst):
    lst.append(1)      # caller sees this
    lst = [99]         # caller does NOT see this

Quirks and gotchas

What is idiomatic

Checklist

Exercises

In each language implement:

  1. divmod(a, b) returning quotient and remainder, with an error/exception for division by zero.
  2. apply(xs, f) returning a new collection with f applied to each element (without using the built-in map).
  3. makeMultiplier(n) returning a function that multiplies its input by n.
  4. sumAll(...) that accepts any number of arguments.
  5. A memoize(f) wrapper that caches results by argument. (Python: also try functools.cache.)

Predict, then verify: what does append(1) print when called twice with the mutable-default Python bug above?