Недавно во время проверки кода мы долго обсуждали, является ли 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 и соседей . |