ยกเลิกก่อนกำหนด


90

วิธีใดที่ดีที่สุดในการยุติการพับก่อนกำหนด เป็นตัวอย่างง่ายๆสมมติว่าฉันต้องการสรุปตัวเลขใน an 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
  }
}

อย่างไรก็ตามวิธีการแก้ปัญหานี้ค่อนข้างน่าเกลียด (เช่นเดียวกับในกรณีที่ฉันทำก่อนหน้านี้และผลตอบแทน - มันจะสะอาดและชัดเจนกว่ามาก) และที่แย่ที่สุดก็คือมันข้ามผ่านการทำซ้ำทั้งหมดแม้ว่าจะเจอกับเลขที่ไม่ใช่คู่ก็ตาม .

แล้วอะไรจะเป็นวิธีที่ดีที่สุดในการเขียนพับแบบนี้ซึ่งจะยุติก่อนเวลาอันควร? ฉันควรจะไปและเขียนสิ่งนี้ซ้ำหรือมีวิธีที่เป็นที่ยอมรับมากกว่านี้?


คุณต้องการยุติและบันทึกคำตอบระดับกลางหรือไม่
Brian Agnew

ในกรณีนี้ไม่ แต่ในกรณีทั่วไปเล็กน้อยฉันอาจต้องการส่งคืนทั้งที่มีข้อผิดพลาดหรืออะไรบางอย่าง
Heptic

มีคำถามนี้: stackoverflow.com/questions/1595427/…
ziggystar

คำตอบเกี่ยวกับการแตกออกจากลูปนี้อาจมีประโยชน์เช่นกัน: stackoverflow.com/a/2742941/1307721
ejoubaud

คำตอบ:


65

ตัวเลือกแรกของฉันมักจะใช้การเรียกซ้ำ มีขนาดกะทัดรัดน้อยกว่าเล็กน้อยอาจเร็วกว่า (ไม่ช้ากว่าแน่นอน) และในการยุติก่อนกำหนดสามารถทำให้ตรรกะชัดเจนยิ่งขึ้น ในกรณีนี้คุณต้องมีค่า def ที่ซ้อนกันซึ่งค่อนข้างอึดอัด:

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หมายถึงการยุติ หรือใช้ฟังก์ชันตัวบ่งชี้ที่สองที่ส่งสัญญาณว่าเสร็จสิ้น การพับแบบขี้เกียจของ Scalaz ที่ Kim Stebel แสดงไว้ครอบคลุมกรณีแรกแล้วดังนั้นฉันจะแสดงที่สอง (พร้อมการใช้งานที่ไม่แน่นอน):

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ตัวมันเองจะทำงานได้ดีถ้ามี a findOrPreviousแต่มันไม่มีและงานพิเศษที่ต้องทำด้วยมือทำให้เป็นตัวเลือกที่โง่ที่จะใช้ที่นี่)


foldOrFailเป็นสิ่งที่ฉันได้มากับเมื่อคิดเกี่ยวกับคำถามที่ว่า ไม่มีเหตุผลที่จะไม่ใช้ตัววนซ้ำแบบผันแปรและ while loop ในการใช้งาน IMO เมื่อทั้งหมดถูกห่อหุ้มอย่างดี การใช้iteratorควบคู่กับการเรียกซ้ำไม่สมเหตุสมผล
0__

@Rex Kerr ขอบคุณสำหรับคำตอบของคุณฉันปรับรุ่นสำหรับการใช้งานของฉันเองที่ใช้ Either ... (ฉันจะโพสต์เป็นคำตอบ)
Core

อาจเป็นข้อเสียอย่างหนึ่งของวิธีการแก้ปัญหาที่ใช้ผลตอบแทนคือต้องใช้เวลาสักครู่เพื่อให้ทราบว่าฟังก์ชันใดใช้กับ: sumEvenNumbersหรือพับop
Ivan Balashov

1
@IvanBalashov - ใช้เวลาสักพักหนึ่งในการเรียนรู้ว่ากฎของ Scala มีไว้เพื่ออะไรreturn(กล่าวคือส่งคืนจากวิธีการที่ชัดเจนที่สุดที่คุณพบ) แต่หลังจากนั้นก็ไม่ควรใช้เวลานานนัก กฎค่อนข้างชัดเจนและdefให้วิธีการปิดล้อมอยู่
Rex Kerr

