PHpullh

LANGUAGE COMPARISON

Lambda를 언어별로 비교하기

같은 개념을 실제 예제로 나란히 확인하고, 각 언어의 개별 설명으로 이어갈 수 있습니다.

익명 함수는 문법 설탕이 아닙니다

Kotlin, Java, PHP의 람다는 겉으로 "짧게 쓰는 함수"처럼 보이지만, 세 언어가 이 기능을 도입한 배경이 서로 다르고 그 흔적이 문법에 남아 있습니다. Kotlin은 함수 타입을 언어의 일급 시민으로 설계했고, Java는 기존 익명 클래스 문법을 줄이면서 기존 타입 체계와 호환을 유지해야 했으며, PHP는 클로저를 객체로 구현했습니다. 그래서 세 언어에서 람다가 바깥 변수를 어떻게 붙잡는지가 눈에 띄게 다릅니다.

Java에서 람다가 캡처하는 지역 변수는 사실상 final이어야 합니다. 캡처한 뒤에 그 변수를 바꾸면 컴파일 오류가 납니다. 값을 누적하려면 배열 한 칸이나 AtomicInteger 같은 가변 상자를 우회로 쓰게 되는데, 이 제약은 불편을 위한 제약이 아니라 람다가 다른 스레드로 옮겨 갔을 때 스택 변수가 이미 사라졌을 수 있다는 문제에서 나옵니다.

Kotlin은 같은 상황에서 가변 지역 변수도 캡처할 수 있습니다. 컴파일러가 그 변수를 힙의 참조 상자로 바꿔 주기 때문입니다. 편하지만 대신 람다가 바깥 상태를 조용히 바꿀 수 있어, 컬렉션 처리 체인 안에서 부수 효과를 넣기가 쉬워집니다. PHP는 아예 명시를 요구합니다. function 클로저는 use 절에 적은 변수만 붙잡고 기본은 값 복사이며, &를 붙여야 공유됩니다. 반면 화살표 함수 fn은 쓰는 변수를 자동으로, 값으로 캡처합니다.

Kotlin

람다 & 고차함수

함수를 일급 객체로 다루는 Kotlin의 함수형 프로그래밍 핵심.

// 함수 타입
val double: (Int) -> Int = { n -> n * 2 }
val greet: (String) -> String = { "Hello, $it!" }  // it: 단일 파라미터
val add: (Int, Int) -> Int = { a, b -> a + b }

// 고차함수 — 함수를 파라미터로 받기
fun transform(list: List<Int>, fn: (Int) -> Int) =
    list.map(fn)

// 고차함수 — 함수를 반환하기
fun multiplier(factor: Int): (Int) -> Int = { it * factor }

// 함수 합성
infix fun <A, B, C> ((A) -> B).then(g: (B) -> C): (A) -> C =
    { g(this(it)) }

// 메서드 참조
fun isPositive(n: Int) = n > 0
val nums = listOf(-3, -1, 0, 2, 5)
val pos = nums.filter(::isPositive)
val strs = nums.map(Int::toString)

fun main() {
    println(transform(listOf(1,2,3), double))   // [2, 4, 6]
    println(transform(listOf(1,2,3)) { it * it }) // trailing lambda

    val triple = multiplier(3)
    println(triple(7))   // 21

    val process = double then { it + 1 } then { "$it!" }
    println(process(5))  // "11!"
}

Python

클로저 & 제너레이터

클로저로 상태를 캡처하고, yield로 메모리 효율적인 이터레이터를 만듭니다.

# 클로저 — 외부 함수의 변수를 기억
def make_counter(start: int = 0):
    count = start

    def counter():
        nonlocal count   # 외부 변수 수정하려면 nonlocal 필요
        count += 1
        return count

    return counter

c1 = make_counter()
c2 = make_counter(10)
print(c1(), c1(), c1())   # 1 2 3
print(c2(), c2())          # 11 12 (독립적)

