Iterating with Steps

튜토리얼 시리즈의 이전 글에서는 명령형으로 작성된 간단한 루프를 함수형으로 변환하는 방법을 살펴봤습니다. 이번 글에서는 좀 더 복잡한 루프, 즉 일정 간격으로 몇 가지 값을 반복해야 하는 루프를 처리하는 방법을 살펴보겠습니다.

한 번에 하나씩 값의 범위를 반복할 때는 함수형으로 구현하는 데 유용한 IntStreamrange() 메서드가 유용했습니다. 이 메서드는 지정된 범위 내의 값에 대해 한 번에 하나의 값을 생성하는 스트림을 반환합니다. 언뜻 생각하면 일부 값을 건너뛰기 위해 스트림에 filter() 메서드를 사용하고 싶을 수 있습니다. 하지만 더 간단한 해결책이 있는데, 바로 IntStreamiterate() 메서드입니다.

 

명령형에서 함수형 스타일로 전환하기

다음은 step을 사용하여 원하는 범위에서 몇 개의 값을 건너뛰는 루프입니다:

for(int i = 0; i < 15; i = i + 3) {
  System.out.println(i);
}

인덱스 변수 i의 값은 0에서 시작하여 반복이 진행됨에 따라 3씩 증가합니다. 이와 같이 범위의 모든 값을 반복하지 않고 일부 값을 건너뛰는 루프를 살펴보고 있다면 IntStreamiterate() 메서드를 사용하는 것을 고려해 보세요.

코드를 리팩터링하기 전에 이전 코드의 for() 루프를 자세히 살펴보기 전에 람다의 잠재적 용도를 살펴볼 수 있는 가상의 안경을 쓰고 살펴봅시다.

//imaginary code
for(int i = 0; i < 15; i = i + 3) //imperative
for(seed, i -> i < 15, i -> i + 3) //functional

for 루프에 전달되는 첫 번째 인수는 반복의 시작 값 또는 시드이며 그대로 유지될 수 있습니다. 두 번째 인수는 인덱스 변수 i의 값이 15의 값을 초과해서는 안 된다는 것을 알려주는 predicate입니다. 함수형 스타일에서는 이를 IntPredicate로 대체할 수 있습니다. 세 번째 인자는 인덱스 변수의 값을 증가시키는 인자로, 함수형 스타일에서는 단순히 IntUnaryOperator입니다. IntStream 인터페이스에는 가상의 코드를 멋지게 표현하는 iterate()라는 static 메서드가 있습니다: iterate(int seed, IntPredicate hasNext, IntUnaryOperator next)입니다.

Let’s refactor the loop to use functional style.

import java.util.stream.IntStream;
 
...
IntStream.iterate(0, i -> i < 15, i -> i + 3)
  .forEach(System.out::println);

그것은 매우 간단합니다. ;,로 바뀌었고, 두 개의 람다를 사용했습니다: 하나는 IntPredicate에, 다른 하나는 IntUnaryOperator에 사용했습니다.

값을 넘어가는 것 외에도 우리는 종종 무한 루프를 사용하기 때문에 조금 더 복잡해지지만, 다음에 살펴보겠지만 Java의 함수형 API로 처리할 수 없는 것은 없습니다.

 

무한 반복 with a break

다음 명령형 루프를 살펴봅시다. 이 명령형 루프는 단계가 무한하지 않고 break 문을 사용합니다.

for(int i = 0;; i = i + 3) {
  if(i > 20) {
    break;
  }
 
  System.out.println(i);
}

종료 조건인 i < 15가 사라지고 반복되는 ;;로 알 수 있듯이 루프는 무한대입니다. 그러나 루프 내에는 i 값이 20보다 크면 반복을 종료하는 break 문이 있습니다.

함수형 스타일의 경우 iterate() 메서드 호출에서 두 번째 인수인 IntPredicate를 제거할 수 있지만 이렇게 하면 반복이 무한 스트림으로 바뀌게 됩니다. 명령형 break에 해당하는 함수형 프로그래밍은 takeWhile() 메서드입니다. 이 메서드는 내부 이터레이터인 스트림에 전달된 IntPredicatefalse로 평가되면 내부 이터레이터를 종료합니다. 이전의 명령형 언바인딩 forbreak를 함수형으로 리팩터링해 보겠습니다.

IntStream.iterate(0, i -> i + 3)
  .takeWhile(i -> i <= 20)
  .forEach(System.out::println);

iterate() 메서드는 과부하가 걸리며, IntPredicate가 있는 버전과 없는 버전의 두 가지 버전이 있습니다. 저희는 predicate가 없는 버전을 사용하여 시드 또는 시작 값에서 값을 생성하는 무한 스트림을 만들었습니다. 두 번째 인수로 전달된 IntUnaryOperator에 따라 단계가 결정됩니다. 따라서 주어진 코드 예제에서 스트림은 0, 3, 6 등의 값을 생성합니다. 인덱스가 20 값을 초과하지 않도록 반복을 제한하고 싶기 때문에 takeWhile()을 사용합니다. takeWhile()에 전달된 predicate는 주어진 파라미터 값인 인덱스 i20의 값을 초과하지 않는 한 반복을 계속할 수 있음을 알려줍니다.

이전 글에서 range()rangeClosed()가 단순한 for 루프를 직접 대체할 수 있다는 것을 보았습니다. 루프가 조금 더 복잡해지더라도 걱정하지 마세요. IntStreamiterate() 메서드를 사용할 수 있고 break를 사용하여 루프가 종료된 경우 선택적으로 takeWhile()을 사용할 수 있습니다.

 

매핑하기

step이 있는 for 루프가 표시되는 모든 곳에서 세 개의 인수, 시드 또는 시작 값, 종료 조건에 대한 IntPredicate, 단계에 대한 IntUnaryOperator를 포함하는 iterate() 메서드를 사용합니다. 루프가 break 문을 사용하는 경우 iterate() 메서드 호출에서 IntPredicate를 삭제하고 대신 takeWhile() 메서드를 사용합니다. takeWhile()은 명령형 break의 함수형에 해당합니다.