1
ฉันชอบ foldOrFail ของคุณ แต่โดยส่วนตัวแล้วฉันจะทำให้ประเภท return Bไม่ใช่Option[B]เพราะมันจะทำงานเหมือน fold โดยที่ return type จะเหมือนกับประเภทของ zero เพียงแค่แทนที่ผลตอบแทน Option ทั้งหมดด้วย b และวางในไม่มีเป็นศูนย์ หลังจากคำถามทั้งหมดต้องการการพับที่สามารถยุติได้ก่อนกำหนดแทนที่จะล้มเหลว
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

คุณสามารถทำสิ่งที่คุณต้องการในรูปแบบการทำงานโดยใช้ foldRight เวอร์ชันขี้เกียจใน scalaz สำหรับคำอธิบายเชิงลึกเพิ่มเติมโปรดดูที่โพสต์บล็อกนี้ ในขณะที่การแก้ปัญหานี้ใช้StreamคุณสามารถแปลงIterableเป็นอย่างมีประสิทธิภาพด้วยStreamiterable.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

ซึ่งแสดงให้เห็นอย่างชัดเจนว่าฟังก์ชันนิรนามถูกเรียกเพียงสองครั้ง (กล่าวคือจนกว่าจะพบเลขคี่) นั่นเป็นเพราะความหมายของ foldr ซึ่งมีลายเซ็น (ในกรณีของStream) def foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): Bจะ โปรดทราบว่าฟังก์ชัน anonymous รับพารามิเตอร์ by name เป็นอาร์กิวเมนต์ที่สองดังนั้นจึงไม่จำเป็นต้องได้รับการประเมิน

Btw คุณยังสามารถเขียนสิ่งนี้ด้วยโซลูชันการจับคู่รูปแบบของ OP ได้ แต่ฉันพบว่า / else และแผนที่ดูหรูหรากว่า


จะเกิดอะไรขึ้นถ้าคุณใส่printlnก่อนif- elseนิพจน์?
missingfaktor

@missingfaktor: จากนั้นพิมพ์ 0 และ 1 แต่ไม่มาก
Kim Stebel

@missingfaktor: เนื่องจากประเด็นของฉันง่ายกว่าที่จะทำแบบนี้ฉันจึงเปลี่ยนคำตอบ
Kim Stebel

1
โปรดทราบว่าคุณสามารถเปลี่ยนการทำซ้ำให้เป็นสตรีมได้toStreamดังนั้นคำตอบนี้จึงมีวัตถุประสงค์ทั่วไปมากกว่าที่ปรากฏในตอนแรก
Rex Kerr

2
เนื่องจากคุณใช้ scalaz ทำไมไม่ใช้ ‛0 บาง‛?
pedrofurla

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จาก cats lib (ตามคำแนะนำของ @Didac) แต่ฉันแนะนำให้ใช้EitherแทนOptionหากคุณต้องการได้ผลรวมจริง

bifoldMapEitherถูกนำมาใช้ในการสกัดผลจาก

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)แต่อย่างไรก็ตาม)
รุนแรง

แทนที่จะเป็นbifoldMapเพียงแค่fold(L => C, R => C): CจะทำงานEither[L, R]แล้วคุณไม่จำเป็นต้องมีMonoid[C]
Ben Hutchison

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] = {
    วาล ii = it.iterator
    var b = เริ่มต้น
    while (ii.hasNext) {
      val x = ii.next
      แผนที่ (x) ตรงกับ {
        กรณีซ้าย (ข้อผิดพลาด) => กลับซ้าย (ข้อผิดพลาด)
        กรณีขวา (d) => b = ผสาน (b, d)
      }
    }
    ขวา (b)
  }

1

คุณสามารถลองใช้ var ชั่วคราวและใช้ 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

วิธีแก้ปัญหาที่สวยงามยิ่งขึ้นคือการใช้ช่วง:

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

... แต่มันจะข้ามรายการสองครั้งถ้าตัวเลขทั้งหมดเป็นเลขคู่


2
ฉันชอบความคิดด้านข้างที่ยกตัวอย่างมาจากวิธีแก้ปัญหาของคุณ แต่จะแก้เฉพาะตัวอย่างที่เลือกในคำถามแทนที่จะจัดการกับคำถามทั่วไปเกี่ยวกับวิธียุติการพับก่อนกำหนด
iainmcgin

ฉันต้องการแสดงวิธีการทำย้อนกลับไม่ใช่การยกเลิกการพับก่อนกำหนด แต่เป็นการพับเท่านั้น (ในกรณีนี้ผลรวม) เหนือค่าที่เราต้องการพับ
Arjan

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.