람다식 (r20200302판)

문서 조회수 확인중...



1. 개요


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

2. 장점


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

3. 단점


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


4. 유의사항


모든 언어에서 제공되지는 않는다: 대부분의 유명한 언어들은 지원하지만, 지원하지 않는 언어도 가끔씩 있다. 특히 고전적인 문법들의 경우 거의 모든 언어에서 제공됨을 보장할 수 있는 부분과는 차별된다. 대표적으로 C, Fortran, 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


for (int i = 0; i < 10; i++) { //C99표준부터 for문안에서 선언이 가능하다
    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 (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."
"; }

5.1.10. Swift


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

5.1.11. Ruby


for x in 0...10
  puts x
end

5.1.12. Kotlin


for (i in 0 until 10) {
    println(i)
}

5.2. 더 나은 방법


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

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::function
    으로 참조할 수 있다.
std::vector 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
    사용이 가능해졌다.).
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(i => console.log(i));
javascript는 java와 다르게 forEach의 첫번째 parameter로 실행함수를 넘겨주고 그 함수를 실행하는 방식인데
넘겨준 함수가 단순히 i를 받아서 i를 출력하므로 console.log라는 변수안에 담긴 함수와 하는일이 같다는 점을 이용하여 더 간결하게 줄인 방식
[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("
",range(0,9));

5.2.9. Swift


클로저 사용:
(0...9).forEach({(i: Int) -> Void in
   print(i)
})
더 간결한 방법:
(0...9).forEach{print($0)}

5.2.10. Ruby


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

5.2.11. Kotlin


(0 until 10).foreach { println(it) }

5.2.12. Haskell


하스켈은 일반적인 상황에서 사용되는
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]
mapM
mapM_
은 리스트 뿐만 아니라 각각 임의의
Traversable
Foldable
타입에 적용가능하다.

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