[JAVA] 함수형 프로그래밍(Functional Programming)
전통적인 명령형 프로그래밍(imperative programming)은 데이터를 어떻게(how) 처리할 것인지를 단계별로 정의하는 방식이다.
이 방식에서는 각 단계에서 데이터가 어떻게 처리되고 프로그램의 상태 또는 플래그를 계속해서 유지하게 된다.
이와 비교되는 선언적 프로그래밍(declarative programming)은 무엇을(what) 처리해야 하는지를 표현함으로써 알고리즘 구현으로 인한 이슈를 최소화하거나 제거한다.
어떻게(how)에 해당하는 구현 부분은 별도로 작성하여 사용하도록 한다.
함수형 프로그래밍(Functional Programming)
개요
- 함수형 프로그래밍은 프로그래밍 패러다임으로 프로그램을 함수를 통해서 표현하는 방식이며, 함수를 통해서 선언적 프로그래밍을 구현한 것이다.
- 선언적 프로그래밍 방식을 통해 로직에 대해서 더 높은 수준의 추상화가 되어있다.
- 널리 사용되는 프로그래밍 스타일이다. 반드시 사용해야 하는 것은 아니지만 사용하면 편한 부분이 존재한다.
특징
1. First class Function : 함수를 일반적인 객체처럼 변수에 할당하거나 parameter로 전달할 수 있고 다른 함수 안에서 return될 수도 있다. 즉, 일반 객체가 사용되는 것과 동일하게 다룰 수 있다.
2. Pure Function : 데이터 처리 결과는 입력 값에 의해서 결정된다. 같은 입력은 같은 결과를 가져다주며, 함수의 실행이 프로그램의 다른 부분에 영향을 주면 안된다. (side-effect가 없어야 한다)
3. Immutability : 함수형 프로그래밍의 결과로 얻은 데이터는 원본 데이터에 영향을 주지 않는다. 결과로 얻은 데이터는 원본 데이터가 변경된 것이 아닌 별도의 데이터이다. 만약 데이터 변경이 필요하다면 결과로 얻은 데이터를 원본에 재 할당 해주어야 한다.
람다 표현식(Lambda expression)
개요
함수형 언어는 람다 계산법(lambda calculus)으로 부터 발전되어 나온 것인데, 추상화와 함수 적용등 논리 연산을 다루는 형식 체계이다. 참고 - 람다 대수(wikipedia.org)
Java에서는 함수형 언어에 이러한 람다 계산법을 표현하기 위한 방법으로 람다 표현식(Lamdba expression)을 제공하는데, 이는 특수한 형태로 표현되는 익명의 함수를 의미한다.
람다 표현식으로의 변형
형태 | 코드 | 비고 |
---|---|---|
1. 일반적인 함수의 정의 | int sum(int x, int y){ return x+y;} | x,y를 입력 받아 그 합을 리턴한다. |
1-1. 함수를 익명화 | (int x, int y){return x+y;} | 리턴 타입과, 함수명을 생략 |
2-1. Lamdba 표현식 변형 (1) | (x,y) -> {return x+y;} | -> 는 입력과 출력을 구분하는 역할을 한다. |
2-2. Lambda 표현식 변형 (2) | (x,y) -> x+y | 한줄로 표현이 가능할때는 return, {} 생략 가능 |
Java는 static type을 가지는 언어이고, lambda 표현식은 자체적으로 타입을 표현하고 있지 않기 때문에 lambda 만으로는 함수형 프로그래밍의 ‘First Class Function’ 특성을 만족시킬 수 없다. (타입이 정해져 있지 않기 때문에 변수에 할당하거나 리턴하기 어렵다.)
Functional Interface
개요
함수형 인터페이스(Functional Interface)는 단 하나의 추상 메서드만 선언된 인터페이스를 의미한다. 추상 메소드를 선언만 해놓고 나중에 해당 메서드를 정의해서 사용하는 익명 객체를 만들 수 있다. 함수형 인터페이스 타입의 변수를 활용하여 lambda 표현식을 참조가능하다.
Functional Interface 구현
1. Object 객체 메소드 정의
Functional Interface 없이 단순하게 Object 객체를 이용하여 임의의 함수를 정의하는 경우 다음과 같은 문제가 발생할 수 있다.
Object func = new Object(){ int sum (int x, int y){ return x+y;};
int result = func.sum(10,20); // ERROR !! Object 클래스에 sum이 없음.
// (인터페이스에 정의되어 있지 않음)
2. Custom Interface 정의
호출하고자 하는 함수를 인터페이스의 추상 메소드로 정의한 뒤, 익명 클래스를 정의하여 사용할 수 있다.
interface CustomFunction {
int sum(int x, int y);
int minus(int x, int y);
}
CustomFunction func1 = new CustomFunction() {
@Override
public int sum(int x, int y) {
return x+y;
}
@Override
public int minus(int x, int y) {
return x-y;
}
};
int result = func1.sum(1,2);
CustomFunction func2 = (x,y) -> x+y; // ERROR !!
람다 표현식으로 제공하기 위해서는 1개의 추상메소드만 정의되어야 한다. 따라서 다음과 같이 수정되는 경우 lambda 표현식을 통해 Function객체를 생성할 수 있다.
interface CustomFunction {
int sum(int x, int y);
}
CustomFunction func2 = (x,y) -> x+y;
int result = func2.sum(1,2);
**/
3. @FunctionalInterface 어노테이션 사용
@FunctionalInterface를 위한 어노테이션을 사용하는 경우 컴파일 타임에 람다 표현식에 대한 검증작업을 수행해준다.
@FunctionalInterface
interface MyFunction{
public abstract int sum(int x, int y);
}
MyFunction func = (x,y) -> x+y ;
int result = func(10,20);
Pre-defined Functional Interface API
개요
위와 같이 @FunctionalInterface를 통해서 람다를 통한 함수 객체 생성을 제공하고 있지만,
매번 이러한 방식으로 인터페이스를 정의해서 사용하는 것은 비효율적이다 (귀찮다)
Java에서는 java.util.function 아래 함수형 인터페이스를 활용할 수 있는 API를 제공한다.
함수형 인터페이스 API | 메서드 | 내용 |
---|---|---|
Function<T,R> | R apply(T t) | 파라미터 O, 리턴값 O |
Predicate |
boolean test(T t) | 파라미터 O, 리턴값 O (Boolean) |
Supplier |
T get() | 파라미터 X, 리턴값 O |
Consumer |
void accept(T t) | 파라미터 O, 리턴값 X |
java.lang.Runnable | void run() | 파라미터 X, 리턴값 X |
Functional Interface API 예시
// 1. 스태틱 메소드
Function<String, Integer> f1 = x -> Integer.parseInt(x);
Function<String, Integer> f2 = Integer::parseInt;
// 2. 임의 객체 인스턴스 메소드
Function<String, Integer> f1 = x -> x.length();
Function<String, Integer> f2 = String::length;
// 3. 특정 객체의 인스턴스 메소드
Function<String, Integer> f1 = x -> object.isEven(x);
Function<String, Integer> f2 = object::isEven;
// 4. 생성자 참조
Supplier<SomeClass> f1 = () -> new SomeClass();
Supplier<SomeClass> f2 = () -> SomeClass::new;
Functional Interface API 확장
Functional Interface API는 입력받는 파라미터 개수, 타입에 따라서 다음과 같이 추가적으로 제공되어 진다.
함수형 인터페이스 API | 메서드 | 내용 |
---|---|---|
BiFunctional<T, U, R> | R apply(T t, U u) | 2개의 파라미터를 받아 처리후 리턴 |
BiConsumer<T,U> | void accept(T t, U u) | 2개의 파라미터를 받아 처리 |
BiPredicate<T,U> | boolean test(T t, U u) | 2개의 파라미터를 받아 boolean 값을 리턴 |
UnaryOperator |
T apply(T t) | 1개의 파라미터를 받아 리턴. 입력 타입과 출력타입이 동일 |
BinaryOperator |
T apply(T t1, T t2) | 2개의 동일한 타입의 파라미터를 받아, 동일한 타입의 결과 리턴 |