# 제너레이터 함수
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 제너레이터는 next() 호출 시에만 실행됨 (lazy)
gen = fibonacci()
fibs = [next(gen) for _ in range(10)]
print(fibs)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# yield from — 하위 제너레이터 위임
def chain(*iterables):
    for it in iterables:
        yield from it

list(chain([1,2], [3,4], [5]))  # [1,2,3,4,5]

# 제너레이터 파이프라인
def read_lines(text):
    yield from text.splitlines()

def filter_empty(lines):
    yield from (l for l in lines if l.strip())

def to_upper(lines):
    yield from (l.upper() for l in lines)

text = "hello

world
python"
result = list(to_upper(filter_empty(read_lines(text))))

Go

클로저 & 함수 타입

Go의 클로저로 상태를 캡슐화하고, 함수를 일급 값으로 활용합니다.

package main

import "fmt"

// 클로저 — 외부 변수 캡처
func makeCounter(start int) func() int {
	count := start
	return func() int {
		count++
		return count
	}
}

// 클로저로 미들웨어 패턴
type Handler func(string) string

func withLogging(h Handler) Handler {
	return func(req string) string {
		fmt.Printf("[LOG] 요청: %s
", req)
		resp := h(req)
		fmt.Printf("[LOG] 응답: %s
", resp)
		return resp
	}
}

func withAuth(h Handler) Handler {
	return func(req string) string {
		if req == "" {
			return "401 Unauthorized"
		}
		return h(req)
	}
}

// 함수 슬라이스
func pipeline(fns ...func(int) int) func(int) int {
	return func(x int) int {
		for _, fn := range fns {
			x = fn(x)
		}
		return x
	}
}

func main() {
	// 카운터 클로저
	c1 := makeCounter(0)
	c2 := makeCounter(10)
	fmt.Println(c1(), c1(), c1()) // 1 2 3
	fmt.Println(c2(), c2())       // 11 12 (독립적)

	// 미들웨어 체이닝
	handler := func(req string) string {
		return "200 OK: " + req
	}
	wrapped := withLogging(withAuth(handler))
	fmt.Println(wrapped("GET /api/users"))
	fmt.Println(wrapped(""))

	// 파이프라인
	double  := func(n int) int { return n * 2 }
	addOne  := func(n int) int { return n + 1 }
	square  := func(n int) int { return n * n }

	pipe := pipeline(double, addOne, square)
	fmt.Println(pipe(3)) // (3*2+1)^2 = 49
}

Java

람다 & 함수형 인터페이스

Java 8의 람다 표현식과 java.util.function 패키지의 함수형 인터페이스를 마스터합니다.

import java.util.function.*;

public class Lambdas {
    public static void main(String[] args) {
        // 기본 람다
        Runnable r = () -> System.out.println("실행!");
        r.run();

        // Function<T, R> — T 입력, R 출력
        Function<String, Integer> strLen = String::length;
        Function<Integer, Integer> double_ = n -> n * 2;
        Function<String, Integer> composed = strLen.andThen(double_);
        System.out.println(composed.apply("Hello")); // 10

        // Predicate<T> — boolean 반환
        Predicate<String> notEmpty = s -> !s.isEmpty();
        Predicate<String> longStr  = s -> s.length() > 5;
        Predicate<String> both     = notEmpty.and(longStr);
        System.out.println(both.test("Hello World")); // true

        // Consumer<T> — 반환 없음
        Consumer<String> print = System.out::println;
        Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
        print.andThen(printUpper).accept("java");

        // Supplier<T> — 인자 없음
        Supplier<String> greeting = () -> "Hello, Java!";
        System.out.println(greeting.get());

        // BiFunction<T, U, R>
        BiFunction<Integer, Integer, Integer> add = Integer::sum;
        System.out.println(add.apply(3, 4)); // 7

        // UnaryOperator, BinaryOperator
        UnaryOperator<Integer>   square = n -> n * n;
        BinaryOperator<Integer> multiply = (a, b) -> a * b;
        System.out.println(square.apply(5));    // 25
        System.out.println(multiply.apply(3,4)); // 12
    }
}

PHP

클로저 팩토리

함수가 함수를 반환하는 클로저 팩토리 예제입니다.

<?php
function makeMultiplier(int $factor): Closure {
    return function (int $value) use ($factor): int {
        return $value * $factor;
    };
}

$double = makeMultiplier(2);
echo $double(10);

JavaScript

람다 함수

람다 대신 function 선언을 쓰되, 내부 reduce 콜백은 화살표 함수로 작성한 혼합 형태입니다. 짧은 콜백은 화살표로, 이름이 필요한 최상위 함수는 선언문으로 두는 조합이 읽기 편합니다. 두 문법의 실질적 차이는 길이가 아니라 this 처리와 호이스팅 여부에 있습니다.

// 람다 함수
function sum(...values) {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 6));

TypeScript

람다 함수

람다 대신 function 선언을 쓰되, 내부 reduce 콜백은 화살표 함수로 작성한 혼합 형태입니다. 짧은 콜백은 화살표로, 이름이 필요한 최상위 함수는 선언문으로 두는 조합이 읽기 편합니다. 두 문법의 실질적 차이는 길이가 아니라 this 처리와 호이스팅 여부에 있습니다.

// 람다 함수
function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 6));

