본문 바로가기

프로그래밍 언어 정리/기초 다지기

[기초 다지기 - 3] 람다 표현식

함수형 프로그래밍을 통해 함수를 하나의 표현식으로 나타낸 것이 람다 표현식입니다.

 

익명함수이고, 사용하면 코드가 간결해지고, 재사용이 불가능하고,

그 외에도 함수형 프로그래밍의 특징과 장단점을 갖고 있습니다.

 

함수형 프로그래밍에 대한 내용은 다른 글에서 따로 설명하겠습니다.

 

 

알고리즘 문제풀이를 하다 보면 객체를 정렬하는 상황이 많이 생기는데, 이를 람다 표현식을 이용해 코드를 작성하겠습니다.

클래스 타입 배열의 객체들을 정렬하는 방법으로는 두 가지 방법이 있습니다.

 

  1. 클래스 내에서 정의한 우선순위를 적용시키는 방법 (기초 다지기 - 2: 클래스에서 구현)
  2. 클래스 외부에 작성한 함수를 이용해 우선순위를 적용시키는 방법

2번 방법을 람다 표현식으로 구현하였습니다.

 

추가로 이 글에서는 람다 표현식을 전부 변수에 담아줬지만, 담지 않고도 바로 사용이 가능합니다.

 


[Java]

 

인터페이스<제네릭> 객체명 = (파라미터) -> {실행문}

 

Java의 람다 표현식은 조금 독특합니다. 인터페이스를 이용하여 함수형 프로그래밍을, 즉 람다 표현식을 사용할 수 있습니다.

인터페이스의 추상 메소드를 익명 함수로 구체화하는 것이 Java에서의 람다 표현식입니다.

그런데 람다 표현식이 등장하기 전, 이미 인터페이스를 상속받아 추상 메소드를 오버라이딩하여 구체화하는 익명 구현 클래스라는 것이 이미 있었습니다.

우선 예시 코드를 보고, 무슨 차이가 있는지 아래에서 설명하겠습니다.

 

아래 두 코드는 인터페이스를 상속 받는 익명 구현 클래스를 사용한 방법과, 인터페이스를 람다 표현식으로 구현한 방법입니다.

 

import java.util.Arrays;
import java.util.Comparator;

class Student {
    String name;
    int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

public class main2 {
    public static void main(String[] args) {
    
    	// 익명 구현 클래스 이용 
        Comparator<Student> sorting = new Comparator<Student>() {
            @Override
            public int compare(Student n1, Student n2) {
                // TODO Auto-generated method stub
                if (n1.score == n2.score) return n1.name.compareTo(n2.name);
                return n2.score - n1.score;
            }
        };

        Student[] studentList = new Student[5];

        studentList[0] = new Student("이건우", 83);
        studentList[1] = new Student("박형기", 83);
        studentList[2] = new Student("박세인", 18);
        studentList[3] = new Student("강지훈", 33);
        studentList[4] = new Student("김현진", 100);

        Arrays.sort(studentList, sorting);

        for (Student st : studentList) {
            System.out.println(st.name + " " + st.score);
        }
    }    
}

 

Comparator를 상속받은 익명 구현 클래스를 사용하여 정렬을 한 코드입니다.

 

람다 표현식이 도입되기 전의 방법으로 Comparator를 상속해 익명으로 구현한 코드입니다.

이제 람다 표현식으로 위 코드를 간소화해보겠습니다.


import java.util.Arrays;
import java.util.Comparator;

class Student {
    String name;
    int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

public class main2 {
    public static void main(String[] args) {
    	// 람다 표현식 이용
        Comparator<Student> sorting = (n1, n2) -> {
            if (n1.score == n2.score) return n1.name.compareTo(n2.name); 
            return n2.score - n1.score;
        };

        Student[] studentList = new Student[5];

        studentList[0] = new Student("이건우", 83);
        studentList[1] = new Student("박형기", 83);
        studentList[2] = new Student("박세인", 18);
        studentList[3] = new Student("강지훈", 33);
        studentList[4] = new Student("김현진", 100);

        Arrays.sort(studentList, sorting);

        for (Student st : studentList) {
            System.out.println(st.name + " " + st.score);
        }
    }    
}

 

익명 구현 클래스가 아닌 람다 표현식을 이용하여 정렬하였습니다. 

 

구조를 먼저 설명하겠습니다.

  • 람다 표현식은 인터페이스 타입으로 저장되었습니다.
  • 파라미터가 한 개인 경우 소괄호가 생략 가능하고, 그 경우에 파라미터의 타입도 생략해야 합니다.
  • 실행문이 한 줄인 경우 중괄호가 생략 가능하고, 그 실행문이 return인 경우 return도 생략해야 합니다.

 

람다 표현식이 기존의 익명 구현 클래스의 역할을 일부 대신할 수 있지만 같지는 않습니다.

기존의 익명 구현 클래스는 인터페이스를 상속받는 클래스를 익명으로 구현하는 것이었다면 람다 표현식은 인터페이스의 추상 메소드를 익명 함수로 구현하는 것입니다.

그렇다면 인터페이스가 추상 메소드를 여러 개 갖고 있는 경우, 람다 표현식으로 어떤 추상 메소드를 구현하는 것인지에 대한 의문이 생길 수 있습니다.

 

그에 대한 해답은 함수형 인터페이스에서 찾을 수 있습니다.

함수형 인터페이스는 추상 메소드가 한 개인 인터페이스를 말합니다.

추상 메소드가 여러 개인 인터페이스를 람다 표현식으로 구현하면 컴파일러는 어떤 메소드를 람다 표현식으로 구현한 것인지 모르기 때문에 에러가 발생합니다.

그렇기에 모든 인터페이스에 람다 표현식을 적용할 수는 없고 함수형 인터페이스에만 람다 표현식을 적용할 수 있습니다.

 

정리하자면,

 

익명 구현 클래스는

