최근 수정 시각 : 2024-10-12 13:17:36

람다식


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

1. 개요

  • 람다식(lambda expression)은 일부 프로그래밍 언어에서 사용되는 개념으로, 익명 함수(匿名函數, Anonymous functions)를 지칭하는 용어이다. 별도의 명칭을 지정해 정의해 주는 일반 함수와 달리 람다 함수는 이름 없이 그 내용물만 정의하기 때문이다.
  • 람다식 또는 람다 함수, 더 짧게는 람다라 부른다.

2. 특징

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

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

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

2.1. 장점

  • 코드의 간결성: 효율적인 람다 함수의 사용을 통하여 불필요한 루프문의 삭제가 가능하며, 동일한 함수를 재활용할 수 있는 여지가 커진다. Java의 경우 Predicate절을 이용하여 조건을 넘기는 방식으로 재활용성을 극대화할 수 있다.
  • 필요한 정보만을 사용하는 방식을 통한 퍼포먼스 향상: 지연 연산을 지원하는 방식(스트리밍, 또는 언어에 따라서는 체인으로 부르기도 하는 방식)을 통하여 효율적인 퍼포먼스를 기대할 수 있다. 이 경우 메모리상의 효율성 및 불필요한 연산의 배제가 가능하다는 장점이 있다.

2.2. 단점

  • 어떤 방법으로 작성해도 모든 원소를 전부 순회하는 경우는 람다식이 조금 느릴 수밖에 없다.(어떤 방법으로 만들어도 최종 출력되는 바이트코드나 어셈블리 코드는 단순 반복문보다 몇 단계를 더 거치게 된다.)
  • 익명 함수의 특성상 함수 외부의 캡처를 위해 캡처를 하는 시간 제약, 논리 제약적인 요소도 고려해야 하며, 디버깅 시 함수 콜스택 추적이 극도로 어렵다.
  • 람다식을 남용하면 오히려 코드를 이해하기 어려울 수 있다. 따라서 람다식을 사용할 경우 주석을 다는 것이 권장된다. 특히 웹 코딩에서의 화살표 함수, C#에서 LINQ를 남용할 경우 스파게티 코드가 되거나 나중에 디버깅할 적에 굉장히 고역이 따른다.

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

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

3.1. C++

C++ 11부터 지원한다. [캡처 블록](매개 변수) {표현식} 형태로 작성한다. 캡처는 복사(=)와 참조(&) 중 선택할 수 있으며, 전달할 변수마다 캡처 형식을 다르게 지정할 수 있다. C++의 람다식은 함수 객체(Functor)로써 전달된다.
  • 전통적인 방법
    {{{#!syntax cpp
for (int i = 0; i < 10; i++) {
std::cout << i;
}
}}}
  • 람다식을 사용하여 만드는 방법
    • 람다식을 포인터std::function으로 참조할 수 있다.
      {{{#!syntax cpp
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; });
}}}
  • 컴파일러의 추론을 통해 파라미터의 자료형을 생략한 코드이다. C++ 14부터 람다의 인자에 auto 사용이 가능해졌다.
    {{{#!syntax cpp
std::for_each(std::begin(v), std::end(v), [](auto n) { std::cout << n; });
}}}
  • 이 경우는 람다보다 "Range-based for loop"가 가독성이 더 좋다.
    {{{#!syntax cpp
for (auto n : v) std::cout << n;
}}}

3.2. C#

  • 전통적인 방법
    {{{#!syntax csharp
for (int i = 0; i < 10; i++)
{
System.Console.Write(i);
}
}}}
  • 람다식을 사용하여 만드는 방법
    • .NET Framework 3.5부터 지원되는 람다식을 명시한 코드
      {{{#!syntax csharp
Enumerable.Range(0, 10).ToList().ForEach((int i) => System.Console.Write(i));
}}}
  • 위의 람다식을 컴파일러의 추론을 통해 파라미터의 자료형을 생략한 코드.
    {{{#!syntax csharp
Enumerable.Range(0, 10).ToList().ForEach(i => System.Console.Write(i));
}}}
  • 대리자를 이용한 코드, 대리자는 2.0부터 지원하고 있다.
    {{{#!syntax csharp
Enumerable.Range(0, 10).ToList().ForEach(System.Console.Write);
}}}

