Статьи

5 головоломок Скала, которые сделают ваш мозг больно

Охота за неинтуитивными случаями исключений и ошибок в Scala

Для этого поста мы связались с Нермином Серифовичем и Эндрю Филлипсом, которых вы, возможно, знаете из Scala Puzzlers . Вместе мы выбрали несколько вопросов для изучения ошибок и исключений в Scala. Поначалу некоторые вопросы могут показаться странными, но все это имеет смысл, когда вы копаете, чтобы узнать, что на самом деле происходит под капотом. Мы надеемся, что этот пост поможет вам лучше узнать все тонкости Scala, и, тем не менее, важно, что вы найдете его приятным. Давайте копаться в.

Ответы внизу поста. Но эй, не заглядывай!

1. Исключительная неудача

Каков результат выполнения следующего кода?

01
02
03
04
05
06
07
08
09
10
11
12
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
 
val f = Future { throw new Error("fatal!") } recoverWith {
    case err: Error => Future.successful("Ignoring error: " + err.getMessage)
}
 
f onComplete {
    case Success(res) => println("Yay: " + res)
    case Failure(e) => println("Oops: " + e.getMessage)
}

ответы

  1. Печать:
    Yay: Игнорирование ошибки: фатально!
  2. Выдает ошибку
  3. Печать:
    Упс: смертельно!
  4. Печать:
    К сожалению: в штучной упаковке ошибка

2. $!. *% Итераторы!

Каков результат выполнения следующего кода?

1
2
3
4
5
6
val t = "this is a test"
val rx = " ".r
val m = rx.findAllIn(t)
println(m)
println(m.end)
println(rx.findAllIn(t).end)

ответы

  1. непустой итератор
    5
    5
  2. пустой итератор
    java.lang.IllegalStateException: нет совпадений
    java.lang.IllegalStateException: нет совпадений
  3. непустой итератор
    5
    java.lang.IllegalStateException: нет совпадений
  4. непустой итератор
    java.lang.IllegalStateException: нет совпадений
    java.lang.IllegalStateException: нет совпадений

3. Что в имени?

Каков результат выполнения следующего кода?

01
02
03
04
05
06
07
08
09
10
class C {
    def sum(x: Int = 1, y: Int = 2): Int = x + y
}
class D extends C {
    override def sum(y: Int = 3, x: Int = 4): Int = super.sum(x, y)
}
val d: D = new D
val c: C = d
c.sum(x = 0)
d.sum(x = 0)

ответы

  1. 2
    3
  2. 1
    3
  3. 4
    3
  4. 3
    3

4. (Ex) Поток Сюрприз

Каков результат выполнения следующего кода?

1
2
3
4
5
val nats: Stream[Int] = 1 #:: (nats map { _ + 1 })
val odds: Stream[Int] = 1 #:: (odds map { _ + 1 } filter { _ % 2 != 0 })
 
nats filter { _ % 2 != 0 } take 2 foreach println
odds take 2 foreach println

ответы

  1. Печать:
    1
    3
    1
    3
  2. Печать:
    1
    2
    1
    3
  3. Первое утверждение печатает:
    1
    3 и второй выдает исключение времени выполнения
  4. Первый оператор генерирует исключение времени выполнения, а второй выводит:
    1
    3

5. Случай Струн

Каков результат выполнения следующего кода?

01
02
03
04
05
06
07
08
09
10
11
12
def objFromJava: Object = "string"
def stringFromJava: String = null
 
def printLengthIfString(a: AnyRef) {
    a match {
        case s: String => println("String of length " + s.length)
        case _ => println("Not a string")
    }
}
 
printLengthIfString(objFromJava)
printLengthIfString(stringFromJava)

ответы

  1. Печать:
    Не строка
    Строка длины 0
  2. Первые отпечатки:
    Не строка, а вторая создает исключение NullPointerException
  3. Печать:
    Строка длины 6
    Не строка
  4. Первые отпечатки:
    Строка длиной 6, а вторая создает исключение NullPointerException

Решения

1. Исключительная неудача

Правильный ответ: 4 => В этом фрагменте будет напечатано «Упс: ошибка в штучной упаковке».

1
2
3
4
5
6
7
8
val f = Future { throw new Error("fatal!") } recoverWith {
    case err: Error => Future.successful("Ignoring error: " + err.getMessage)
}
 
f onComplete {
    case Success(res) => println("Yay: " + res)
    case Failure(e) => println("Oops: " + e.getMessage)
}

Так что у нас здесь?

Есть будущее, которое выдает ошибку с сообщением «фатально!», Наивно вы можете подумать, что оно сможет восстановиться и закончить с успешным статусом. Это не тот случай. Здесь выдается исключение, а не ошибка, поэтому мы не вводим регистр ошибок в блоке recoveryWith.

Теперь, когда мы знаем, что будущее терпит неудачу, есть еще одна оговорка. Сообщение, которое мы распечатываем, не является сообщением об ошибке, которое мы выдавали. Исключением, которое было сгенерировано, является java.util.concurrent.ExecutionException, которое выходит из коробки с сообщением… «Ошибка в штучной упаковке», когда оно создается из будущего Fails.

Следовательно, результат будущего:
Сбой (новое ExecutionException («штучная ошибка», новая ошибка («фатально!»)))

2. $!. *% Итераторы!

Правильный ответ: 3 =>

непустой итератор
5
java.lang.IllegalStateException: нет совпадений

1
2
3
4
5
6
val t = "this is a test"
val rx = " ".r
val m = rx.findAllIn(t)
println(m)
println(m.end)
println(rx.findAllIn(t).end)

Так что у нас здесь?