C#

람다 함수

람다 대신 function 선언을 쓰되, 내부 reduce 콜백은 화살표 함수로 작성한 혼합 형태입니다. 짧은 콜백은 화살표로, 이름이 필요한 최상위 함수는 선언문으로 두는 조합이 읽기 편합니다. 두 문법의 실질적 차이는 길이가 아니라 this 처리와 호이스팅 여부에 있습니다.

// 람다 함수
using System;

Func<int> CreateCounter() { var current = 0; return () => ++current; }
var next = CreateCounter();
Console.WriteLine(next());

C++

람다 함수

람다 대신 function 선언을 쓰되, 내부 reduce 콜백은 화살표 함수로 작성한 혼합 형태입니다. 짧은 콜백은 화살표로, 이름이 필요한 최상위 함수는 선언문으로 두는 조합이 읽기 편합니다. 두 문법의 실질적 차이는 길이가 아니라 this 처리와 호이스팅 여부에 있습니다.

// 람다 함수
#include <functional>
#include <iostream>

std::function<int()> make_counter() { int current = 0; return [current]() mutable { return ++current; }; }
int main() {
    auto next = make_counter();
    std::cout << next() << "\n";
}

Rust

람다 함수

람다 대신 function 선언을 쓰되, 내부 reduce 콜백은 화살표 함수로 작성한 혼합 형태입니다. 짧은 콜백은 화살표로, 이름이 필요한 최상위 함수는 선언문으로 두는 조합이 읽기 편합니다. 두 문법의 실질적 차이는 길이가 아니라 this 처리와 호이스팅 여부에 있습니다.

// 람다 함수
fn make_counter() -> impl FnMut() -> i32 { let mut current = 0; move || { current += 1; current } }
fn main() {
    let mut next = make_counter();
    println!("{}", next());
}

타입 자리에 무엇이 들어가는가

가장 큰 구조적 차이는 람다를 담는 타입입니다. Kotlin에는 (Int) -> String 같은 함수 타입이 문법에 있어서, 함수를 인자로 받는 API를 자연스럽게 설계할 수 있습니다. Java에는 함수 타입이 없고 대신 추상 메서드가 하나뿐인 인터페이스, 즉 함수형 인터페이스가 그 자리를 대신합니다. Function, Predicate, Consumer, Supplier 같은 이름을 외우게 되는 이유이며, 인자가 세 개인 함수를 받고 싶으면 인터페이스를 직접 만들어야 합니다.

