Перервати рано в складці


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 - це було б набагато чистішим і зрозумілішим) і найгірше за все, воно проходить весь ітерабель, навіть якщо зустрічає не парне число .

То який найкращий спосіб написати такий згин, який закінчується достроково? Чи мені просто піти і написати це рекурсивно, чи є більш прийнятний спосіб?


Ви хочете припинити і записати проміжну відповідь?
Brian Agnew

У цьому випадку ні. Але в дещо більш загальному випадку я міг би повернути
E,

Існує це питання: stackoverflow.com/questions/1595427 / ...
ziggystar

Ця відповідь про злом з петель також може бути знайдений корисним: stackoverflow.com/a/2742941/1307721
ejoubaud

Відповіді:


64

Моїм першим вибором, як правило, було б використовувати рекурсію. Він лише помірно менш компактний, потенційно швидший (звичайно, не повільніший) і на початку припинення може зробити логіку більш зрозумілою. У цьому випадку вам потрібні вкладені defs, що трохи незручно:

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

що в цьому конкретному випадку набагато компактніше, ніж рекурсія (хоча нам особливо не пощастило з рекурсією, оскільки нам довелося зробити ітераційне / ітераторне перетворення). Стримкого потоку управління - це те, чого слід уникати, коли все інше рівне, але тут це не так. Не шкодить його використанню у випадках, коли це цінно.

Якби я робив це часто і хотів, щоб це було десь у середині методу (тому я не міг просто використовувати return), я б, мабуть, використовував обробку винятків для генерації нелокального потоку управління. Тобто, зрештою, в чому він хороший, і обробка помилок - не єдиний раз, коли це корисно. Єдина хитрість - уникнути створення трасування стека (що дійсно повільно), і це легко, тому що ознака 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де завгодно, а не просто обертати цілий метод.

Наступним на черзі для мене буде повторна реалізація fold (або я, або пошук бібліотеки, яка це робить), щоб це могло сигналізувати про дострокове припинення. Два природні способи зробити це не розповсюдження значення, а Optionвміст значення, де Noneозначає припинення; або використовувати другу функцію індикатора, яка сигналізує про завершення. Ледача складка Scalaz, показана Кімом Стібелем, уже охоплює перший випадок, тому я покажу другий (із змінним виконанням):

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саме те, що я придумав, роздумуючи над цим питанням. Немає причин не використовувати змінний ітератор та цикл while у реалізації IMO, коли все гарно інкапсульовано. Використання iteratorразом з рекурсією не має сенсу.
0__

@Rex Kerr, спасибі за вашу відповідь, я налаштував версію для власного користування, яка використовує будь-який ... (збираюся опублікувати її як відповідь)
Core,

Ймовірно, однією з мінусів рішення, що базується на поверненні , є те, що потрібно деякий час, щоб усвідомити, до якої функції воно застосовується: sumEvenNumbersабо фолдop
Іван Балашов

1
@IvanBalashov - Ну, потрібно один раз, щоб дізнатись, для чого призначені правила Scala return(тобто він повертається із найпотаємнішого явного методу, в якому ви його знайдете), але після цього це не займе дуже багато часу. Правило досить чітке, і defвидає, де знаходиться метод укладання.
Рекс Керр,

Мені подобається ваш foldOrFail, але особисто я б зробив тип return Bне Option[B]тому, що тоді він поводиться як fold, де тип повернення такий самий, як і тип нульового накопичувача. Просто замініть усі повернення Option на b. і pas в None як нуль. Врешті-решт, питання вимагало складки, яка може закінчитися раніше, а не провалитися.
Карл

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

Саме такої простоти я шукав - дякую!
Таннер

14

Ви можете робити те, що хочете, у функціональному стилі, використовуючи ледачу версію foldRight у scalaz. Для більш детального пояснення див. Цю публікацію в блозі . Хоча це рішення використовує a Stream, ви можете перетворити Iterablea на Streamефективно за допомогою 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) єdef foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): B . Зверніть увагу, що анонімна функція приймає параметр by name як другий аргумент, тому її не потрібно оцінювати.

До речі, ви все ще можете написати це з рішенням збігу шаблонів OP, але я вважаю, що if / else і карта є більш елегантною.


Що станеться, якщо поставити printlnперед if- elseвираз?
missingfaktor

@missingfaktor: тоді він друкує 0 і 1, але не більше
Кім Стебель

@missingfaktor: оскільки мою думку легше зробити таким чином, я змінив її у відповіді
Кім Стебель

1
Зверніть увагу, що ви можете перетворити будь-яку ітерацію на потік за допомогою toStream , тому ця відповідь є більш загальною, ніж вона з’являється спочатку.
Рекс Керр,

2
Оскільки ур використовує скалаз, чому б не використовувати ‛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).
Луїджі Плінге

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

Як тільки один з елементів колекції не є парним, він повертається.


4

Ви можете використовувати foldMвід lib кішки (як запропонував @Didac), але я пропоную використовувати 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)але все одно)
відступництво

1

@Rex Kerr, ваша відповідь мені допомогла, але мені потрібно було її налаштувати, щоб використовувати будь-який

  
  def foldOrFail [A, B, C, D] (map: B => Або [D, C]) (злиття: (A, C) => A) (початкове: A) (it: Ітерабельне [B]): Або [D, A] = {
    val ii = it.iterator
    var b = початковий
    while (ii.hasNext) {
      val x = ii.наступ
      map (x) match {
        case Left (помилка) => return Left (помилка)
        справа Справа (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

Більш гарним рішенням було б використання span:

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")
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.