람다식

1 개요

람다식, 또는 람다 함수는 프로그래밍 언어에서 사용되는 개념으로 익명 함수(Anonymous functions)를 지칭하는 용어이다. 프로그래밍 언어학적으로 파고들면 이것만 한 달 이상 배우는 경우도 많으며, 실제로 여러 대학교들에서 사용하는 프로그래밍 언어 교재에서도 꽤나 많은 분량을 차지하는 개념이지만 실무적으로는 코드의 간결함, 지연 연산을 통한 퍼포먼스 향상, 그리고 기존 이터레이션 관련 코드를 구현하는 데 있어 불필요한 부분들을 제거할 수 있다는 점에서 비교적 중요하게 다루어지고 있다. 람다식은 주로 고차 함수에 인자(argument)로 전달되거나 고차 함수가 돌려주는 결과값으로 쓰인다.

2 장점

  1. 코드의 간결성: 효율적인 람다 함수의 사용을 통하여 불필요한 루프문의 삭제가 가능하며, 동일한 함수를 재활용할 수 있는 여지가 커진다. [1]
  2. 필요한 정보만을 사용하는 방식을 통한 퍼포먼스 향상: 지연 연산을 지원하는 방식[2]을 통하여 효율적인 퍼포먼스를 기대할 수 있다. 이 경우 메모리 상의 효율성 및 불필요한 연산의 배제가 가능하다는 장점이 있다.

3 단점

  1. 어떤 방법으로 작성해도 모든 원소를 전부 순회하는 경우는 람다식이 조금 느릴 수 밖에 없다. (어떤 방법으로 만들어도 최종 출력되는 bytecode 나 어셈블리 코드는 단순 while(혹은 for) 문 보다 몇 단계를 더 거치게 된다.)
  2. 익명함수의 특성상 함수 외부의 캡처를 위해 캡처를 하는 시간제약 논리제약적인 요소도 고려해야한다.
  3. 람다식을 너무 남발하여 사용하게되면 오히려 코드를 이해하기 어려울 수 도 있다.

4 유의사항

모든 언어에서 제공되지는 않는다: 대부분의 유명한 언어들은 지원하지만, 지원하지 않는 언어도 가끔씩 있다. 특히 고전적인 문법들의 경우 거의 모든 언어에서 제공됨을 보장할 수 있는 부분과는 차별된다. 대표적으로 C, Fortlan, Pascal 등이 지원하지 않는 언어. Java의 경우 8부터 지원하며, C++은 C++11부터 지원한다.
.NET Framework는 이미 2.0부터 대리자, 메서드 참조, 제너릭을 통해 비슷하게나마 지원하고 있었지만, 본격적으로 람다식이 지원되기 시작한 건 LINQ가 추가된 3.5 부터이다. 그래봤자 대부분은 굳이 람다식을 쓰지 않고도 사용할 수 있기에 큰 의미는 없다.

5 예제

5.1 습관적 방법

기존의 전통적인, 또는 기초 프로그래밍에서 흔히 볼 수 있는 문법을 이용한 코드로, for문을 이용한 아주 기초적인 코드이나 이는 해당 글에서 언급하듯이 각각의 요소들을 하나하나 일일이 검증하며 순차적으로 값을 확인하여 조건절이 끝날 때까지 진행되고 있으며, 이러한 코드는 특별한 경우가 아니라면 최적화되지 않고 들어오는 순서대로 진행된다. 그러나 빨리 끝나는 일을 먼저 하거나 한번에 여러 가지 일을 하는 것이 당연히 효율적이며 짧은 시간 내에 작업을 끝낼 수 있다. 또한 1부터 10까지 1씩 증가하면서 이 코드를 순차적으로 실행해라고 명령하는 것보다는 여기 있는거 다 해라고 설명하는것이 더욱 직관적이며 더 간결하다. 이러한 방식을 Tell, Don't Ask 원칙이라고 한다.

아래 예제는 모두 1~10이 아닌 0~9를 대상으로 했다는 점에 유의할 것.

5.1.1 Java

for (int i = 0; i < 10; i++) {
System.out.println(i);
}

5.1.2 Scala

var i: Int = 0
while (i < 10) {
println(i)
i += 1
}

5.1.3 C

int i;
for (i = 0; i < 10; i++) {
printf("%d", i);
}

