Hủy bỏ sớm trong màn hình đầu tiên


88

Cách tốt nhất để kết thúc màn hình gấp là gì? Như một ví dụ đơn giản, hãy tưởng tượng tôi muốn tính tổng các số trong một Iterable, nhưng nếu tôi gặp phải điều gì đó mà tôi không mong đợi (giả sử là một số lẻ), tôi có thể muốn chấm dứt. Đây là ước tính đầu tiên

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
  }
}

Tuy nhiên, giải pháp này khá xấu (như trong, nếu tôi đã thực hiện .foreach và return - nó sẽ gọn gàng và rõ ràng hơn nhiều) và tệ nhất là nó đi qua toàn bộ có thể lặp lại ngay cả khi nó gặp một số không chẵn .

Vậy cách tốt nhất để viết một trang gấp như thế này, kết thúc sớm là gì? Tôi có nên đi và viết điều này một cách đệ quy, hay có cách nào được chấp nhận hơn không?


Bạn có muốn chấm dứt và ghi lại câu trả lời trung gian không?
Brian Agnew,

Trong trường hợp này, không. Tuy nhiên, trong một trường hợp hơi tổng quát hơn tôi có thể muốn trả về một Dù rằng có một lỗi hoặc một cái gì đó
Heptic


Trả lời này về vi phạm ra khỏi vòng cũng có thể được tìm thấy hữu ích: stackoverflow.com/a/2742941/1307721
ejoubaud

Câu trả lời:


64

Lựa chọn đầu tiên của tôi thường là sử dụng đệ quy. Nó chỉ nhỏ gọn hơn vừa phải, có khả năng nhanh hơn (chắc chắn không chậm hơn) và việc kết thúc sớm có thể làm cho logic rõ ràng hơn. Trong trường hợp này, bạn cần có các định nghĩa lồng nhau hơi khó xử:

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)
}

Lựa chọn thứ hai của tôi là sử dụng return, vì nó giữ cho mọi thứ khác nguyên vẹn và bạn chỉ cần bọc nếp gấp defđể bạn có thứ gì đó để trả lại - trong trường hợp này, bạn đã có một phương thức, vì vậy:

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

mà trong trường hợp cụ thể này là nhỏ gọn hơn rất nhiều so với đệ quy (mặc dù chúng tôi đặc biệt không may mắn với đệ quy vì chúng tôi phải thực hiện một phép biến đổi có thể lặp lại / lặp). Luồng điều khiển nhảy vọt là điều cần tránh khi tất cả những thứ khác bằng nhau, nhưng ở đây thì không. Không có hại khi sử dụng nó trong những trường hợp nó có giá trị.

Nếu tôi làm việc này thường xuyên và muốn nó ở giữa một phương thức ở đâu đó (vì vậy tôi không thể chỉ sử dụng return), tôi có thể sẽ sử dụng xử lý ngoại lệ để tạo luồng điều khiển không cục bộ. Đó là, xét cho cùng, nó tốt ở điểm nào và việc xử lý lỗi không phải là lần duy nhất nó hữu ích. Mẹo duy nhất là tránh tạo ra một dấu vết ngăn xếp (thực sự chậm), và điều đó thật dễ dàng vì đặc điểm NoStackTracevà đặc điểm con của nó ControlThrowableđã làm điều đó cho bạn. Scala đã sử dụng nội bộ này (trên thực tế, đó là cách nó thực hiện việc trả về từ bên trong màn hình đầu tiên!). Hãy tạo của riêng chúng ta (không thể lồng vào nhau, mặc dù người ta có thể khắc phục điều đó):

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
  })
}

Tất nhiên ở đây sử dụng returnsẽ tốt hơn, nhưng lưu ý rằng bạn có thể đặt ở shortcutbất cứ đâu, không chỉ gói toàn bộ một phương thức.

Tiếp theo đối với tôi sẽ là triển khai lại màn hình đầu tiên (bản thân tôi hoặc tìm một thư viện thực hiện điều đó) để nó có thể báo hiệu kết thúc sớm. Hai cách tự nhiên để làm điều này là không truyền giá trị mà là Optionchứa giá trị, nơi Nonebiểu thị sự kết thúc; hoặc sử dụng chức năng chỉ báo thứ hai báo hiệu hoàn thành. Phần lười Scalaz do Kim Stebel thể hiện đã bao gồm trường hợp đầu tiên, vì vậy tôi sẽ hiển thị phần thứ hai (với cách triển khai có thể thay đổi):

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)(_ + _)

