# 함수형 프로그래밍 ![image](https://hackmd.io/_uploads/HkXF-dlPn.png) - 함수형 프로그래밍(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` 패키지에서 이들을 확인할 수 있다. 이 중 몇 가지를 살펴보면 다음과 같다 ![image](https://hackmd.io/_uploads/ByfcrmIZR.png) ### 함수형 인터페이스 사용 예시 - 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; ```