접기 초기에 중단


88

접기를 일찍 종료하는 가장 좋은 방법은 무엇입니까? 단순화 된 예를 들어,의 숫자를 합산하고 Iterable싶지만 예상치 못한 (홀수라고 말하면) 무언가를 만나면 종료하고 싶을 수 있습니다. 이것은 첫 번째 근사치입니다.

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  nums.foldLeft (Some(0): Option[Int]) {
    case (Some(s), n) if n % 2 == 0 => Some(s + n)
    case _ => None
  }
}

그러나이 솔루션은 매우 추하고 (.foreach 및 return을 수행하면 훨씬 깨끗하고 명확합니다) 최악의 경우 짝수가 아닌 숫자를 만나더라도 전체 반복 가능을 순회합니다. .

그렇다면 이와 같이 폴드를 작성하는 가장 좋은 방법은 무엇입니까? 그냥 가서 이것을 재귀 적으로 써야합니까, 아니면 더 받아 들여지는 방법이 있습니까?


종료하고 중간 답변을 기록 하시겠습니까?
브라이언 애그뉴

이 경우에는 아닙니다. 그러나 약간 더 일반적인 경우에 오류 또는 무언가가있는 Either를 반환하고 싶을 수 있습니다
Heptic


루프에서 벗어나는 것에 대한이 답변도 유용 할 수 있습니다. stackoverflow.com/a/2742941/1307721
ejoubaud

답변:


64

내 첫 번째 선택은 일반적으로 재귀를 사용하는 것입니다. 다소 덜 컴팩트하고 잠재적으로 더 빠르며 (확실히 더 느리지 않음) 조기 종료시 로직을 더 명확하게 만들 수 있습니다. 이 경우 약간 어색한 중첩 정의가 필요합니다.

def sumEvenNumbers(nums: Iterable[Int]) = {
  def sumEven(it: Iterator[Int], n: Int): Option[Int] = {
    if (it.hasNext) {
      val x = it.next
      if ((x % 2) == 0) sumEven(it, n+x) else None
    }
    else Some(n)
  }
  sumEven(nums.iterator, 0)
}

두 번째 선택은를 사용 return하는 것입니다. 다른 모든 것을 그대로 유지하고 접기 만하여 def반환 할 항목을 가지기 만하면됩니다.이 경우에는 이미 메서드가 있습니다.

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  Some(nums.foldLeft(0){ (n,x) =>
    if ((n % 2) != 0) return None
    n+x
  })
}

이 특별한 경우에는 재귀보다 훨씬 더 간결합니다 (반복 가능 / 반복 변환을 수행해야했기 때문에 재귀에 특히 불행 해졌지만). 비정상적인 제어 흐름은 다른 모든 것이 동일 할 때 피해야하는 것이지만 여기서는 그렇지 않습니다. 귀중한 경우 사용해도 해가 없습니다.

이 작업을 자주 수행하고 어딘가에서 메서드 중간에 원하는 경우 (반환 만 사용할 수는 없음) 예외 처리를 사용하여 로컬이 아닌 제어 흐름을 생성 할 수 있습니다. 즉, 결국 그것이 좋은 점이며 오류 처리가 유용한 유일한 시간은 아닙니다. 유일한 트릭은 스택 트레이스 (정말 느린) 생성을 피하는 것입니다. 트레이 트 NoStackTrace와 그 자식 트레이 트가 ControlThrowable이미 그렇게 하기 때문에 쉽습니다 . Scala는 이미 이것을 내부적으로 사용하고 있습니다 (사실, 이것이 폴드 내부에서 리턴을 구현하는 방법입니다!). 우리 자신을 만들어 봅시다 (중첩 될 수는 없지만 수정할 수 있습니다) :

import scala.util.control.ControlThrowable
case class Returned[A](value: A) extends ControlThrowable {}
def shortcut[A](a: => A) = try { a } catch { case Returned(v) => v }

def sumEvenNumbers(nums: Iterable[Int]) = shortcut{
  Option(nums.foldLeft(0){ (n,x) =>
    if ((x % 2) != 0) throw Returned(None)
    n+x
  })
}

