Functions

Mapping from a set of inputs X (domain) to a set of outputs (range) within a co-domain (Y). Every input in domain maps to exactly one output. Multiple inputs can map to the same output.

Pure Function

Takes in arguments and returns a deterministic value, effect-free. Absence of side-effects necessary for referential transparency. Deterministic also means not infinite / undefined.

Side effects: Anything a function does besides computing a returning a value. It’s an interaction with the outside world. Examples: modifying external state, program input/output, throwing exceptions.

Not deterministic

int q(int x, int y) {
	return x / y;
}
// What happens y = 0

int s(int i) {
	return this.x + i;
}
// x can be changed. This is not a function. Same input, different output.

Higher-order Functions

Functions are “first-class citizens” = functions can be stored in variables. Takes in other functions as arguments, returns other functions. E.g. reduce(0, (x, y) x + f.apply(y));

Function with multiple arguments

Multiple arguments can be applied at the same time. Lambda expressions can be curried for partial application. f = x y x + y. f.apply(1).apply(2) returns 3.

Partial Application - Under the Hood

f.apply(1) takes x = 1 and returns y 1 + y. This is partial application. Applying one argument and get back a function.

Function<Integer, Function<Integer, Integer>> f = new Function<>() {
    @Override
    public Function<Integer, Integer> apply(Integer x) {
        return new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer y) {
                return x + y;
            }
        };
    }
}

Closure: Instance of Local Class

class A {
    private final int z;
    
    A(int z) { 
	    this.z = z; 
    }
    
    Predicate<Integer> foo(int y) {
        return new Predicate<Integer>() {
            @Override
            public boolean test(Integer x) {
                return x == y + z;
            }
        };
    }
    
}

y is captured by value (e.g foo(2))
If I do foo(3), I'll create a new predicate.
I can also do pred1 = a.foo(2) and pred2 = a.foo(2) but they are separate objects.

When foo(2) returns and stack frame disappears, y = 2 lives on safely inside the Predicate object.

Even though you never wrote private final int y in the anonymous class, the compiler secretly generates it for you.
That's the magic of closures.

z is captured by reference

Variable capture

The closure has a copy of y and z, not the original. y and z cannot be changed and should be kept as final.


Function Composition

Function composition: (g ◦ f)(x) = g(f(x)) This reads as g compose f, which is the same as f and then g.

g.compose(f).apply("abc")

// g.compose(f) essentially means:
// input -> g(f(input))
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    return (V v) -> this.apply(before.apply(v));
}

f.andThen(g).apply("abc")

// f.andThen(g) essentially means:
// input -> g(f(input))
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    return (T t) -> after.apply(this.apply(t));
}

Func abstract class

abstract class Func {
	abstract int apply(int x);
}

Func f = new Func() {
	int apply(int x) 
		return x + 2;
	}
}

Func g = new Func() {
	int apply(int x) 
		return x / 2;
	}
}

Example: Integer Function Composition

abstract class Func {
	abstract int apply(int x);
	
	Func compose(Func before) {
		return new Func() {
			int apply(int x) {
				int result = before.apply(x);
				return ???
			}
		};
	}
}

Walking through g.compose(f).apply(10):
1) g.compose(f) returns a new Func object
2) .apply(10) is called on that new object
3) Inside: result = before.apply(10) → calls f(10) → 20
4) Then: Func.this.apply(20) → calls g(20) → 22

Generic Function Composition

abstract class Func<T, R> {
    <V> Func<V, R> compose(Func<? super V, ? extends T> before) {
        return new Func<V, R>() {
            R apply(V v) {
                return Func.this.apply(before.apply(v));
            }
        };
    }
    abstract R apply(T t);
}

The key idea: f.compose(g) creates a new Func<V, R> that does V → g → T → f → R. The before parameter uses PECS — ? super V (consumes V as input) and ? extends T (produces T as output) — so the composed function accepts any g whose input is a supertype of V and whose output is a subtype of T.

f.compose(g) g input (V) output (T) for f f input (T) output (R)


Type Parameters

Class-level parameters

Func<String, Integer> f = new Func<String, Integer>() {
    Integer apply(String x) { return x.length(); }
}

// T = String (Input)
// R = Integer (Output) — locked in for this object

Method-level parameters

// map has a method-level type parameter U
<U> Maybe<U> map(Function<T, U> mapper)

Maybe<String> name = Maybe.of("Elkan");  // T = String, locked in

name.map(s -> s.length())
// Java sees the lambda returns an Integer
// So U = Integer, decided right now
// Returns Maybe<Integer>

Lazy vs Eager Evaluation using andThen

Eager

Stream.<String>of("one", "two", "three")
    .map(x -> x.length())       // Stream<Integer>: 3, 3, 5
    .reduce(0, (x,y) -> x + y)  // 0 + 3 + 3 + 5 = 11

Lazy

Function<String, Function<Integer, Integer>> mapper = x -> y -> y + x.length();

Function<Integer, Integer> f = Stream.<String>of("one", "two", "three")
    .map(mapper) // returns a function y -> y + x.length()
    .reduce(Function.<Integer>identity(), (a, b) -> a.andThen(b));

f.apply(0)

So after .map(mapper), the stream becomes: “one” → y y + 3 “two” → y y + 3 “three” → y y + 5

Now we reduce these three functions into one using a.andThen(b) — which chains them together. The identity Function.<Integer>identity() is the starting point (does nothing, just returns its input).

The reduction builds: identity.andThen(y y + 3).andThen(y y + 3).andThen(y y + 5) This gives us a single Function<Integer, Integer> called f. Nothing has been computed yet — that’s the “lazy” part.