3.3. Go

  • 전통적인 방법
    {{{#!syntax go
for i := 0; i < 10; i++ {
println(i)
}
}}}
  • 람다식을 사용하여 만드는 방법
    {{{#!syntax 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를 임시변통했기 때문이다. 함수형 코드가 명령형 코드보다 간결하고 직관적임에는 변함이 없다.

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

  • 전통적인 방법
    {{{#!syntax java
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}}}
  • 람다식을 사용하여 만드는 방법
    • Java 8부터 지원되는 람다식을 사용한 코드
      {{{#!syntax java
IntStream.range(0, 10).forEach((int value) -> System.out.println(value));
}}}
  • 컴파일러의 추론을 통해 파라미터의 자료형을 생략한 코드
    {{{#!syntax java
IntStream.range(0, 10).forEach(value -> System.out.println(value));
}}}
  • 메서드 참조를 사용한 코드
    {{{#!syntax java
IntStream.range(0, 10).forEach(System.out::println);
}}}

3.6. JavaScript

  • 자바스크립트에서는 람다식을 화살표 함수(arrow function)라고 부른다.
  • 전통적인 방법
    {{{#!syntax javascript
for (let i = 0; i < 10; i++) {
console.log(i);
}
}}}
  • 람다식을 사용하여 만드는 방법
    {{{#!syntax javascript
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(i => console.log(i));
}}}
  • Array.from을 추가로 이용한 방법[2]
    {{{#!syntax javascript
Array.from({length: 10}, (_, i) => console.log(i));
}}}

3.7. Kotlin

  • 전통적인 방법
    {{{#!syntax kotlin
for (i in 0 until 10) {
println(i)
}
}}}
  • 람다식을 사용하여 만드는 방법
    {{{#!syntax kotlin
(0 until 10).forEach { println(it) }
}}}
  • Kotlin에서는 Java 람다식에서 필요한 변수 이름을 생략할 수 있다. 생략 시에 Kotlin은 자동으로 변수 이름을 "it"(그거)로 지정한다.

3.8. PHP

  • 전통적인 방법 for 또는 foreach
    {{{#!syntax php
for ($i = 0; $i < 10; $i++) {
echo "$i\n";
}

//또는 foreach 를 사용
foreach (range(0, 9) as $x) {
echo $x;
}
}}}
  • 람다식을 사용하여 만드는 방법
    {{{#!syntax php
array_map(fn($x) => print($x), range(0, 9));
}}}
  • map 함수인 array_map을 이용한 코드
  • 익명 함수는 PHP 5.3에서, 화살표 함수는 PHP 7.4에서 도입되었다.

3.9. Python

  • 전통적인 방법
    {{{#!syntax python
for i in range(10):
print(i)
}}}
  • 람다식을 사용하여 만드는 방법
    {{{#!syntax python
list(map(lambda x: print(x), range(0, 10)))

# lambda를 쓰지 않고 아래처럼 써도 상관 없다.
list(map(print, [1,2,3,4,5,6,7,8,9]))
}}}
  • 위와 같이 map 함수를 사용하거나 아래와 같이 list comprehension을 사용할 수도 있다.
    {{{#!syntax python
[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부터 시작하는 예제 특성상 아예 시작점을 뺄 수 있다.
[print(x) for x in range(10)]
}}}
  • 물론, JavaScript와 비슷하게 아래처럼 join()을 사용할 수도 있다.
    {{{#!syntax python
print("\n".join(map(str, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])))
}}}

3.10. Ruby

  • 전통적인 방법
    {{{#!syntax ruby
for x in 0...10
puts x
end
}}}
  • 람다식을 사용하여 만드는 방법
    {{{#!syntax ruby
(0...10).each { |x| puts x }
}}}

3.11. Rust

  • 전통적인 방법
    {{{#!syntax rust
for i in 0..10 {
println!("{i}");
}
}}}
  • 사실 for문을 사용하긴 했지만, 러스트에선 for문이 내부적으로 이터레이터를 사용하는 문법적 설탕일 뿐이기에 진정한 의미의 C 언어와 비슷한 for문은 존재하지 않는다.
  • 대신 아래처럼 while문을 사용할 수 있다.
    {{{#!syntax rust
let mut i = 0;

while i < 10 {
println!("{i}");
i += 1;
}
}}}
  • 람다식을 사용하여 만드는 방법
    {{{#!syntax rust
(1..10).for_each(|i| println!("{i}"));
}}}
  • 이 경우 |i| {...} 부분이 클로저(람다)이다.
  • 참고로 이 코드는 rustc 1.58 버전 이상에서만 컴파일 된다. 그 이하의 버전에서는 println!("{}", i)와 같은 형태로 작성해야 하니 주의해야 한다.

3.12. Scala

  • 전통적인 방법
    {{{#!syntax typescript
var i: Int = 0
while (i < 10) {
println(i)
i += 1
}
}}}
  • 람다식을 사용하여 만드는 방법
    {{{#!syntax typescript
(0 until 10) foreach println
}}}

3.13. Swift

  • 전통적인 방법
    {{{#!syntax ruby
for x in 0..<10 {
print(x)
}
}}}
  • 람다식을 사용하여 만드는 방법
    • Swift에서는 람다 함수를 모두 클로저(Closure)라고 통칭한다.
    • 아래는 클로저를 사용한 것이다.
      {{{#!syntax ruby
(0...9).forEach({(i: Int) -> Void in
print(i)
})
}}}
  • 아래는 더 간결한 방법이다.
    {{{#!syntax ruby
(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] http://java.dzone.com/articles/why-we-need-lambda-expressions [2] https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/from