물론 여기서 사용하는 return것이 더 좋지만 shortcut전체 메서드를 래핑하는 것이 아니라 어디에나 넣을 수 있습니다.

다음으로 폴드를 다시 구현하여 (나 자신이나이를 수행하는 라이브러리를 찾아서) 조기 종료 신호를 보낼 수 있습니다. 이를 수행하는 두 가지 자연스러운 방법은 값을 전파하지 않고 값을 Option포함하는 것입니다. 여기서 None종료를 의미합니다. 또는 완료를 알리는 두 번째 표시기 기능을 사용합니다. Kim Stebel이 보여준 Scalaz lazy fold는 이미 첫 번째 경우를 다루고 있으므로 두 번째 사례를 보여 드리겠습니다 (변경 가능한 구현 포함).

def foldOrFail[A,B](it: Iterable[A])(zero: B)(fail: A => Boolean)(f: (B,A) => B): Option[B] = {
  val ii = it.iterator
  var b = zero
  while (ii.hasNext) {
    val x = ii.next
    if (fail(x)) return None
    b = f(b,x)
  }
  Some(b)
}

def sumEvenNumbers(nums: Iterable[Int]) = foldOrFail(nums)(0)(_ % 2 != 0)(_ + _)

(재귀, 반환, 게으름 등으로 종료를 구현할지 여부는 귀하에게 달려 있습니다.)

나는 그것이 주요 합리적인 변형을 포함한다고 생각합니다. 다른 옵션도 있지만이 경우 왜 사용하는지 모르겠습니다. ( Iterator가있는 경우 자체적으로 잘 작동 findOrPrevious하지만 그렇지 않으며 손으로 수행하는 데 필요한 추가 작업으로 인해 여기서 사용하는 것은 어리석은 옵션입니다.)


foldOrFail질문에 대해 생각할 때 제가 생각해 낸 것입니다. 모두 멋지게 캡슐화되었을 때 구현 IMO에서 변경 가능한 반복기와 while 루프를 사용하지 않을 이유가 없습니다. iterator재귀와 함께 사용하는 것은 의미가 없습니다.
0__

@Rex Kerr, 귀하의 답변에 감사드립니다. Either를 사용하는 내 자신의 사용을 위해 버전을 조정했습니다 ... (나는 답변으로 게시 할 것입니다)
Core

아마도 수익 기반 솔루션 의 단점 중 하나는 적용되는 기능을 인식하는 데 시간이 걸린다는 것입니다. sumEvenNumbers또는 폴드의op
이반 Balashov

1
@IvanBalashov-음, Scala의 규칙이 무엇인지 배우는 데 한 시간이 걸리지 만 return(즉, 발견 한 가장 안쪽의 명시 적 메서드에서 반환 됨) 그 후에는 그리 오래 걸리지 않습니다. 규칙은 매우 명확 def하며 둘러싸는 방법이 어디에 있는지 알려줍니다.
Rex Kerr

1
나는 당신의 foldOrFail을 좋아하지만 개인적으로 반환 유형을 B만들지 않았을 것입니다. Option[B]왜냐하면 반환 유형이 0 누산기 유형과 동일한 fold처럼 동작하기 때문입니다. 단순히 모든 옵션 반환을 b로 바꿉니다. 0으로 None의 pas. 결국 질문은 실패하기보다는 일찍 끝날 수있는 접기를 원했습니다.
Karl

26

설명하는 시나리오 (원치 않는 조건에서 종료)는 takeWhile방법에 대한 좋은 사용 사례처럼 보입니다 . 본질적 filter으로이지만 조건을 충족하지 않는 요소를 만나면 종료되어야합니다.

예를 들면 :

val list = List(2,4,6,8,6,4,2,5,3,2)
list.takeWhile(_ % 2 == 0) //result is List(2,4,6,8,6,4,2)

이것은 Iterators / Iterables에서도 잘 작동 합니다. "짝수 합계이지만 홀수로 나누기"에 대해 제안하는 해결책은 다음과 같습니다.

list.iterator.takeWhile(_ % 2 == 0).foldLeft(...)