  • 모든 인터페이스에 적용이 가능
  • 인터페이스를 상속받는 익명 클래스
  • 그렇기 때문에 인터페이스의 모든 추상 메소드를 오버라이드 해야 함

람다 표현식은

  • 추상 메소드가 한 개인 함수형 인터페이스에만 적용이 가능
  • 함수형 인터페이스의 추상 메소드를 구현하는 익명 함수

[C++]

 

function 객체명 = [캡쳐] (파라미터) {실행문}

 

#include <iostream>
#include <algorithm>
using namespace std;

class Student {
public:
    string name;
    int score;

    Student() { }

    Student(string name, int score) {
        this->name = name;
        this->score = score;
    }
};

int main(void) {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

	// 람다 표현식
    auto sorting = [] (Student st1, Student st2) {
        if (st1.score == st2.score) return st1.name.compare(st2.name) < 0 ? true : false;
        return st1.score > st2.score;
    };

    Student studentList[5];
    studentList[0] = Student("이건우", 83);
    studentList[1] = Student("박형기", 83);
    studentList[2] = Student("박세인", 18);
    studentList[3] = Student("강지훈", 33);
    studentList[4] = Student("김현진", 100);

    sort(studentList, studentList + 5, sorting);

    for (Student st : studentList) {
        cout << st.name << ' ' << st.score << '\n';
    }

    return 0;
}

 

람다 표현식을 이용하여 객체끼리의 대소비교 결과를 bool 타입으로 반환하는 익명 함수를 만들고, 이를 이용해 객체 배열을 정렬하였습니다.

 

구조를 설명하겠습니다.

  • 함수 객체이기 때문에 function 타입으로 저장할 수 있습니다. 하지만 일반 변수처럼 타입 추론이 가능하므로 auto로 사용할 수 있습니다.
  • 캡쳐 부분은 아래에서 따로 설명하겠습니다.
  • 파라미터는 따로 설명할 부분이 없습니다.
  • 해당 함수가 어떤 타입인지 따로 명시하지 않았으므로 실행문에서 결정됩니다. return이 없으면 void, return이 있으면 그냥 return하면 됩니다.

 

캡쳐 부분을 추가로 설명하겠습니다.

 

캡쳐란 외부 변수를 참조하는 공간입니다. 캡쳐 부분에 변수명을 넣어 놓으면 해당 변수는 람다 표현식 실행문 내에서 참조가 가능합니다.

그렇다고 변수명만 넣을 수 있는 것은 아니고 예외적으로 =, &을 넣을 수도 있습니다.

=을 명시하면 모든 외부 변수를 참조할 수 있고, &을 명시하면 모든 외부 변수를 참조 및 변경이 가능합니다.

=는 call by value, &은 call by reference와 같은 느낌으로 생각하시면 됩니다.

 

int A = 10;
int B = 20;
int C = 30;
int D = 40;

function fun1 = [A] () {
    int temp = A; // 가능
    temp = B; // 불가능
};

function fun2 = [=, &D] () {
    int temp = A; // 가능
    temp = B; // 가능
    C += 1; // 불가능
    D++; // 가능
};

function fun3 = [&, D] () {
    int temp = A; // 가능
    temp = B; // 가능
    C += 1; // 가능
    D++; // 불가능
};

function fun4 [=, A] () {}; // 불가능

function fun5 [&, &A] () {}; // 불가능

 

위 코드를 참고하시면 어렵지 않게 이해할 수 있을 겁니다.

 

 

선언과 동시에 파라미터에 값을 넣어 return값을 얻을 수도 있습니다.

 

int a = [] (int x, int y) {return x + y} (2, 4);

 


[Python]

 

객체명 = lambda 파라미터 : 실행문

 

파이썬답게 역시 간단합니다.

그리고 타 언어와 다른 점이 있는데, 파이썬의 람다 표현식은 무조건 한 줄이어야 합니다.

 

import sys

br = sys.stdin
bw = sys.stdout

class Student:
    def __init__(self, name, score):
        self.name = name
        self.score = score
        

# 람다 표현식 이용
sorting = lambda st : (-st.score, st.name)

st1 = Student("이건우", 83)
st2 = Student("박형기", 83)
st3 = Student("박세인", 18)
st4 = Student("김현진", 100)
st5 = Student("강지훈", 33)

studentList = []

studentList.append(st1)
studentList.append(st2)
studentList.append(st3)
studentList.append(st4)
studentList.append(st5)

studentList.sort(key = sorting)

for st in studentList:
    bw.write(st.name + ' ' + str(st.score) + '\n')

bw.close()
br.close()

 

람다 표현식을 이용하여 -score와 name을 저장하는 튜플을 반환하는 익명 함수를 만들고, 이를 sort함수의 key에 넣어 객체 리스트를 정렬하였습니다.

 

구조를 설명하겠습니다.

  • 파이썬이기 때문에 객체명만 있어도 됩니다.
  • lambda 키워드를 먼저 넣어 람다 표현식임을 명시합니다.
  • 파라미터는 괄호가 필요 없습니다.
  • 실행문 역시 괄호가 필요 없습니다.

C++과 마찬가지로 선언과 동시에 파라미터에 값을 넣을 수 있습니다.

 

a = (lambda x, y : x + y)(2, 4)