5.1.4 C++

for (int i = 0; i < 10; i++) {
std::cout << i;
}

5.1.5 C#

for (int i = 0; i < 10; i++) {
System.Console.Write(i);
}

5.1.6 Go

for i := 0; i < 10; i++ {
println(i)
}

5.1.7 JavaScript

for (var i = 0; i < 10; i++) {
console.log(i);
}

5.1.8 Python

for i in range(10):
print(i)

5.1.9 PHP

for ($i = 0; $i < 10; $i++) {
echo $i."<br />";
}

5.1.10 Swift

for x in 0..<10 {
print(x)
}

5.1.11 Ruby

for x in 0...10
puts x
end

5.2 더 나은 방법

Tell, Don't Ask 원칙에 따라 람다식으로 재작성한 코드로, 이렇게 할 경우 기초적인 수준에서 생기는 장점은 for문과는 달리 개념적으로 설명이 단순하여 이해가 빠르다는 점이며물론 for이고 while이고 다 모르는 사람에게는 이거나 저거나 다 어렵다, 여기서는 잘 드러나지 않지만 복잡한 프로그래밍을 할 때 코드가 간결해지는 장점이 있다.

5.2.1 Java

  • Java 8부터 지원되는 람다식을 사용한 코드
IntStream.range(0, 10).forEach((int value) -> System.out.println(value));
  • 컴파일러의 추론을 통해 파라미터의 자료형을 생략한 코드.
IntStream.range(0, 10).forEach(value -> System.out.println(value));
  • 메서드 레퍼런스를 사용한 코드
IntStream.range(0, 10).forEach(System.out::println);

5.2.2 Scala

(0 until 10) foreach println

5.2.3 C++

  • C++11부터 지원
std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
std::for_each(std::begin(v), std::end(v), [&](const int &i) { std::cout << i; });
  • 컴파일러의 추론을 통해 파라미터의 자료형을 생략한 코드.
std::for_each(v.begin(), v.end(), [](auto n) { std::cout << n; });
  • 이 경우는 람다보다 "Range-based for loop" 가 가독성이 더 좋다.
for (auto n : v) std::cout << n;

5.2.4 JavaScript

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(console.log);

5.2.5 C#

  • .NET Framework 3.5부터 지원되는 람다식을 명시한 코드
Enumerable.Range(0, 10).ToList().ForEach((int i) => System.Console.Write(i));
  • 컴파일러의 추론을 통해 파라미터의 자료형을 생략한 코드.
Enumerable.Range(0, 10).ToList().ForEach(i => System.Console.Write(i));
  • 메서드 레퍼런스를 이용한 코드, 메서드 레퍼런스 자체는 2.0부터 지원하고 있다.
Enumerable.Range(0, 10).ToList().ForEach(System.Console.Write);

5.2.6 Go

foreach := func(slice []int, f func(int)) {
for _, i := range slice {
f(i)
}
}

foreach(
[]int{0,1,2,3,4,5,6,7,8,9},
func(i int) { println(i) },
)

imperative한 방식(습관적 방법)보다 코드 길이가 더 길어진 건 Go가 함수형 라이브러리를 제공하지 않아 foreach를 임시변통했기 때문이다. 함수형 코드가 임페러티브형 코드보다 간결하고 직관적임에는 변함이 없다.

5.2.7 Python

list(map(lambda x: print(x), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))

과 같이 map 함수를 사용하거나, [3]
[print(x) for x in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]

과 같이 list comprehension을 사용할 수도 있다.
[print(x) for x in range(0, 10)]

range()를 이용해 이렇게 줄일 수도 있다.

물론, JavaScript와 비슷하게,

print("\n".join(str(x) for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))

join()을 사용할 수도 있다. list comprehension은 str 형으로 변환하는 데 필요하므로 기억해두자.

5.2.8 PHP

echo implode("<br />",range(0,9));

5.2.9 Swift

(0...9).forEach{print($0)}

5.2.10 Ruby

(0...10).each { |x| puts x }

  1. Java의 경우 Predicate절을 이용하여 조건을 넘기는 방식으로 재활용성을 극대화 할 수 있다.
  2. 스트리밍, 또는 언어에 따라서는 체인으로 부르기도 하는 방식
  3. lambda를 쓰지 않고 list(map(print, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))와 같이 써도 상관없다.