홀수에 도달하면 시간을 낭비하지 않는다는 것을 증명하기 위해 ...

scala> val list = List(2,4,5,6,8)
list: List[Int] = List(2, 4, 5, 6, 8)

scala> def condition(i: Int) = {
     |   println("processing " + i)
     |   i % 2 == 0
     | }
condition: (i: Int)Boolean

scala> list.iterator.takeWhile(condition _).sum
processing 2
processing 4
processing 5
res4: Int = 6

이것이 바로 제가 찾던 단순함이었습니다. 감사합니다!
Tanner

14

scalaz에서 lazy 버전의 foldRight를 사용하여 기능적인 스타일로 원하는 것을 할 수 있습니다. 자세한 설명은 이 블로그 게시물을 참조하십시오 . 이 솔루션은 사용하는 동안 Stream, 당신은 변환 할 수 있습니다 IterableStream효율적으로 iterable.toStream.

import scalaz._
import Scalaz._

val str = Stream(2,1,2,2,2,2,2,2,2)
var i = 0 //only here for testing
val r = str.foldr(Some(0):Option[Int])((n,s) => {
  println(i)
  i+=1
  if (n % 2 == 0) s.map(n+) else None
})

이것은 인쇄 만

0
1

익명 함수가 두 번만 호출된다는 것을 명확하게 보여줍니다 (즉, 홀수를 만날 때까지). 이는 서명 (의 경우 Stream)이 다음 과 같은 foldr의 정의 때문 입니다.def foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): B . 익명 함수는 이름 매개 변수를 두 번째 인수로 사용하므로 평가할 필요가 없습니다.

Btw, OP의 패턴 매칭 솔루션으로 여전히 이것을 작성할 수 있지만 if / else를 찾고 더 우아하게 매핑합니다.


- 표현 println앞에 넣으면 어떻게 되나요? ifelse
missingfaktor

@missingfaktor : 다음은 0 인쇄하고 1,하지만 더
김 Stebel

@missingfaktor : 내 요점은이 방법을 쉽게하기 때문에, 나는이 질문에 대해 답을 변경
김 Stebel에게

1
을 사용하여 이터 러블을 스트림으로 바꿀 수 있으므로이 toStream답변은 처음 나타나는 것보다 더 일반적인 목적입니다.
Rex Kerr

2
scalaz를 사용하고 있으므로 ‛0.some ‛을 사용하지 않는 이유는 무엇입니까?
pedrofurla 2012 년

7

음, Scala는 비 로컬 반환을 허용합니다. 이것이 좋은 스타일인지 아닌지에 대한 의견이 다릅니다.

scala> def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
     |   nums.foldLeft (Some(0): Option[Int]) {
     |     case (None, _) => return None
     |     case (Some(s), n) if n % 2 == 0 => Some(s + n)
     |     case (Some(_), _) => None
     |   }
     | }
sumEvenNumbers: (nums: Iterable[Int])Option[Int]

scala> sumEvenNumbers(2 to 10)
res8: Option[Int] = None

scala> sumEvenNumbers(2 to 10 by 2)
res9: Option[Int] = Some(30)

편집하다:

이 특별한 경우 @Arjan이 제안한 것처럼 다음을 수행 할 수도 있습니다.

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  nums.foldLeft (Some(0): Option[Int]) {
    case (Some(s), n) if n % 2 == 0 => Some(s + n)
    case _ => return None
  }
}

2
대신 Some(0): Option[Int]그냥 쓸 수 있습니다 Option(0).
Luigi Plinge

1
@LuigiPlinge, 네. 방금 OP의 코드를 복사하여 붙여 넣었고 요점을 만드는 데 필요한 충분한 수정 만했습니다.
missingfaktor

5

고양이 라는 방법이있다 foldM 단락 않습니다 (위해를 Vector, List, Stream, ...).

다음과 같이 작동합니다.

def sumEvenNumbers(nums: Stream[Int]): Option[Long] = {
  import cats.implicits._
  nums.foldM(0L) {
    case (acc, c) if c % 2 == 0 => Some(acc + c)
    case _ => None
  }
}

