728x90
함수형 프로그래밍 소개
어떤 것이 언어를 함수형
으로 만드는가?
- 프로그래밍 언어가 추상 함수(abstract function)을 생성하고 조합함으로써 논리 연산을 표현할 수 있을 때 함수형 언어로 간주.
람다 대수를 구성하는 세가지 구성 요소
- 추상화: 단일 입력을 받는 익명 함수, 람다를 의미
- 응용: 값에 추상화가 적용되어 결과를 생성. 개발자의 관점에서는 함수나 메서드 호출을 의미.
- 베타 축약: 추상화된 변수를 적용된 인수로 대체
- 제어 흐름이나 알고리즘을 설명하지 않고, 연산의 논리를 선언적 문장으로 표현할 수 있다.
- 함수형 프로그래밍은 불변성을 강조하며, 상태를 변경하는 대신 새로운 상태를 생성한다.
- 문장이 아닌 표현식을 사용하여 결과와 프로그램이 작동하는 방식을 설명.
- 무엇을 해야하는지가 아닌,
무엇을 원하는지
표현
// 변수에 초기값을 할당하여 프로그램에 상태를 대입
int totalTreasure = 0;
// findTreasure(6) 함수 호출은 함수형 표현이지만 newTreasuresFound는 문장
int newTreasuresFound = findTreasure(6);
// 오른쪽 표현식의 결과를 totalTresure의 값에 대입
totalTreasure = totalTreasure + newTreasuresFound;
// if-else문은 (totalTresure>10) 표현식의 결과에 따라 어떤 동작을 수행해야 하는지 전달.
if (totalTreasure > 10) {
// 함수 호출 이후 반환 값이 없기 때문에 출력
System.out.println("You have a lot of treasure!");
} else {
// 함수 호출 이후 반환 값이 없기 때문에 출력
System.out.println("You should look for more treasure!");
}
- 표현식과 문장의 기본적인 차이는 값의 반환 유무
함수형 프로그래밍의 개념
- 추상 함수에 기반을 두고 있으며 패러다임을 구성하는 많은 개념은 선언적 스타일로
무엇을 해결할 것인가
에 초점을 두고 있다.- 이것은
어떻게 해결할 것인가
를 고민하는 명령형 접근 방식과 상반된다
- 이것은
순수 함수와 참조 투명성
- 순수 함수와 불순 함수로 분류
- 사이드 이펙트가 발생하지 않는 표현식이나 순수 함수는 결정론적이며 참조 투명성의 특징을 가진다.
- 다른 프로그램을 작성할 때도 코드를 변경하지 않고 재사용 할 수 있따는 것을 의미.
public class PureFunctions {
// 순수 함수 예시
public static int add(int a, int b) {
return a + b;
}
// 순수 함수가 아닌 예시
private static int counter = 0;
public static int incrementCounter() {
counter += 1;
return counter;
}
public static void main(String[] args) {
System.out.println(add(1, 2)); // 3
System.out.println(incrementCounter()); // 1
System.out.println(incrementCounter()); // 2
}
}
순수 함수 만족 조건
- 동일한 입력에 대해 항상 동일한 출력 반환
- 순수 함수의 리턴 값은 함수 호출 시 입력되는 인수에 의존
- 어떠한 사이드 이펙트 없이 자기 충족적 성질을 소유
- 인수값을 변경하거나, I/O를 사용하는 등 글로벌 상태에 영향을 미치지 않는다.
불변성
- 객체 행성 후 setter를 통해 변경이 가능하지만, 이러한 가변성은 예상하지 못한 사이드 이펙트를 일으킨다.
- 불변성을 갖는 경우 초기화된 이후, 자료 구조를 더 이상 변경할 수 없다.
- 많은 프로그래밍 언어에서는 구조 공유를 사용하여 효율적인 복사 메커니즘을 제공하고 있어서 데이터 변경에 대한 비효율성을 최소화한다.
public class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public ImmutablePoint move(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
}
재귀
- 재귀는 동일한 형태의 문제를 부분적으로 해결하고 그 결과를 조합하여 최종적으로 원래의 문제를 해결하는 문제 해결 기법
- 재귀는 스스로 호출.
- 순수 함수형 프로그래밍은 종종 반복문이나 반복자 대신 재귀를 선호하는 경향이 있다.
- 반복적으로 함수를 호출하는 것은 비효율적일 수 있으며 스택 오버플로의 위험성을 갖는다.
- 많은 함수형 언어는 스택 프레임을 줄이기 위해 꼬리물기 최적화 또는 루프 언롤링과 같은 최적화 기법을 활용하지만, 자바는 이런 기술을 지원하지 않는다.
public class BinarySearchTree {
static class Node{
int key;
Node left, right;
public Node(int item) {
key = item;
left = right = null;
}
}
Node root;
BinarySearchTree(){
root = null;
}
void insert(int key) {
root = insertRec(root, key);
}
Node insertRec(Node root, int key) {
if (root == null) {
root = new Node(key);
return root;
}
if (key < root.key) {
root.left = insertRec(root.left, key);
} else if (key > root.key) {
root.right = insertRec(root.right, key);
}
return root;
}
}
일급 함수와 고차 함수
- 일급 함수는 프로그래밍 언어에서 함수를
일급 객체
로 대우하는 것을 의미.- 함수 자체를 다른 함수에 인수로 전달하거나 반환값으로 사용 가능, 변수에 할당할 수 있는 함수를 지칭.
public class HigherOrderFunctions {
public static void main(String[] args) {
// 일급 함수 예시: 람다 표현식을 사용하여 함수를 변수에 할당
Function<Integer, Integer> square = x -> x * x;
System.out.println("Square of 5 is: " + square.apply(5));
// 고차 함수 예시: 함수를 인자로 받고, 함수를 반환하는 메소드
Function<Function<Integer, Integer>, Function<Integer, Integer>> createAdder = f -> {
return y -> f.apply(y) + 10;
};
Function<Integer, Integer> addTenAndSquare = createAdder.apply(square);
System.out.println("addTenAndSquare.apply(5) : " + addTenAndSquare.apply(5));
}
}
함수 합성
- 순수 함수를 결합하여 더 복잡한 표현식을 만들어낼 수 있음.
public class FunctionComposition {
public static void main(String[] args) {
Function<Integer, Integer> multiplyByThree = x -> x * 3;
Function<Integer, Integer> addOne = x -> x + 1;
// andThen 사용: 먼저 multiplyByThree을 적용하고, 그 결과에 addOne을 적용
Function<Integer, Integer> multiplyThenAdd = multiplyByThree.andThen(addOne);
// compose 사용: 먼저 addOne을 적용하고, 그 결과를 multiplyByThree에 적용
Function<Integer, Integer> addThenMultiply = multiplyByThree.compose(addOne);
// 결과 출력
System.out.println("andThen(3 * 3 + 1): " + multiplyThenAdd.apply(3)); // 결과: 10
System.out.println("compose((3 + 1) * 3): " + addThenMultiply.apply(3)); // 결과: 12
}
}
커링
- 커링 함수는 여러 개의 인수를 받는 함수들을 분리하여 인수를 하나씩만 받는 함수의 체인으로 변환
public class CurryingExample {
public static void main(String[] args) {
// 두 인자를 받는 함수를 커링
Function<Integer, Function<Integer, Integer>> add = a -> b -> a + b;
// 커링 함수 사용: 첫 번째 인자 적용
Function<Integer, Integer> add5 = add.apply(5);
// 두 번째 인자 적용
int result = add5.apply(3);
System.out.println("5 + 3 = " + result); // 출력: 5 + 3 = 8
}
}
부분 적용 함수 애플리케이션
- 부분 적용 함수 애플리케이션은 기존 함수의 인수 중 일부만 제공하여 새로운 함수를 생성하는 과정
public class PartialApplicationExample {
public static void main(String[] args) {
// 두 인자를 받는 함수
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
// 첫 번째 인자 5를 미리 적용
Function<Integer, Integer> multiplyByFive = partialApply(multiply, 5);
// 나머지 인자 적용
System.out.println("5 * 3 = " + multiplyByFive.apply(3)); // 출력: 5 * 3 = 15
}
// 부분 적용을 구현하는 함수
static <T, U, R> Function<U, R> partialApply(BiFunction<T, U, R> biFunction, T firstArgument) {
return u -> biFunction.apply(firstArgument, u);
}
}
느긋한 계산법
- 느긋한 계산법은 표현식의 계산을 그 결과가 실제로 필요한 시점까지 지연시키는 계산 전략으로, 표현식을 어떻게 생성하는지와 해당 표현식을 언제 사용하는지에 대한 문제를 분리하는 개념.
- 자바를 포함한 많은 비함수형 언어는 주로 문법에 엄격하고, 즉시 연산을 수행하는 특징을 가지고 있다. 이러한 언어들은 여전히 if-else문이나 반복문가 같은 몇 가지 느긋한 구조를 가지고 있으며, 논리적인 단축 평가 연산자도 그 중하나.
- 느긋함은 무한한 자료 구조나 일부 알고리즘의 효율적인 구현과 같이, 다른 방식으로는 불가능한 구조들을 가능하게 한다. 또한 참조 투명성과 아주 잘 어울린다.
public class LazyEvaluationExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> numberStream = numbers.stream()
.map(n -> {
System.out.println("더블링 " + n);
return n * 2;
})
.filter(n -> n > 5);
System.out.println("스트림 생성, 필터와 매핑은 아직 수행되지 않음.");
// 여기서 첫 번째 요소를 요청
Integer first = numberStream.findFirst().orElse(null);
System.out.println("첫 번째 요소가 5보다 큽니다.: " + first);
}
}
함수형 프로그래밍의 장점
간결성
- 가변 상태에서 사이드 이펙트가 없는 경우, 함수는 보통 더 작아지며 '해야할 일'만 수행
일관성
- 불변 자료 구조는 신뢰성과 일관성이 있다.
(수학적) 정확성
- 일관된 자료 구조를 갖춘 더 간결한 코드는 자동으로 더 '정확한'코드를 만들며 버그를 발생시킬 수 있는 부분이 줄어든다.
안전한 동시성
- 동시성은 전통적인 자바에서 제대로 처리하기 가장 어려운 작업 중 하나.
- 함수형 개념을 활용함으로 다양한 문제를 해결하고, (거의) 비용 없이 안전한 병렬 처리의 이점을 누릴 수 있다.
모듈성
- 작고 독립적인 함수는 간단한 재사용성과 모듈을 제공
테스트 용이성
- 순수 함수, 참조 투명성, 불변성, 역할 분리와 같은 많은 함수형 개념은 테스트와 검증을 더 쉽게 만든다.
함수형 프로그래밍의 단점
학습 곡선
- 고급 수학 용어 개념을 기반으로 하기 때문에 다소 어렵게 느껴질 수 있음.
고수준 추상화
- 객체 지향 프로그래밍이 객체를 사용하여 추상화를 모델링하는 반면, 함수형 프로그래밍은 더 높은 추상화 수준을 사용하여 자료 구조를 표현
상태 처리
- 상태 처리는 어떤 패러다임을 선택하더라도 결고 쉬운 작업이 아님.
- 함수형 프로그래밍의 불변성 접근 방식은 많은 버그 가능성을 제거하지만, 자료 구조를 실제로 변경해야 하는 경우 상태를 변경하는 것이 어려울 수 있음.
성능 영향도
- 함수형 프로그래밍은 동시성 환경에서 사용하지 더 쉽고 안전하지만, 이는 다른 패러다임과 비교했을 때 반드시 빠르다는 것은 아님
- 불변이나 재귀와 같은 많은 함수형 기술은 오버헤드로 인해 성능이 저하될 수 있음
- 함수형 프로그래밍 언어는 복사를 최소화하는 특수화된 자료 구조나 재귀에 대해 컴파일러 최적화 같은 다양한 최적과 기법을 도입하고 있음
최적의 문제 상황
- 고성능 컴퓨팅, 입출력 중심의 문제 또는 저수준 시스템 및 임베디드 컨트롤러와 같은 도메인은 데이터 지역성과 명시적 메모리 관리와 같은 세부 사항에 민감하기 때문에 함수형 프로그래밍과는 어울리지 않을 수 있음.
반응형
'스터디 > 함수형 프로그래밍 with 자바' 카테고리의 다른 글
Chapter 06. 스트림 (Stream) (1) | 2024.06.06 |
---|---|
Chapter 05. 레코드 (Record) (0) | 2024.05.30 |
Chapter 04. 불변성 (0) | 2024.05.21 |
Chapter 03. JDK의 함수형 인터페이스 (1) | 2024.05.16 |
Chapter 02. 자바 람다 (0) | 2024.05.08 |