여기서 실무적으로 번거로운 지점이 원시 타입입니다. Java의 제네릭은 원시 타입을 담지 못하므로 Function<Integer, Integer>는 박싱을 유발합니다. 그래서 표준 라이브러리에 IntFunction, IntPredicate 같은 전용 인터페이스가 따로 존재합니다. Kotlin은 인라인 함수로 람다 호출 자체를 호출 지점에 펼쳐 넣어 이 비용을 피하는 별도의 길을 갖고 있습니다. PHP의 클로저는 객체이므로 생성 비용이 있고, 반복문 안에서 매번 새로 만들면 그만큼 할당이 늘어납니다.

람다를 쓰지 않는 편이 나은 경우

람다는 코드를 짧게 만들지만 짧은 것이 목표는 아닙니다. 세 언어 모두에서 다음 경우에는 이름 있는 함수로 빼는 편이 낫습니다. 본문이 서너 줄을 넘어갈 때, 같은 로직이 두 곳 이상에서 필요할 때, 그리고 단위 테스트를 따로 붙이고 싶을 때입니다. 특히 스택 트레이스가 문제입니다. 중첩된 람다에서 예외가 나면 트레이스에 의미 없는 합성 이름이 늘어서고, 어느 단계에서 터졌는지 읽기 어려워집니다. 이름 있는 메서드로 한 단계만 빼도 디버깅 난이도가 확 내려갑니다.

Kotlin의 it, Java의 메서드 참조(String::length), PHP의 fn은 모두 짧은 변환 하나를 표현할 때 가장 잘 맞습니다. 반대로 조건 분기가 들어가고 예외를 다뤄야 하는 로직을 람다 안에 밀어 넣으면, 체인은 여전히 한 줄처럼 보이지만 읽는 사람은 각 단계에서 무슨 일이 일어나는지 추적하지 못합니다. 함수형 스타일의 이점은 간결함이 아니라 각 단계가 하는 일이 하나로 좁혀지는 데 있습니다.

람다와 붙어 다니는 고차 함수, 컬렉션 파이프라인 사용법은 Kotlin 함수형, Java 람다와 메서드 참조, PHP 함수형 문서에서 이어집니다. 일반 함수 선언과의 차이는 Function 비교에서 확인하실 수 있습니다.

세 언어를 오갈 때의 실무 메모

Java에서 Kotlin으로 넘어가면 함수 타입이 문법에 있다는 점 때문에 API 설계가 훨씬 자유로워지지만, Java 코드에서 Kotlin의 함수 타입을 호출할 때는 여전히 Function 계열로 보인다는 사실을 알고 있어야 합니다. 혼재된 프로젝트에서 시그니처가 예상과 다르게 보이는 이유입니다. 반대로 Kotlin에서 Java의 함수형 인터페이스를 받는 API를 호출할 때는 SAM 변환이 적용되어 람다를 그대로 넘길 수 있습니다.

PHP에서는 클로저가 객체라는 점이 오히려 유용하게 쓰입니다. bindTo로 클로저가 바라볼 객체를 바꿀 수 있어, 테스트에서 내부 상태에 접근하거나 DSL 형태의 설정 코드를 만드는 데 활용됩니다. 다만 이런 기법은 읽는 사람이 문맥을 추적하기 어렵게 만들기 때문에, 프레임워크 내부나 테스트 유틸리티 같은 좁은 범위로 한정하는 편이 낫습니다.

정리하면 세 언어의 람다는 문법의 길이가 아니라 캡처 규칙과 타입 표현에서 갈립니다. Java는 안전을 위해 캡처를 제한하고 인터페이스로 타입을 대신하며, Kotlin은 함수 타입과 가변 캡처를 언어에 넣어 표현력을 얻는 대신 부수 효과가 섞이기 쉬워졌고, PHP는 무엇을 붙잡을지 코드에 직접 적게 합니다. 어느 쪽이 낫다기보다 각자 무엇을 감수했는지를 알고 쓰는 것이 실수를 줄입니다.