짝수가 아닌 요소를 찾으면 None나머지를 계산하지 않고 반환하고, 그렇지 않으면 짝수 항목의 합계를 반환합니다.

짝수 항목을 찾을 때까지 카운트를 유지하려면 다음을 사용해야합니다. Either[Long, Long]


4

foldM@Didac이 제안한 것처럼 cats lib에서 사용할 수 있지만 실제 합계를 얻으려면 Either대신 사용 하는 것이 좋습니다 Option.

bifoldMap에서 결과를 추출하는 데 사용됩니다 Either.

import cats.implicits._

def sumEven(nums: Stream[Int]): Either[Int, Int] = {
    nums.foldM(0) {
      case (acc, n) if n % 2 == 0 => Either.right(acc + n)
      case (acc, n) => {
        println(s"Stopping on number: $n")
        Either.left(acc)
      }
    }
  }

예 :

println("Result: " + sumEven(Stream(2, 2, 3, 11)).bifoldMap(identity, identity))
> Stopping on number: 3
> Result: 4

println("Result: " + sumEven(Stream(2, 7, 2, 3)).bifoldMap(identity, identity))
> Stopping on number: 7
> Result: 2

이것은 내 의견으로는 가장 편리하지만 여전히 FP 방법이기 때문에 비슷한 답변을 게시하기 위해 여기에 왔습니다. 아무도 이것에 투표하지 않는다는 사실에 놀랐습니다. 그래서, 내 +1을 잡아. ( (acc + n).asRight대신 선호 Either.right(acc + n)하지만 어쨌든)
abdolence

보다는 bifoldMap단지 fold(L => C, R => C): C에 작동 Either[L, R]하고 당신은 필요 없어요Monoid[C]
벤 허치슨

1

@Rex Kerr 귀하의 답변이 도움이되었지만 어느 쪽이든 사용하기 위해 조정해야했습니다.

  
  def foldOrFail [A, B, C, D] (map : B => Either [D, C]) (merge : (A, C) => A) (initial : A) (it : Iterable [B]) : [D, A] = {
    val ii = it.iterator
    var b = 이니셜
    while (ii.hasNext) {
      val x = ii. 다음
      map (x) match {
        case Left (error) => return Left (error)
        case Right (d) => b = merge (b, d)
      }
    }
    오른쪽 (b)
  }

1

임시 변수를 사용하고 takeWhile을 사용해 볼 수 있습니다. 여기 버전이 있습니다.

  var continue = true

  // sample stream of 2's and then a stream of 3's.

  val evenSum = (Stream.fill(10)(2) ++ Stream.fill(10)(3)).takeWhile(_ => continue)
    .foldLeft(Option[Int](0)){

    case (result,i) if i%2 != 0 =>
          continue = false;
          // return whatever is appropriate either the accumulated sum or None.
          result
    case (optionSum,i) => optionSum.map( _ + i)

  }

evenSum있어야한다 Some(20)이 경우.



0

더 아름다운 솔루션은 span을 사용하는 것입니다.

val (l, r) = numbers.span(_ % 2 == 0)
if(r.isEmpty) Some(l.sum)
else None

...하지만 모든 숫자가 짝수이면 목록을 두 번 탐색합니다.


2
나는 당신의 솔루션에서 예시 된 측면 적 사고를 좋아하지만, 폴드를 조기에 종료하는 방법에 대한 일반적인 질문을 처리하는 대신 질문에서 선택한 특정 예만 해결합니다.
iainmcgin

나는 폴드를 일찍 끝내는 것이 아니라 우리가 폴드하려는 값에 대해 폴딩 (이 경우 합계)
만하

0

"학문적"이유 (:

var headers = Source.fromFile(file).getLines().next().split(",")
var closeHeaderIdx = headers.takeWhile { s => !"Close".equals(s) }.foldLeft(0)((i, S) => i+1)

두 번 걸릴 것입니다 그러나 그것은 좋은 라이너입니다. "닫기"를 찾지 못하면 반환됩니다.

headers.size

또 다른 (더 나은) 이것은 다음과 같습니다.

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.