# 함수형 프로그래밍

- 함수형 프로그래밍(Functional Programming, FP)은 컴퓨터 프로그래밍의 패러다임 중 하나
- 함수의 평가를 프로그래밍의 주요 방법으로 사용하는 접근 방식을 가르킨다.
- 수학적 함수의 개념을 프로그래밍에 적용하여 부작용(Side Effects)을 최소화하고 높은 수준의 추상화를 제공한다.
함수형 프로그래밍의 주요 특징은 다음과 같다
- **불변성(Immutability)**: 함수형 프로그래밍에서는 데이터가 한 번 생성된 후에는 그 상태가 변하지 않는다. 이는 복잡성을 줄이고 버그를 예방하는 데 도움이 된다.
- **순수 함수(Pure Functions)**: 순수 함수는 같은 입력이 주어지면 항상 같은 출력을 반환하며, 프로그램의 상태를 변경하거나 부작용을 유발하지 않는다.
- **고차 함수(Higher-order Functions)**: 함수형 프로그래밍에서는 함수를 다른 함수의 인수로 전달하거나, 함수에서 함수를 반환할 수 있다다.
- **재귀(Recursion)**: 함수형 프로그래밍 언어는 상태 변경을 피하려고 루프(Loop) 사용을 최소화하고, 대신 재귀를 사용하여 반복적인 작업을 수행한다.
- **형식 추론(Type Inference)**: 많은 함수형 프로그래밍 언어는 형식을 명시적으로 선언하지 않아도 프로그램의 형식을 추론할 수 있다.
- **지연 평가(Lazy Evaluation)**: 함수형 프로그래밍 언어는 필요할 때까지 계산을 미루는 지연 평가를 지원하는 경우가 많다. 이렇게 하면 무한 시퀀스를 모델링하거나, 효율성을 높이는 데 도움이 될 수 있다.
함수형 프로그래밍은 복잡성을 관리하고, 가독성과 유지 보수성을 향상시키며, 동시성과 병렬성을 처리하기 위한 효율적인 도구로 간주된다.
그러나 이러한 이점을 최대화하려면 함수형 프로그래밍의 원칙과 패턴을 정확히 이해하고 적용해야 한다.
## 자바 함수형 인터페이스
- 함수형 인터페이스는 자바에서 함수형 프로그래밍을 지원하기 위해 도입된 개념.
- 자바는 원래 객체 지향 프로그래밍(OOP) 언어로, 모든 동작은 객체와 그 객체의 메서드로 이루어진다는 기본 원칙을 따르고 있지만, Java 8부터 함수형 프로그래밍의 개념이 도입되면서 "함수형 인터페이스(Functional Interface)"라는 개념이 등장했다.
- 자바의 함수형 인터페이스는 함수형 프로그래밍의 기반을 제공하며, 함수형 프로그래밍의 원칙과 개념을 자바로 구현할 수 있도록 도와준다.
- 이를 통해 자바 개발자들도 함수형 프로그래밍의 이점을 활용하여 좀 더 유연하고 효율적인 코드를 작성할 수 있게 되었다.
### 함수형 인터페이스의 이해
- 함수형 인터페이스는 '정확히 하나의 추상 메서드를 가진 인터페이스'를 의미한다. 물론, `default` 메서드나 `static` 메서드는 여러 개 있을 수 있다.
- 이러한 함수형 인터페이스는 람다 표현식을 위한 타입으로 사용되며, 이를 통해 메서드를 직접 참조하거나 전달할 수 있게 된다.
- 함수형 인터페이스는 `@FunctionalInterface` 어노테이션으로 명시할 수 있다.
- 이 어노테이션은 인터페이스가 함수형 인터페이스의 규칙을 따르는지 컴파일러에게 확인하도록 요청하는 역할을 한다.
```java
//구현해야 할 메소드가 한개이므로 Functional Interface이다.
@FunctionalInterface
public interface Math {
public int Calc(int first, int second);
}
//구현해야 할 메소드가 두개이므로 Functional Interface가 아니다. (오류 사항)
@FunctionalInterface
public interface Math {
public int Calc(int first, int second);
public int Calc2(int first, int second);
}
```
### 함수형 인터페이스의 예
자바는 여러 가지 내장 함수형 인터페이스를 제공하고 있다. `java.util.function` 패키지에서 이들을 확인할 수 있다.
이 중 몇 가지를 살펴보면 다음과 같다

