Статьи

Option.fold () считается нечитаемым

Недавно во время проверки кода мы долго обсуждали, является ли scala.Option.fold() идиоматичным, умным или, возможно, нечитаемым и хитрым? Давайте сначала опишем, в чем проблема. Option.fold делает две вещи: отображает функцию f на значение Option (если есть) или возвращает альтернативный alt если он отсутствует. Используя простое сопоставление с образцом, мы можем реализовать его следующим образом:

1
2
3
4
5
6
7
8
val option: Option[T] = //...
def alt: R = //...
def f(in: T): R = //...
  
val x: R = option match {
    case Some(v) => f(v)
    case None => alt
}

Если вы предпочитаете однострочное, getOrElse фактически является комбинацией map и getOrElse :

1
val x: R = option map f getOrElse alt

Или, если вы программист на C, который все еще хочет писать на C, но использует компилятор Scala:

1
2
3
4
val x: R = if (option.isDefined)
    f(option.get)
else
    alt

Интересно, что это похоже на то, как на самом деле реализован fold() , но это деталь реализации. ОК, все вышеперечисленное может быть заменено одним Option.fold() :

1
val x: R = option.fold(alt)(f)

Технически вы можете даже использовать операторы /: и \: ( alt /: option но это будет просто мазохистским. У меня есть три проблемы с option.fold() . Прежде всего — это совсем не читабельно. Мы сворачиваем (уменьшаем) Option которая не имеет особого смысла. Во-вторых, он переворачивает обычный поток в положительном, а затем отрицательном случае , начиная с условия сбоя (отсутствия, alt ), за которым следует блок присутствия (функция f ; см. Также: Рефакторинг map-getOrElse для свертывания ). Интересно, что этот метод работал бы отлично для меня, если бы он был назван mapOrElse :

1
2
3
4
5
/**
 * Hypothetical in Option
 */
def mapOrElse[B](f: A => B, alt: => B): B =
    this map f getOrElse alt

На самом деле в Scalaz уже есть такой метод, который называется OptionW.cata . кат . Вот что Мартин Одерски должен сказать по этому поводу :

«Лично я нахожу такие методы, как cata которые используют два замыкания, поскольку аргументы часто перестараются. Вы действительно получаете удобство чтения по map + getOrElse ? Подумайте о новичке в вашем коде […] »

В то время как у cata есть некоторый теоретический фон , Option.fold звучит как случайное столкновение имен, которое ничего не приводит к таблице, кроме путаницы. Я знаю, что вы скажете, что у TraversableOnce есть fold и мы вроде как делаем то же самое. Почему это случайное столкновение, а не продление контракта, описанного в TraversableOnce ? Метод fold() в коллекциях Scala обычно просто делегирует один из foldLeft() / foldRight() (который лучше работает для данной структуры данных), поэтому он не гарантирует порядок, а функция свертывания должна быть ассоциативной. Но в Option.fold() контракт другой: функция сворачивания принимает только один параметр, а не два. Если вы читали мою предыдущую статью о сгибах, вы знаете, что функция сокращения всегда принимает два параметра: текущий элемент и накопленное значение (начальное значение во время первой итерации). Но Option.fold() принимает только один параметр: текущее значение параметра! Это нарушает согласованность, особенно при реализации Option.foldLeft() и Option.foldRight() имеют правильный контракт (но это не значит, что они более читабельны).

Единственный способ понять опцию сворачивания — это представить Option как последовательность с 0 или 1 элементом. Тогда это имеет смысл, верно? Нет.

1
2
3
4
def double(x: Int) = x * 2
  
Some(21).fold(-1)(double)   //OK: 42
None.fold(-1)(double)       //OK: -1

но:

1
2
3
4
5
6
Some(21).toList.fold(-1)(double)
<console>: error: type mismatch;
 found   : Int => Int
 required: (Int, Int) => Int
              Some(21).toList.fold(-1)(double)
                                       ^

Если мы рассматриваем Option[T] как List[T] , неловкое Option.fold() разрывается, потому что его тип отличается от типа TraversableOnce.fold() . Это моя самая большая проблема. Я не могу понять, почему свертывание не было определено в терминах системы типов (черта?) И строго реализовано. В качестве примера рассмотрим:

Data.Foldable в Haskell (продвинутый уровень)

Data.Foldable Data.Foldable описывает различные варианты фолдинга в Haskell. Есть знакомые foldl / foldr / foldl1 / foldr1 , в Scala они называются соответственно foldLeft / reduceLeft / reduceRight / reduceRight . Они имеют тот же тип, что и Scala, и неудивительно, что они работают со всеми типами, которые вы можете свернуть, включая Maybe , списки, массивы и т. Д. Существует также функция с именем fold , но она имеет совершенно другое значение:

1
2
class Foldable t where
    fold :: Monoid m => t m -> m

В то время как другие сгибы довольно сложны, этот едва берет складной контейнер m s (который должен быть Monoid s) и возвращает тот же тип Monoid . Краткий обзор: тип может быть Monoid если существует нейтральное значение этого типа и операция, которая принимает два значения и производит только одно. Применение этой функции с одним из аргументов, имеющих нейтральное значение, приводит к другому аргументу. String ( [Char] ) — хороший пример с пустой строкой, являющейся нейтральным значением ( mempty ), и конкатенацией строк, являющейся такой операцией ( mappend ). Обратите внимание, что существует два различных способа построения моноидов для чисел: при сложении с нейтральным значением, равным 0 ( x + 0 == 0 + x == x для любого x ), и при умножении на нейтральное 1 ( x * 1 == 1 * x == x для любого x ). Давайте придерживаться строк. Если я сверну пустой список строк, я получу пустую строку. Но когда список содержит много элементов, они объединяются:

1
2
3
4
5
6
> fold ([] :: [String])
""
> fold [] :: String
""
> fold ["foo", "bar"]
"foobar"

В первом примере мы должны явно сказать, что является типом пустого списка [] . В противном случае компилятор Haskell не сможет выяснить, какой тип элементов в списке, таким образом, какой моноидный экземпляр выбрать. Во втором примере мы объявляем, что все, что возвращается из fold [] , должно быть String . Из этого компилятор делает вывод, что [] самом деле должен иметь тип [String] . Последнее fold является самым простым: программа сворачивает элементы в списке и объединяет их, потому что объединение — это операция, определенная в экземпляре класса типов Monoid Monoid String .

Вернуться к настройкам (точнее Maybe ). Свертывание поверх монады Maybe с параметром типа Monoid (не могу поверить, что я только что это сказал) имеет интересную интерпретацию: оно либо возвращает значение внутри Maybe либо значение Monoid умолчанию:

1
2
3
4
> fold (Just "abc")
"abc"
> fold Nothing :: String
""

Just "abc" такой же, как Some("abc") в Scala. Здесь вы можете увидеть, что если Maybe String равно Nothing , возвращается моноидное значение нейтральной String , то есть пустая строка.

Резюме

Haskell показывает, что сворачивание (также поверх Maybe ) может быть, по крайней мере, последовательным. В Scala Option.fold имеет отношения к List.fold , запутан и нечитаем. Я советую избегать этого и оставаться с более подробными преобразованиями map / getOrElse или сопоставлением с образцом.

PS: я упоминал, что есть также Either.fold() (даже с другим контрактом), но нет Try.fold() ?

Ссылка: Option.fold () считается нечитаемым от нашего партнера по JCG Томаша Нуркевича из блога Java и соседей .