함수형 프로그래밍 기법

김종준·2023년 3월 9일
0

모던자바

목록 보기
15/15

함수형 프로그래밍 기법

함수는 모든 곳에 존재한다.

함수형 프로그래밍이란 함수나 메서드 수학의 함수처럼 동작함을, 즉 부작용 없이 동작함을 의미했다.

함수형 언어 프로그래머는 함수형 프로그래밍이라는 용어를 좀 더 폭넓게 사용한다.

즉, 함수를 마치 일반값처럼 사용해서 인수로 전달하거나, 결과로 반환받거나, 자료구조에 저장할 수 있음을 의미한다.

일반값처럼 취급할 수 있는 함수를 일급함수라고 한다.

자바 8이 이전 버전과 구별되는 특징 중 하나가 일급 함수를 지원한다는 점이다.

자바 8에서는 :: 연산자로 메서드 참조를 만들거나 람다 표현식으로 직접 함수값을 표현해서 메서드 함수값으로 사용할 수 있다.

고차원 함수

함수형 프로그래밍 커뮤니티에 따르면 다음 중 하나 이상의 동작을 수행하는 함수를 고차원함수라 부른다.

  • 하나 이상의 함수를 인수로 받음
  • 함수를 결과로 반환

커링

온도 변환, 통화 변환, 단위 변환과 같이 변환하는 메서드를 만들때 따로 만드는 방법도 있지만 로직을 재활용하지 못한다는 단점이 있다.

기존 로직을 활용해서 변환기를 특정 상황에 적용할 수 있는 방법이 있다.

다음은 커링이라는 개념을 활용해서 한 개의 인수를 갖는 변환 함수를 생산하는 팩토리를 정의하는 코드다.

static DoubleUnaryOperator curriedConverter(double f, double b) {
  return (double x) -> x * f + b;
}

f 와 b만 적절한 값으로 넘겨주면 우리가 원하는 작업을 수행할 함수가 반환된다.

아래는 이를 활용한 변환기이다.

DoubleUnaryOperator convertCtoF = curriedConverter(9.0/5, 32);
DoubleUnaryOperator convertUSDtoGBP = curriedConverter(0.6, 0);
DoubleUnaryOperator convertKmtoMi = curriedConverter(0.6214, 0);

// 사용 예시
double gbp = convertUSDtoGBP.applyAsDouble(100);

영속 자료구조

함수형 프로그램에서는 함수형 자료구조, 불변 자료구조 등의 용어로 사용하지만 보통은 영속 자료구조라고 부른다.

함수형 메서드에서 전역 자료구조나 인수로 전달된 구조를 갱신할 수 없다.

자료구조를 바꾼다면 같은 메서드를 두 번 호출했을 때 결과가 달라지면서 참조 투명성에 위배되고 인수를 결과로 단순하게 매핑할 수 있는 능력이 상실되기 때문이다.

파괴적인 갱신과 함수형

class TrainJourney {
  public int price;
  public TrainJourney onward;
  public TranJourney(int p, TrainJourney t) {
    price = p;
    onward = t;
  }
}
static TrainJourney link(TrainJourney a, TrainJourney b) {
  if(a == null) return b;
  TranJourney t = a;
  while(t.onward != null) {
    t = t.onward;
  }
  t.onward = b;
  return a;
}

X에서 Y로(firstJourney) 그리고 Y에서 Z로(secondJourney) 여행을 나타내는 별동의 TrainJourney 객체가 있다고 가정하자.

그리고 이를 link라는 메서드를 호출하여 묶는다면 위의 코드 상으로는 firstJourney가 secondJourney를 포함하면서 파괴적인 갱신이 일어나 X에서 Y로의 여정이 아니라 X에서 Z로의 여정이 되어버린다.

함수형에서는 이 같은 부작용을 수반하는 메서드를 제한하는 방식으로 문제를 해결한다.

계산 결과를 표현할 자료구조가 필요하면 기존 자료구조를 갱신하지 않도록 새로운 자료구조를 만들어야 한다.

이는 표준 객체지향 프로그래밍의 관점에서도 좋은 방법이다.

따라서 깔끔한 함수형 해결 방법을 사용하는 것이 좋다.

static TrainJourney append(TrainJourney a, TrainJourney b) {
  return a == null ? b : new TrainJourney(a.price, append(a.onward, b));
}

위의 코드는 명확하게 함수형이며 기존 자료구조를 변경하지 않는다.

하지만 TrainJourney 전체를 새로 만들지 않는다.

위와 같은 함수형 자료를 영속이라고 하며 따라서 프로그래머는 인수로 전달된 자료구조를 변화시키지 않아야한다.

즉 "결과 자료구조를 바꾸지 말라"는 것이 자료구조를 사용하는 모든 사용자에게 요구하는 단 한가지 조건이다.

만약 이를 무시한다면 전달된 자료구조에 의도치 않은 그리고 원치 않는 갱신이 일어난다.

게으른 리스트 만들기

자바 8의 스트림은 요청할 때만 값을 생성하는 블랙박스와 같다.

스트림에 일련의 연산을 적용하면 연산이 수행되지 않고 일단 저장된다.

스트림에 최종 연산을 적용해서 실제 계산을 해야 하는 상황에서만 실제 연산이 이루어 진다.

특히 스트림에 여러 연산을 적용할 때 이와 같은 특성을 활용할 수 있다.

게으른 특성 때문에 각 연산별로 스트림을 탐색할 필요 없이 한 번에 여러 연산을 처리할 수 있다.

기본적인 게으른 리스트

조금 더 일반적인 스트림의 형태인 게으른 리스트의 개념을 알아보자.

Supplier<T>를 이용해서 게으른 리스트를 만들면 꼬리가 모두 메모리에 존재하지 않게 할 수 있다.

Supplier<T>로 리스트의 다음 노드를 생성할 것이다.

Supplier의 get 메서드를 호출하면 LazyList의 노드가 만들어진다.

class LazyList<T> implements MyList<T> {
  final T head;
  final Supplier<MyList<T>> tail;
  public LazyList(T head, Supplier<MyList<T>> tail) {
    this.head = head;
    this.tail = tail;
  }
  
  public T head() {
    return head;
  }
  
  public MyList<T> tail() {
    return tail.get();
  }
  
  public boolean isEmpty() {
    return false;
  }
}

그럼 아래와 같이 Supplier를 전달하는 방식으로 n으로 시작하는 무한히 게으른 리스트를 만들 수 있다.

public static LazyList<Integer> from(int n) {
  return new LazyList<Integer>(n, () -> from(n+1));
}

// 사용
LazyList<Integer> numbers = from(2);
int two = numbers.head();
int three = numbers.tail().head();
int four = numbers.tail().tail().head();

이를 활용하면 아래와 같이 소수를 생성하는 게으른 리스트를 생성할 수 있다.

public static MyList<Integer> primes(MyList<Integer> numbers) {
  return new LazyList<>(
  	numbers.head(),
    () -> primes(
    	numbers.tail()
      				.filter(n -> n % numbers.head() != 0)
    )
  );
}

public MyList<T> filter(Predicate<T> p) {
  return isEmpty() ?
    			this :
  				p.test(head()) ?
            new LazyList<>(head(), () -> tail.filter(p)) :
  					tail().filter(p);
}

이렇게 자바 8 덕분에 함수를 자료구조 내부에 추가할 수 있다는 사실을 알았고 이런 함수는 자료구조를 만드는 시점이 아니라 요청 시점에 실행된다는 사실도 확인했다.

0개의 댓글