mir.pe (일반/밝은 화면)
최근 수정 시각 : 2024-12-08 18:44:32

람다식

람다 함수에서 넘어옴

프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all" <colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C( 포인터 · 구조체 · size_t) · C++( 자료형 · 클래스 · 이름공간 · 상수 표현식 · 특성) · C# · Java · Python( 함수 · 모듈) · Kotlin · MATLAB · SQL · PHP · JavaScript · Haskell( 모나드)
마크업 문법 HTML · CSS
개념과 용어 함수( 인라인 함수 · 고차 함수 · 람다식) · 리터럴 · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 #! · == · === · deprecated · NaN · null · undefined · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

1. 개요

2. 특징

프로그래밍 언어학적으로 파고들면 이것만 한 달 이상 배우는 경우도 많으며, 실제로 여러 대학들에서 사용하는 프로그래밍 언어 교재에서도 꽤나 많은 분량을 차지하는 개념이다. 물론 결점이 없는 개념이 결코 아니기에 람다식을 까는 논문 또한 해마다 나온다.

실무적으로는 코드를 간결하게 만들고, 지연 연산으로 성능을 높이고, 반복 관련 코드의 불필요한 부분들을 제거할 수 있으므로 중요하다. 람다식은 주로 고차 함수에 인자(argument)로 전달되거나 고차 함수가 돌려주는 결괏값으로 쓰인다. 필요한 적재적소에 람다식을 투입하는 프로그래머는 효율과 결과 모두를 가져오는 훌륭한 프로그래머라고 볼 수 있다. 그러나 간결함을 얻은 대신 가독성을 포기한 문법이라고 부를 정도로, 조금이라도 라인이 길어지면 난해해지는 데다 컴퓨터적 사고가 필요하기 때문에 초심자들은 람다식의 개념을 이해하는 것부터 장벽이 되곤 한다.[1]

예제에 나오듯, 1부터 10까지 1씩 증가하면서 이 코드를 순차적으로 실행해라고 지시하는 것보다는, 여기 있는거 다 해라고 지시하는 것이 더욱 직관적이고 간결하다. 이러한 방식을 Tell, Don't Ask 원칙이라 하며, '묻지 않고 시키기'이다.

2.1. 장점

2.2. 단점

2.3. 유의 사항

모든 언어에서 제공되지는 않는다. 대부분의 유명한 언어들은 지원하지만, 지원하지 않는 언어도 가끔씩 있다. 특히 고전적인 문법들의 경우 거의 모든 언어에서 제공됨을 보장할 수 있는 부분과는 차별된다. 대표적으로 C, Fortran, Pascal 등이 지원하지 않는다. Java는 8부터 지원하며, C++은 C++ 11부터 지원한다.

Microsoft .NET은 이미 Framework 2.0부터 대리자(delegate), 메서드 참조, 제너릭을 통해 비슷하게나마 지원하고 있었지만, 본격적으로 람다식이 지원되기 시작한 건 LINQ가 추가된 Framework 3.5부터이다. 사실상 람다식의 대유행을 야기한 장본인이다. 람다식(익명 함수) 자체는 LISP에서도 사용된, 꽤 오래된 개념이다. LISP는 그 자체가 함수형 언어이기도 하고, 현재 아주 메이저하게 사용되는 언어라고 보긴 무리가 다소 있지만 워낙 역사가 길어서 다른 언어에 미친 영향이 크다. 그러나 객체 지향 언어나 스크립트 언어 등에서 적극적으로 람다를 사용하는 경향이 나타나게 된 것은 이 이후로 봐도 무방하다. 물론 대부분은 굳이 람다식을 쓰지 않고도 사용할 수는 있다.

언어에 따라서는 람다식 로직의 일부를 재활용할 수 없는 경우도 있다. Java의 단말 연산(Terminal Operation)이 이에 해당하는데, 이런 언어에서는 단말 연산 수행 즉시 결과가 '닫히며', 그 이전의 중간 결과에서 람다식 연산을 다시 하려고 하면 오류가 발생한다. 따라서, 이런 언어에서는 미리 연산을 수행하여 중간 결과를 도출한 뒤, 그 결과에서 람다식을 다시 사용해야 한다. .NET에는 이런 제약이 덜하기에 람다식 로직의 재활용이 얼마든지 가능하다.

3. 예제

0부터 9까지의 숫자를 출력하는 코드를 각 언어로 설명한다.

1. 전통적인 방법
for문 등을 이용한 아주 기초적인 코드이다. 각각의 요소들을 하나하나 검증하며 순차적으로 값을 확인하여 조건절이 끝날 때까지 진행한다. 특별한 경우가 아니라면 최적화되지 않고 들어오는 순서대로 진행된다.[2]

2. 람다식을 사용하여 만드는 방법
for문과 i 등의 변수를 사용하는 방식과 다르게 매번 같은 동작이 보장되어 병렬 처리가 보다 수월해진다.