Этот фрагмент создает регулярное выражение и находит все его совпадения (пробелы) в предоставленной строке. Имеет смысл, но findAllIn возвращает итератор. При печати его toString дает нам либо «пустой итератор», либо «непустой итератор». Поскольку у нас есть 3 попадания на эту строку, она не пустая.

В дальнейшем end возвращает индекс персонажа после окончания текущего попадания. Так что это индекс после первого пробела, который 5.

Теперь наступает удивительная часть. В финале мы получаем исключение IllegalStateException. Действительно $!. *% Итераторы! Когда дело доходит до дела, если вы не попросите или не проверите первый элемент, регулярное выражение не будет активировано. Для первого println мы запросили toString и в качестве «побочного эффекта» мы активировали регулярное выражение, поэтому со вторым println проблем не возникало. Для третьего вывода мы оставляемся наедине и получаем исключение.

3. Что в имени?

Правильный ответ: 3 => Печать 4, 3

01
02
03
04
05
06
07
08
09
10
class C {
    def sum(x: Int = 1, y: Int = 2): Int = x + y
}
class D extends C {
    override def sum(y: Int = 3, x: Int = 4): Int = super.sum(x, y)
}
val d: D = new D
val c: C = d
c.sum(x = 0)
d.sum(x = 0)

Так что у нас здесь?

Есть 2 вопроса:

  • Привязка имен параметров выполняется компилятором, и единственная информация, которую может использовать компилятор, — это статический тип переменной.
  • Для параметров со значением по умолчанию компилятор создает методы, которые вычисляют выражения аргумента по умолчанию. В приведенном выше примере оба класса C и D содержат методы sum $ default $ 1 и sum $ default $ 2 для двух параметров по умолчанию. Когда параметр отсутствует, компилятор использует результат этих методов, и эти методы вызываются во время выполнения.

Начиная с c.sum (x = 0), мы называем сумму класса C с отсутствующим параметром y. Что является вторым параметром. Что происходит за кулисами, поскольку c был создан из d, он содержит методы по умолчанию, созданные для отсутствующих параметров из класса d. Когда он выбирает, какой метод использовать, он делает это независимо от имен, противоположных для x и y. Второй параметр отсутствует, поэтому предполагается, что он равен 4, а первый — 0. Первый результат — 0 + 4 = 4. Вау, мой мозг болит.

С d.sum (x = 0) ситуация немного отличается, мы сразу переходим к сумме D, и первый параметр y отсутствует. Мы предполагаем, что это 3, а результат 0 + 3.

Джош Суерет резюмирует это с помощью правила: «Имена статичны; значения времени выполнения ».

4. (Ex) Поток Сюрприз

Правильный ответ: 3 => Первый оператор печатает: 1 3, а второй выдает исключение времени выполнения

1
2
3
4
5
val nats: Stream[Int] = 1 #:: (nats map { _ + 1 })
val odds: Stream[Int] = 1 #:: (odds map { _ + 1 } filter { _ % 2 != 0 })
 
nats filter { _ % 2 != 0 } take 2 foreach println
odds take 2 foreach println

Так что у нас здесь?

Этот фрагмент создает 2 потока, натс и шансы. Первый содержит (1,?) И содержит правило для добавления к нему большего количества элементов, простые +1 приращения. Точно так же второе имеет то же правило с фильтром, который позволяет ему содержать только нечетные числа. Проблема возникает, когда мы пытаемся напечатать это.

Для nats, когда мы разворачиваемся, проблем нет, и мы просто берем первые 2 нечетных значения, так как фильтр применяется после ленивого создания потока: (1, (2, (3,?)))

Для шансов, мы не можем взять 2. Просто первый. Мы входим в бесконечное рекурсивное состояние. Поскольку 2 не является нечетным, и фильтр активируется при создании потока с правилом +1, мы никогда не достигаем 3 и не сталкиваемся с исключением времени выполнения.

5. Случай Струн

Правильный ответ: 3 => Печать: строка длиной 6, а не строка

01
02
03
04
05
06
07
08
09
10
11
12
def objFromJava: Object = "string"
def stringFromJava: String = null
 
def printLengthIfString(a: AnyRef) {
    a match {
        case s: String => println("String of length " + s.length)
        case _ => println("Not a string")
    }
}
 
printLengthIfString(objFromJava)
printLengthIfString(stringFromJava)

Так что у нас здесь?

Для objFromJava мы имеем дело со строкой, даже если она изначально была создана как объект. Поскольку разрешение сопоставления с образцом основано на типе времени выполнения, длина строки «строка» выводится на печать.

Для stringFromJava, который является нулевым, мы получаем «Не строка». Поскольку ноль — это совсем другая проблема, нам нужно явно проверить его как один из случаев и уделить ему особое внимание.

источники

  1. Исключительная неудача
  2. $!. *% Итераторы!
  3. Что в имени?
  4. (Ex) Поток сюрприз
  5. Случай Струн

Посетите Scala Puzzlers, чтобы просмотреть полный список.

Последние мысли

Еще раз спасибо за Нермин и Эндрю за то, что они поделились с нами своими головоломками! Мы надеемся, что вам понравилось читать их и пытаться их решить. При написании поста мы, конечно, узнали кое-что новое, и мы надеемся, что и вы тоже. Хотя, если вы когда-нибудь сталкивались с такими головоломками в своем собственном коде, это может быть не так весело. Для подобных ситуаций мы создали Takipi для Scala . Takipi — это собственный агент, который знает, как отслеживать необработанные исключения, перехваченные исключения и регистрировать ошибки на рабочих серверах. Он позволяет вам видеть значения переменных, которые вызывают ошибки, по всему стеку и накладывать их на ваш код.