### 함수형 인터페이스 사용 예시
- Predicate 인터페이스 예시
```java
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isEven = new Predicate<Integer>() {
@Override
public boolean test(Integer n) {
return n % 2 == 0;
}
};
System.out.println(isEven.test(4)); // 출력: true
System.out.println(isEven.test(5)); // 출력: false
}
}
```
- Function 인터페이스 예시
```java
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<Integer, String> intToString = new Function<Integer, String>() {
@Override
public String apply(Integer n) {
return Integer.toString(n);
}
};
System.out.println(intToString.apply(12345)); // 출력: "12345"
}
}
```
- Supplier 인터페이스 예시
```java
import java.util.function.Supplier;
public class Main {
public static void main(String[] args) {
Supplier<Double> randomSupplier = new Supplier<Double>() {
@Override
public Double get() {
return Math.random();
}
};
System.out.println(randomSupplier.get()); // 임의의 랜덤값 출력
}
}
```
- Consumer 인터페이스 예시
```java
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Consumer<String> print = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
print.accept("Hello, world!"); // 출력: "Hello, world!"
}
}
```
## 자바에서의 람다
- 람다 표현식(Lambda expression)은 익명 함수(이름이 없는 함수)를 정의하는 방법이다.
- 람다 표현식은 Java 8에서 처음 도입된 기능으로, 간결하게 함수를 표현하는 방법이다.
- 람다는 프로그램 코드에서 한 번이나 두 번 사용되고 버려질 수 있는 작은 코드 조각을 간단하게 표현하기 위해 주로 사용된다.
- 람다 표현식 사용 시, 익명 클래스를 사용하는 것보다 코드가 간결해지기 때문에 함수형 프로그래밍을 할 때 많이 사용된다.
- 함수형 인터페이스와 결합하여 사용되는 경우가 많으며, 이를 통해 더욱 간결하고 직관적인 코드 작성이 가능하다.
람다 표현식의 기본 구조는 다음과 같다
```java
(parameter) -> { body }
```
- `parameters`: 함수가 받는 인자들의 목록. 이 부분은 함수가 인자를 받지 않는 경우 생략될 수 있다.
- `->`: 람다 표현식의 인자 목록과 본문을 분리하는 토큰.
- `body`: 함수의 본문으로, 실행될 코드 블록을 의미한다.
### 람다와 함수형 인터페이스의 연관성
- 함수형 인터페이스의 인스턴스를 생성하는 방법 중 하나
- 함수형 인터페이스의 유일한 추상 메서드와 람다 표현식이 실행할 코드 사이에는 일대일 대응 관계가 있다.
예를 들어, 다음과 같은 함수형 인터페이스가 있다고 가정해보자.
```java
@FunctionalInterface
public interface SimpleFunctionalInterface {
void doWork();
}
```
이 함수형 인터페이스의 인스턴스를 람다 표현식을 사용하여 생성할 수 있다.
```java
SimpleFunctionalInterface sfi = () -> System.out.println("Doing work...");
```
이렇게 람다 표현식을 사용하면, 구현해야 하는 메서드의 시그니처를 이미 알고 있는 함수형 인터페이스에 대해 훨씬 간결하게 코드를 작성할 수 있다.
### 람다의 표현식
- 매개변수 화살표 `->` 함수몸체로 이용하여 사용할 수 있다
- 매개변수가 하나일 경우 매개변수 생략 가능하다
- 함수몸체가 단일 실행문이면 괄호 `{}`생략 가능하다
- 함수몸체가 return문으로만 구성되어있는 경우 괄호 `{}` 생략 가능하다
#### 람다의 표현식 예시
- `() -> {}`: 아무런 파라미터를 받지 않고 아무런 동작도 하지 않는 람다 표현식.
- `() -> 1`: 아무런 파라미터를 받지 않고 항상 1을 반환하는 람다 표현식.
- `() -> { return 1; }`: 위와 동일하지만 명시적으로 return 키워드를 사용하고 있다.
- `(int x) -> x+1`: 정수 x를 입력으로 받아 x+1을 반환하는 람다 표현식.
- `(x) -> x+1`, `x -> x+1`: 위와 동일하지만 타입을 명시하지 않았다. 자바의 타입 추론 기능 덕분에 가능하다.
- `(int x) -> { return x+1; }`, `x -> { return x+1; }`: 위와 동일하지만 중괄호와 return 키워드를 사용하고 있다.
- `(int x, int y) -> x+y`, `(x, y) -> x+y`, `(x, y) -> { return x+y; }`: 두 개의 정수를 입력으로 받아 두 수의 합을 반환하는 람다 표현식.
- `(String lam) -> lam.length()`, `lam -> lam.length()`: 문자열을 입력으로 받아 문자열의 길이를 반환하는 람다 표현식.
- `(Thread lamT) -> { lamT.start(); }`, `lamT -> { lamT.start(); }`: Thread 객체를 입력으로 받아 스레드를 시작하는 람다 표현식.
> 잘못된 람다의 표현식
> `(x, int y) -> x+y` : 람다 표현식에서는 모든 파라미터의 타입을 명시하거나, 모든 파라미터의 타입을 생략해야 한다.
> `(x, final y) -> x+y` : 람다 파라미터는 기본적으로 final이므로 final 키워드를 사용할 수 없다.
### 람다의 특징
- 익명 함수: 람다식은 익명 함수로써 이름이 없고, 함수 자체가 값으로 취급된다. 이를 통해 필요한 곳에서 즉석으로 함수를 정의하고 사용할 수 있다.
- 함수형 인터페이스와 함께 사용: 람다식은 주로 함수형 인터페이스를 구현하기 위해 사용된다. 함수형 인터페이스는 하나의 추상 메서드를 가진 인터페이스로, 람다식을 통해 해당 추상 메서드를 구현할 수 있다.
- 클로저: 람다식은 클로저(closure) 개념을 지원한다. 클로저는 함수 내부에서 외부 변수에 접근할 수 있는 개념으로, 람다식을 포함하는 범위 내의 변수에 접근할 수 있다. 이를 통해 함수 내에서 외부 변수를 사용하면서 불변성과 상태 변경을 조절할 수 있다.
### 람다의 장단점
#### 장점
- 간결성과 가독성: 람다식은 코드의 길이를 줄여주고 가독성을 향상시켜 준다. 필요한 내용에 집중할 수 있으며, 불필요한 부분을 제거하여 코드를 간결하게 작성할 수 있다.
- 함수형 프로그래밍 지원: 람다식은 함수형 프로그래밍 패러다임을 지원한다. 함수를 값으로 다루는 것이 가능하며, 불변성과 순수성을 강조하는 함수형 프로그래밍의 특징을 구현할 수 있다.
- 유연한 사용: 람다식을 변수에 할당하거나 매개변수로 전달하는 등의 유연한 사용이 가능하다. 이를 통해 코드의 재사용성과 확장성을 높일 수 있다.
- 병렬 처리와 비동기 프로그래밍: 람다식은 병렬 처리와 비동기 프로그래밍에 유용하게 사용될 수 있다. 병렬 스트림, CompletableFuture 등과 함께 사용하여 병렬 처리 작업을 간편하게 구현할 수 있다.
#### 단점
- 학습 곡선: 람다식의 문법과 개념은 초기에는 생소할 수 있다. 함수형 프로그래밍 패러다임에 익숙하지 않은 개발자들은 학습 곡선을 거쳐야 할 수 있다.
- 제약사항: 람다식은 함수형 인터페이스에만 사용될 수 있다. 따라서, 이미 정의된 인터페이스에 람다식을 사용하기 위해서는 해당 인터페이스가 함수형 인터페이스여야 한다.
- 디버깅의 어려움: 람다식은 익명 함수로서 이름이 없기 때문에 디버깅 시점에서 추적이 어려울 수 있다. 따라서, 디버깅에 어려움을 겪을 수 있다.
- 성능 오버헤드: 람다식을 사용하면 메모리 사용과 실행 시간 측면에서 약간의 오버헤드가 발생할 수 있다. 하지만 대부분의 상황에서는 미미한 차이로 인해 실제로는 큰 문제가 되지 않는다.
### 람다 식 사용 예시
- Predicate 인터페이스 예시
```java
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // 출력: true
System.out.println(isEven.test(5)); // 출력: false
}
}
```
- Function 인터페이스 예시
```java
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<Integer, String> intToString = n -> Integer.toString(n);
System.out.println(intToString.apply(12345)); // 출력: "12345"
}
}
```
- Supplier 인터페이스 예시
```java
import java.util.function.Supplier;
public class Main {
public static void main(String[] args) {
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // 임의의 랜덤값 출력
}
}
```
- Consumer 인터페이스 예시
```java
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Consumer<String> print = s -> System.out.println(s);
print.accept("Hello, world!"); // 출력: "Hello, world!"
}
}
```
## 메서드 참조 (Method Reference)
- Java 8부터 도입 된 람다 표현식(Lambda Expression)을 좀 더 간결하게 표현할 수 있도록 해주는 문법
- 메서드 참조는 특정 메서드만을 호출하는 람다 표현식을 단순화하는 방법으로, 4가지 주요 형식이 있다
### 정적 메서드 참조
`클래스명::메서드명` 형식을 사용한다. 예를 들어, Integer 클래스의 parseInt 메서드를 참조하려면 `Integer::parseInt`라고 작성할 수 있다.
```java
Function<String, Integer> f1 = s -> Integer.parseInt(s);
Function<String, Integer> f2 = Integer::parseInt;
```
### 특정 객체의 인스턴스 메서드 참조
`객체변수명::메서드명` 형식을 사용한다. 예를 들어, 특정 String 객체 str의 length 메서드를 참조하려면 `str::length`라고 작성할 수 있다.
```java
String str = "Hello";
Supplier<Integer> s1 = () -> str.length();
Supplier<Integer> s2 = str::length;
```
### 임의 객체의 인스턴스 메서드 참조
`클래스명::메서드명` 형식을 사용하지만, 이 메서드는 인스턴스 메서드이다. 예를 들어, String 클래스의 length 메서드를 참조하려면 `String::length`라고 작성할 수 있다.
```java
Function<String, Integer> f1 = s -> s.length();
Function<String, Integer> f2 = String::length;
```
### 생성자 참조
`클래스명::new` 형식을 사용한다. 예를 들어, ArrayList의 생성자를 참조하려면 `ArrayList::new`라고 작성할 수 있다.
```java
Supplier<List<String>> s1 = () -> new ArrayList<>();
Supplier<List<String>> s2 = ArrayList::new;
```