
자바를 쓰는 프로그래머로서
자바8 부터는 함수형 프로그래밍을 일부를 지원한다고 해서 해당 기능으로 함수형 프로그래밍을 공부한다고 했을 때 모두들 말렸다 그래서
초기 함수형 프로그래밍 언어인 LISP 와 현대적인 함수형프로그래밍언어인 Scala의 함수형 프로그래밍을 공부하고 다시 자바의 관련 함수형을 보았을 때의 그 의미와 개인적인 생각을 정리한다.
// 자바의 함수형 인터페이스 방식
Function<Integer, Integer> multiply = x -> x * 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.stream().map(multiply);
// Scala의 1급 함수 방식
val multiply: Int => Int = _ * 2
List(1, 2, 3).map(multiply)
Function, Consumer, Supplier, Predicate 등의 인터페이스를 통해 함수를 객체처럼 다룰 수 있게 했지만, 이는 실제 함수형 언어의 1급 함수와는 다른 제한적인 방식이다
추가적으로 정의는 할 수 있겠지만 그 이상 사용하는 것은 오히려 코드의 복잡성만 더하게 되고 그런 상황이면 그냥 객체지향적으로 해결하는 것이 좋은상황이 많을 것이다
// Java Record
public record Person(String name, int age) {}
// 일반 클래스에서 불변성 구현 - 더 많은 코드 필요
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// getter만 제공
public String getName() { return name; }
public int getAge() { return age; }
}
// Scala case class - 기본적으로 불변
case class Person(name: String, age: Int)
// Java에서 StackOverflowError 발생 가능한 재귀
public static long factorial(long n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// 반복문으로 우회해야 함
public static long factorial(long n) {
long result = 1;
for (long i = n; i > 0; i--) {
result *= i;
}
return result;
}
// Scala의 꼬리 재귀 최적화
@tailrec
def factorial(n: Long, acc: Long = 1): Long = {
if (n <= 1) acc
else factorial(n - 1, n * acc)
}
// Optional의 모나드 법칙 위반 예시
Optional<String> opt = Optional.of("test");
// 모나드 법칙 중 왼쪽 항등 법칙:
// Optional.of(x).flatMap(f) 는 f(x)와 같아야 함
Function<String, Optional<String>> f = s -> null; // null을 반환하는 함수
// 왼쪽 연산
try {
Optional<String> left = opt.flatMap(f);
System.out.println("Left: " + left);
} catch (NullPointerException e) {
System.out.println("Left: NPE 발생");
}
// 오른쪽 연산
try {
Optional<String> right = f.apply(opt.get());
System.out.println("Right: " + right);
} catch (NullPointerException e) {
System.out.println("Right: NPE 발생");
}
// Stream의 제한적 지연 평가
Stream<Integer> numbers = Stream.of(1, 2, 3)
.map(x -> {
System.out.println("mapping " + x); // 중간 연산이 즉시 실행되지 않음
return x * 2;
});
// 하지만 재사용 불가능
numbers.forEach(System.out::println);
numbers.forEach(System.out::println); // IllegalStateException
// Scala의 LazyList는 진정한 지연 평가와 재사용 가능
val numbers = LazyList(1, 2, 3)
.map(x => {
println(s"mapping $x")
x * 2
})
numbers.foreach(println) // 재사용 가능
numbers.foreach(println) // 다시 사용 가능
실제 함수형언어의 함수형의 접근방식과 자바의함수형을 보고 느낀점은 실제 함수형프로그래밍의 모나드의 구현체중에 유용한 대표적인 구현체만의 성질을 흉내내서 만들었다고 느껴졌다
이런 제한적인 성격 안에서도 함수적 프로그래밍이 같는 장점(간결성, 일관성, 정확성, 병렬성 동시성 다루기)
은 가져갈수 있고 적용하는데 적절할지는 판단이 필요하다
예를 들어 간단한 반복문을 억지로 바꿀필요는 없다던가 동시성 처리를할때 이걸 써서 더 효율적인 처리 데이터 양인가를 가늠해야 한다.
굳이 억지로 바꿀필요는 없다
자바 자체에 없던 형식을 함수형 언어에서는 불변성, 일급함수등을 바로 지원하기 때문에 더 강력하고 추가적인 코드 필요 작성 없이 가능하다
자바에서 만약에 추가적인 조합기를 추가하거나,합성관련을 구현하라고하면 복잡해진다
오히려 코드를 작성하는 수고가 더 들고 코드가 더 알아보기 어렵게 된다
자바에서 만약에 함수형을 사용한다고 하면 기존에 지원하는 기본적 조합기 이외에 무언가를 해야한다고하면 지양하는 것이 좋은 것 같다
현재 상황에서는 실제 자바에서 함수형을 사용하는 이것을 적용하는 경우 이득이 있는가를 판단하는 능력이 중요할 것 같다
"함수형언어의 함수형 프로그래밍이 새라면 자바의 함수형은 하늘다람쥐이다"
태생이 날기 위해 태어난 언어와 그렇지 않은데 조금 날수도 있도록 진화된 상황이라 다르다
접근방식을 다르게 보아 이런 함수형언어의 일부 장점을 취한 정도이기 때문에 적절하게 써야될 상황에 대한 판단이 심히 요구된다!
3줄 요약