(Cho dù bạn thực hiện kết thúc bằng đệ quy, trả lại, lười biếng, v.v. hay không là tùy thuộc vào bạn.)

Tôi nghĩ rằng điều đó bao gồm các biến thể hợp lý chính; cũng có một số tùy chọn khác, nhưng tôi không chắc tại sao người ta lại sử dụng chúng trong trường hợp này. ( IteratorBản thân nó sẽ hoạt động tốt nếu nó có findOrPrevious, nhưng nó không, và công việc bổ sung cần thiết để làm điều đó bằng tay khiến nó trở thành một lựa chọn ngớ ngẩn khi sử dụng ở đây.)


Các foldOrFailchính xác là những gì tôi đã đưa ra khi nghĩ về câu hỏi này. Không có lý do gì để không sử dụng một vòng lặp có thể thay đổi và một vòng lặp while trong IMO triển khai, khi tất cả được đóng gói độc đáo. Sử dụng iteratorcùng với đệ quy không có ý nghĩa.
0__

@Rex Kerr, cảm ơn câu trả lời của bạn, tôi đã chỉnh sửa một phiên bản cho mục đích sử dụng của riêng mình, sử dụng Either ... (Tôi sẽ đăng nó dưới dạng câu trả lời)
Core

Có lẽ một trong những nhược điểm của việc trở lại giải pháp dựa trên, đó là phải mất một thời gian để nhận ra có chức năng nó áp dụng cho: sumEvenNumbershoặc gấp củaop
Ivan Balashov

1
@IvanBalashov - Chà, phải mất một lúc để tìm hiểu các quy tắc của Scala dùng để làm gì return(tức là, nó trả về từ phương thức rõ ràng nhất mà bạn tìm thấy trong đó), nhưng sau đó sẽ không mất nhiều thời gian. Quy tắc này khá rõ ràng, và defphương pháp cho đi nằm ở đâu.
Rex Kerr

Tôi như foldOrFail của bạn, nhưng cá nhân tôi đã có thể làm các kiểu trả về Bkhông Option[B]vì sau đó nó hoạt động như lần nơi kiểu trả về là giống như kiểu số không cho ác của. Đơn giản là thay thế tất cả các trả về Quyền chọn bằng b. và pas trong None là số không. Sau tất cả, câu hỏi muốn một màn hình có thể kết thúc sớm, thay vì thất bại.
Karl

26

Kịch bản bạn mô tả (thoát ra khi gặp một số điều kiện không mong muốn) có vẻ như là một trường hợp sử dụng tốt cho takeWhilephương pháp. Về cơ bản filter, nó sẽ kết thúc khi gặp một phần tử không đáp ứng điều kiện.

Ví dụ:

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)

Điều này cũng sẽ hoạt động tốt cho Iterators / Iterables. Giải pháp tôi đề xuất cho "tổng các số chẵn, nhưng ngắt trên số lẻ" là:

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

Và chỉ để chứng minh rằng nó không lãng phí thời gian của bạn khi nó chạm một số lẻ ...

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

Đây chính xác là kiểu đơn giản mà tôi đang tìm kiếm - cảm ơn bạn!
Tanner

14

Bạn có thể làm những gì bạn muốn theo một phong cách chức năng bằng cách sử dụng phiên bản lười biếng của foldRight trong scalaz. Để có lời giải thích sâu hơn, hãy xem bài đăng trên blog này . Trong khi giải pháp này sử dụng a Stream, bạn có thể chuyển đổi một Iterablethành một Streamhiệu quả với 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
})

Điều này chỉ in

0
1

điều này cho thấy rõ ràng rằng hàm ẩn danh chỉ được gọi hai lần (tức là cho đến khi nó gặp số lẻ). Đó là do định nghĩa của foldr, mà chữ ký của nó (trong trường hợp Stream) là def foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): B. Lưu ý rằng hàm ẩn danh lấy tham số by name làm đối số thứ hai của nó, vì vậy nó không cần được đánh giá.

