함수형 프로그래밍이란?
Rxjava, RxAndroid를 공부하는 중인데 둘을 공부하다 보면 함수형 프로그래밍이란 말이 매우 자주 보이고, Rx를 공부하려면 함수형 프로그래밍에 대한 이해가 필요해 보여 따로 정리해둔다.
함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나다. 명령형 프로그래밍에선 상태를 바꾸는 것을 강조하는 것과 달리 함수형 프로그래밍은 함수의 응용을 강조한다. 프로그래밍이 문이 아닌 선언으로 수행되는 선언형 프로그래밍을 따르고 있다. 함수형 프로그래밍은 1930년대에 계산 가능성, 결정 문제, 함수 정의, 함수 응용과 재귀를 연구하기 위해 개발된 형식 체계인 람다 대수에 근간을 두고 있다. 다수의 함수형 프로그래밍 언어들은 람다 연산을 발전시킨 것으로 볼 수 있다
https://en.wikipedia.org/wiki/Functional_programming
함수형 프로그래밍은 함수를 적용하고 구성해 프로그램을 구성하는 프로그래밍 패러다임이다. 이는 함수 정의가 프로그램의 실행 상태를 업데이트하는 명령문의 시퀀스가 아닌 값을 다른 값에 매핑하는 표현식 트리인 선언적 프로그래밍 패러다임이다. 함수형 프로그래밍에서 함수는 1급 시민으로 취급된다. 즉, 다른 데이터 유형과 마찬가지로 이름(로컬 식별자 포함)에 바인딩되고 인수로 전달되고 다른 함수에서 반환될 수 있다. 이를 통해 작은 기능이 모듈 방식으로 결합되는 선언적이고 구성 가능한 스타일로 프로그램을 작성할 수 있다
1급 시민이란 단어가 나온다. 이건 무슨 뜻일까?
https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B8%89_%EA%B0%9D%EC%B2%B4
일급 객체란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다. 보통 함수에 매개변수로 넘기기, 수정하기, 변수에 대입하기 같은 연산을 지원할 때 일급 객체라고 한다...(중략)...로빈 포플스톤은 일급 객체를 구성하는 요소는 기본적인 권리가 있다는, 다음의 정의를 내렸다
1. 모든 요소는 함수의 실제 매개변수가 될 수 있다
2. 모든 요소는 함수의 반환값이 될 수 있다
3. 모든 요소는 할당 명령문의 대상이 될 수 있다
4. 모든 요소는 동일 비교의 대상이 될 수 있다
https://en.wikipedia.org/wiki/First-class_citizen
일급 시민(type, object, entity or value)은 일반적으로 다른 엔티티에서 사용할 수 있는 모든 작업을 지원하는 엔티티다. 이 작업에는 일반적으로 인수로 전달, 함수에서 반환, 수정 및 변수 할당이 포함된다
정리하면 함수의 매개변수나 리턴값이 될 수 있고 할당 명령문, 동일 비교 대상이 되면 일급 객체라고 할 수 있는 듯하다. 당연히 이해되지 않는다. 이걸로 이해했으면 이 글 안 썼지
다른 곳에선 어떻게 설명하는지 확인해봤다.
https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function
어떤 프로그래밍 언어의 함수가 다른 변수처럼 취급될 때 일급 함수를 갖는다고 한다. 그런 언어에서 함수는 다른 함수에 인수로 전달될 수 있고, 다른 함수에 의해 반환될 수 있으며 변수에 값으로 할당될 수 있다
https://stackoverflow.com/questions/5178068/what-is-a-first-class-citizen-function
함수가 일급 시민인 함수형 프로그래밍 패러다임의 예를 살펴본다. 함수가 일급 시민이라고 할 때 함수로 다음을 수행할 수 있다
1. 변수에 함수를 할당할 수 있다
2. 자료구조에 함수를 할당할 수 있다
3. 함수는 다른 함수에 인수로 전달될 수 있다
4. 함수에서 함수를 반환할 수 있다
함수형 프로그래밍 언어에선 위 작업을 수행할 수 있다. 자바에서 메서드는 함수와 동일하다. 메서드로 위의 작업을 수행하는 건 불가능하다. 그러나 위의 모든 것은 자바 객체로 가능하다. 따라서 객체는 자바의 일급 시민이다. 자바 8은 함수형 인터페이스와 람다 표현식(람다식)을 사용해서 다른 메서드에 메서드(정확하게는 메서드 동작) 전달을 지원한다. 그러나 그것이 자바가 일급 시민으로서의 기능을 갖고 있다는 걸 의미하진 않는다. 함수를 전달하거나 함수에서 함수를 반환하는 것과 같은 작업을 수행하는 기능은 데이터뿐 아니라 동작을 전달할 수 있기 때문이다
참고로 함수를 일급 시민으로 취급할 경우 일급 함수를 갖는다고 말한다.
https://blog.fearcat.in/a?ID=00001-4eef82d8-3a7b-47dd-919a-4b02cf6c326d
일급 시민의 개념은 1960년대 영국의 컴퓨터 과학자 Christopher Strachey에 의해 제안됐다. 그러나 그는 일급 시민에 대한 엄격한 정의를 내리지 않았다. 일급 시민의 정의에 대해 Programming Language Pragmatics라는 책에 답이 나와 있다.
- 일반적으로 프로그래밍 언어의 값은 매개변수로 전달되거나 서브루틴에서 반환되거나 변수에 할당될 수 있으면 일급 상태라고 한다
프로그래밍 언어에서 일급 시민은 함수 매개변수로 쓰일 수 있고 함수의 리턴값으로 쓰일 수 있으며 변수에 할당될 수도 있다. 자바스크립트에선 함수 표현식을 써서 함수를 정의할 수 있다. 따라서 함수는 변수에 저장할 수 있다. ECMAScript의 함수명은 변수 자체기 때문에 함수를 값(value)으로 쓸 수도 있다. 즉, 하나의 함수가 다른 함수에 매개변수로 전달될 수 있을 뿐 아니라 한 함수가 다른 함수의 결과로 반환될 수도 있다(클로저)
수십 년 전에 정의를 개념을 제시한 사람도 따로 정의를 내리지 않았단다. 이 외에 다른 글들을 찾아본 결과 아래의 특징을 가진 함수는 일급 시민이라고 할 수 있겠다.
- 함수의 매개변수로 전달할 수 있다
- 함수의 리턴값이 될 수 있다 = 리턴값이 함수일 수 있다
- 변수에 함수를 할당할 수 있다
말은 쉽다. 코드로 확인해보고 싶어서 찾아봤다. 이 포스팅의 예제 코드를 직접 실행하려 한다면 자바 8 이상이 설치되어 있어야 한다.
일급 및 고차 함수
위에서 상술한 대로, 함수를 일급 시민으로 취급하는 경우엔 일급 함수를 갖는다고 말한다. 이는 기본적으로 메서드가 다른 엔티티에서 사용할 수 있는 모든 작업(함수를 변수에 할당, 다른 함수에 인수로 전달, 함수를 다른 함수의 값으로 반환하는 것 등)을 지원할 수 있음을 의미한다.
이렇게 함수를 인자로 받거나 결과로 반환하는 함수를 이르는 단어가 고차 함수다. 이 고차 함수가 되기 위해선 일급 시민이어야 한다는 조건이 있다.
컬렉션 메서드 중 sort()로 커스텀 비교자(comparator)를 만든다고 가정한다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(4);
list.add(7);
list.add(1);
list.add(9);
list.add(5);
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println(list);
}
}
Collections.sort()가 무슨 메서드인지 알기만 하면 부연설명도 필요 없는 매우 간단한 코드지만 콜백 때문에 조금 지저분해 보인다.
자바 8부터는 아래와 같이 조금 더 단순한 형태로 사용할 수 있다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(4);
list.add(7);
list.add(1);
list.add(9);
list.add(5);
list.sort((o1, o2) -> o1.compareTo(o2));
System.out.println(list);
}
}
여기서 한번 더 단순화한다면 아래 형태가 된다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(4);
list.add(7);
list.add(1);
list.add(9);
list.add(5);
list.sort(Integer::compareTo);
System.out.println(list);
}
}
콜론 두 개(::)는 람다식에서 메서드 참조(Method reference)라고 하는데, 람다식이 한 메서드만 호출할 경우 콜론 두 개를 써서 간략하게 만드는 기법이다. (o1, o2) -> o1.compareTo(o2) 형태의 코드도 복잡해 보였는지 Integer::compareTo 형태로 극한으로 줄여 쓸 수 있도록 만들어놨다.
다른 예제를 확인해보자. Stream API를 사용해 컬렉션 안에서 "d"를 찾아 대문자로 바꿔 출력하는 예제다.
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> list = Arrays.asList("a1", "d3", "z4", "e5", "6");
// for문으로 반복해서 찾는 방식
for (int i = 0; i < list.size(); i++) {
String element = list.get(i);
if (element.startsWith("d")) {
System.out.println(element.toUpperCase());
}
}
// Stream API를 사용한 방식
list.stream()
.filter(element -> element.startsWith("d"))
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
// D2
// D2
코드를 실행하면 콘솔에는 D2가 2번 출력된다. 같은 동작을 하는 코드지만 for문을 사용하는 것보다 Stream API 쪽이 더 간결한 걸 볼 수 있다.
그렇다면 이 함수형 프로그래밍 패러다임은 왜 필요한 건가? 객체지향 프로그래밍 패러다임으로도 충분히 많은 문제들을 해결할 수 있을 것이다.
다음은 함수형 프로그래밍의 장점과 사용하는 이유에 대해 찾아본 결과들이다.
장점
1. 더 간단하게 문제를 해결하는 데 도움이 된다
2. 모듈성이 향상된다
3. 복잡한 문제 해결을 위해 프로그램에서 람다 미적분을 구현할 수 있다
4. 일부 프로그래밍 언어는 코드 유지 관리성을 향상시키는 중첩 함수를 지원한다
5. 복잡한 문제를 간단한 조각으로 줄인다
6. 개발자 생산성을 향상시킨다
7. 코드를 빠르게 디버그하는 데 도움이 된다.
단점
1. 초보자가 이해하기 어렵다. 새 프로그래머를 위한 초보자 친화적 패러다임 접근법이 아니다
2. 프로젝트 규모가 큰 경우 유지보수가 어렵다
3. 함수형 프로그래밍에서 재사용성은 까다로운 작업이다
https://en.wikipedia.org/wiki/Functional_programming
함수형 프로그래밍 언어는 일반적으로 C, 파스칼 같은 명령형 언어보다 CPU 및 메모리 사용 효율성이 떨어진다. 이것은 배열 같은 일부 변경 가능한 자료구조가 현재 하드웨어를 사용해 매우 간단하게 구현된단 사실과 관련 있다. flat array는 깊게 파이프라인된 CPU를 써서 매우 효율적으로 접근하거나, 캐시를 통해 효율적으로 미리 가져오거나(복잡한 포인터 추적 없이) SIMD 명령으로 처리 가능하다. 동등하게 효율적인 범용 불변 대응물을 만드는 것도 쉽지 않다. 순수 함수형 언어의 경우 최악의 경우 사용되는 메모리 셀 수의 대수 속도 저하가 발생한다...(중략)
https://spin.atomicobject.com/2019/08/29/functional-prog-pros-cons/
장점
1. 강력한 추상화
2. 본질적으로 평행함
3. 쉽게 테스트, 디버깅 가능
4. 빠른 개발
단점
1. I/O
2. 재귀
3. 용어 문제
4. 컴퓨터의 비 기능
5. 상태 저장 프로그래밍의 어려움
함수형 프로그래밍은 대부분의 범용 프로그래밍에 매우 적합하지만 상태 관리 및 I/O 영역에서의 제한사항에 주의해야 한다
위 포스팅의 경우 장단점 항목 별 설명하는 내용이 많아 모든 내용을 가져오는 대신 항목만 가져왔다. 궁금한 사람은 직접 들어가서 읽어보는 게 좋겠다.
그 외 다른 글들도 찾아봤지만, 함수형 프로그래밍 패러다임이 객체지향 프로그래밍 패러다임보다 나은 패러다임이라곤 할 수 없는 듯하다. 다 떠나서 러닝 커브가 높아 초보자에게 높은 진입장벽이 있다는 게 제일 큰 단점으로 느껴진다.
공부해서 익숙해진다면 별 문제야 되지 않겠지만, 개발 특성상 다른 개발자들과 협업해야 하는데 앞서 개발하던 개발자가 함수형 프로그래밍에 빠삭해서 함수형으로 코드를 짜 놨다면 늦게 들어온 개발자는 함수형 프로그래밍 요소가 보일 때마다 당혹스러울 것이다. 생산성이 영향받을 수 있겠다고도 생각되는 부분이다.
어차피 함수형 프로그래밍 또한 여러 패러다임 중 하나일 뿐이니 프로젝트 상황 및 주변 환경에 따라 가장 적절한 프로그래밍 패러다임을 적용해 프로그램을 만들면 될 듯하다.