3.1. C++

C++ 11부터 지원한다. [캡처 블록](매개 변수) {표현식} 형태로 작성한다. 캡처는 복사(=)와 참조(&) 중 선택할 수 있으며, 전달할 변수마다 캡처 형식을 다르게 지정할 수 있다. C++의 람다식은 함수 객체(Functor)로써 전달된다.
for (int i = 0; i < 10; i++) {
std::cout << i;
}
}}} std::array<int, 10> 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(std::begin(v), std::end(v), [](auto n) { std::cout << n; });
}}} for (auto n : v) std::cout << n;
}}}

3.2. C#

for (int i = 0; i < 10; i++)
{
System.Console.Write(i);
}
}}} Enumerable.Range(0, 10).ToList().ForEach((int i) => System.Console.Write(i));
}}} Enumerable.Range(0, 10).ToList().ForEach(i => System.Console.Write(i));
}}} Enumerable.Range(0, 10).ToList().ForEach(System.Console.Write);
}}}

3.3. Go

for i := 0; i < 10; i++ {
println(i)
}
}}} 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) },
)
}}}

3.4. Haskell

하스켈에는 for문이 없기 때문에 이 문서에서 말하는 전통적인 방식으로 기술하기 어렵다.

하스켈은 일반적인 상황에서 사용되는 map 외에도, 모나딕 함수 전용의 mapM이 있다. 이는 map을 모나딕 함수에 사용할 시 결과의 타입이 모나딕 타입의 리스트 Monad m => [m a]가 되기 때문이다. mapM은 리스트의 각 원소에 인자로 받은 함수를 적용한 결과를 순차적으로 bind (>>=)하여 리스트의 모나드 Monad m => m [a]를 만든다.

다음은 map을 사용해 리스트의 각 원소에 1을 더하는 코드이다.
map (\x -> x+1) [0..9]
mapM을 사용한 입출력은 다음과 같다.
mapM print [0..9]
그런데 IO 출력에 사용되는 putStrLn 등 결괏값이 의미를 가지지 않는 함수도 있다. 이처럼 결괏값이 필요하지 않은 경우 일반적으로 mapM_을 사용한다. mapM_ 결과의 타입은 Monad m => m ()로 아무런 정보를 담고 있지 않다.
mapM_ print [0..9]
mapMmapM_은 리스트뿐만 아니라 각각 임의의 TraversableFoldable 타입에 적용 가능하다.

3.5. Java

for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}}} 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);
}}}

3.6. JavaScript

for (let i = 0; i < 10; i++) {
console.log(i);
}
}}} [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(i => console.log(i));
}}} Array.from({length: 10}, (_, i) => console.log(i));
}}}

3.7. Kotlin

for (i in 0 until 10) {
println(i)
}
}}} (0 until 10).forEach { println(it) }
}}}

3.8. PHP

for ($i = 0; $i < 10; $i++) {
echo "$i\n";
}

//또는 foreach 를 사용
foreach (range(0, 9) as $x) {
echo $x;
}
}}} array_map(fn($x) => print($x), range(0, 9));
}}}

3.9. Python

for i in range(10):
print(i)
}}} list(map(lambda x: print(x), range(0, 10)))

# lambda를 쓰지 않고 아래처럼 써도 상관 없다.
list(map(print, [1,2,3,4,5,6,7,8,9]))
}}} [print(x) for x in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]

# 아래처럼 range()를 이용해 줄일 수도 있다.
# 이렇게 정수 리스트 부분을 바꾸면 직관성이 올라갈 것이다.
[print(x) for x in range(0, 10)]

# 0부터 시작하는 예제 특성상 아예 시작점을 뺄 수 있다.
any(print(x) for x in range(10))
}}} print("\n".join(map(str, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])))
}}}

3.10. Ruby

for x in 0...10
puts x
end
}}} (0...10).each { |x| puts x }
}}}

3.11. Rust

for i in 0..10 {
println!("{i}");
}
}}} let mut i = 0;

while i < 10 {
println!("{i}");
i += 1;
}
}}} (1..10).for_each(|i| println!("{i}"));
}}}

3.12. Scala

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

3.13. Swift

for x in 0..<10 {
print(x)
}
}}} (0...9).forEach({(i: Int) -> Void in
print(i)
})
}}} (0...9).forEach{print($0)}
}}}

3.14. Scheme

Scheme에 for문이 없기 때문에 이 문서에서 말하는 전통적인 방식으로 기술하기 어렵다.

람다식을 사용하여 만드는 방법은 다음과 같다.
(for-each
  (lambda (x) (display x) (newline))
  '(1 2 3 4 5 6 7 8 9))

[1] 일급 객체 또는 일급 함수에 대해 이해해야만 이해가 가능한 개념이기에 관련 지식이 없다면 먼저 공부하고 오는 것을 추천한다. [2] http://java.dzone.com/articles/why-we-need-lambda-expressions [3] https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/from