Btw, bạn vẫn có thể viết điều này với giải pháp khớp mẫu của OP, nhưng tôi thấy if / else và phối đồ thanh lịch hơn.


Điều gì xảy ra nếu bạn đặt printlntrước if- elsebiểu thức?
missfaktor

@missingfaktor: sau đó nó in 0 và 1, nhưng không nhiều
Kim Stebel

@missingfaktor: từ quan điểm của tôi là dễ dàng hơn để thực hiện theo cách này, tôi đã thay đổi nó trong câu trả lời
Kim Stebel

1
Lưu ý rằng bạn có thể biến bất kỳ tệp nào có thể lặp lại thành luồng với toStream, vì vậy câu trả lời này có mục đích chung hơn so với lần đầu xuất hiện.
Rex Kerr

2
Vì bạn đang sử dụng scalaz tại sao không sử dụng ‛0.some‛?
pedrofurla

7

Chà, Scala không cho phép trả hàng không thuộc địa phương. Có nhiều ý kiến ​​khác nhau về việc liệu đây có phải là một phong cách tốt hay không.

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)

BIÊN TẬP:

Trong trường hợp cụ thể này, như @Arjan đã đề xuất, bạn cũng có thể làm:

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
thay vì Some(0): Option[Int]bạn chỉ có thể viết Option(0).
Luigi Plinge

1
@LuigiPlinge, vâng. Tôi chỉ sao chép mã của OP và chỉ thực hiện đủ sửa đổi cần thiết để tạo ra một điểm.
missfaktor

5

Mèo có một phương pháp gọi là foldM mà ngắn mạch (ví Vector, List, Stream, ...).

Nó hoạt động như sau:

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
  }
}

Ngay sau khi một trong các phần tử trên tập hợp không đồng đều, nó sẽ trả về.


4

Bạn có thể sử dụng foldMtừ mèo lib (theo đề xuất của @Didac) nhưng tôi khuyên bạn nên sử dụng Eitherthay vì Optionnếu bạn muốn tính tổng thực tế.

bifoldMapđược sử dụng để trích xuất kết quả từ 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)
      }
    }
  }

ví dụ:

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

Đến đây để đăng một câu trả lời tương tự, bởi vì đây là cách thuận tiện nhất nhưng vẫn là FP để làm theo ý kiến ​​của tôi. Tôi ngạc nhiên là không ai bỏ phiếu cho điều này. Vì vậy, hãy lấy +1 của tôi. (Tôi thích (acc + n).asRightthay vì Either.right(acc + n)nhưng dù sao)
abdolence

1

@Rex Kerr câu trả lời của bạn đã giúp tôi, nhưng tôi cần chỉnh sửa nó để sử dụng Either

  
  def foldOrFail [A, B, C, D] (map: B => Either [D, C]) (merge: (A, C) => A) (initial: A) (it: Iterable [B]): Hoặc [D, A] = {
    val ii = it.iterator
    var b = ban đầu
    while (ii.hasNext) {
      val x = ii. tiếp theo
      map (x) match {
        trường hợp Left (lỗi) => trả về Left (lỗi)
        case Right (d) => b = merge (b, d)
      }
    }
    Đúng (b)
  }

1

Bạn có thể thử sử dụng var tạm thời và sử dụng takeWhile. Đây là một phiên bản.

  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)

  }

Điều evenSumnên có Some(20)trong trường hợp này.



0

Một giải pháp đẹp hơn sẽ là sử dụng span:

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

... nhưng nó lướt qua danh sách hai lần nếu tất cả các số đều


2
Tôi thích tư duy bên được minh họa bởi giải pháp của bạn, nhưng nó chỉ giải quyết được ví dụ cụ thể được chọn trong câu hỏi hơn là giải quyết câu hỏi chung về cách kết thúc sớm một giao dịch.
iainmcgin

tôi muốn chỉ ra cách làm ngược lại, không kết thúc sớm một màn hình mà chỉ gấp (trong trường hợp này là tổng) đối với các giá trị mà chúng tôi muốn gấp lại
Arjan

0

Chỉ vì lý do "học thuật" (:

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

Phải mất hai lần sau đó nhưng nó là một lớp lót tốt đẹp. Nếu không tìm thấy "Đóng", nó sẽ trở lại

headers.size

Cái khác (tốt hơn) là cái này:

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.