С нуля до распределенных приложений.
Рассмотрим, казалось бы, простую проблему работы с опциональными значениями. Допустим, у нас есть таблица данных о людях с обязательным полем name и опциональными полями nickname, height и weight.
Представить такой объект можно следующим классом:
case class Person(name: String, nickname: String, height: Double, weight: Double)
В этом примере опциональность полей не выражена в типах и может поддерживаться только негласными соглашениями. Например:
Клиентский код, использующий этот объект, всегда находится под угрозой NullPointerException и многих других:
def isTall(p: Person): Boolean =
if (p.height != 0.0) p.height > 1.9
else ??? // что возвращать тут?
// null pointer exception, забыли проверить на null
def nicknameLength(p: Person) = p.nickname.length
// division by zero exception, забыли проверить на 0
def calcBMI(p: Person) = p.weight / (p.height * p.height)
Попробуем ввести понятие “возможно отсутствующего значения” в систему типов.
abstract class Maybe[+T]
case class Just[T](get: T) extends Maybe[T]
case object Not extends Maybe[Nothing]
// Nothing - это специальный тип в Scala, являющийся производным от всех остальных типов
Тогда указанные выше примеры можно переписать так:
case class Person(
name: String,
nickname: Maybe[String],
height: Maybe[Double],
weight: Maybe[Double])
def isTall(p: Person): Maybe[Boolean] = p.height match {
case Just(h) => Just(h > 1.9)
case Not => Not
}
def nicknameLength(p: Person): Maybe[Int] = p.nickname match {
case Just(nickname) => Just(nickname.length)
case Not => Not
}
Отметим сходство реализации этих методов. Их можно упростить добавив метод map:
abstract class Maybe[+T] {
def map[U](f: T => U): Maybe[U]
}
case class Just[T](get: T) extends Maybe[T] {
def map[U](f: T => U) = Just(f(get))
}
case object Not extends Maybe[Nothing] {
def map[U](f: Nothing => U) = Not // всегда остается Not
}
def isTall(p: Person): Maybe[Boolean] = p.height.map(_ > 1.9)
def nicknameLength(p: Person): Maybe[Int] = p.nickname.map{_.length}
def calcBMI(p: Person): Maybe[Maybe[Double]] =
p.weight.map { w =>
p.height.map { h => w / (h * h) }
}
Как избавиться от вложенного Maybe? Для этого нам понадобится добавление метода flatMap в реализацию:
// Maybe[+T]
def flatMap[U](f: T => Maybe[U]): Maybe[U]
// Just[T]
def flatMap[U](f: T => Maybe[U]): Maybe[U] = f(get)
// Not
def flatMap[U](f: Nothing => Maybe[U]): Maybe[U] = Not
def calcBMI(p: Person): Maybe[Double] =
p.weight.flatMap { w =>
p.height.map { h => w / (h * h) }
}
Отметим, что применяя эти операции мы сохраняем контекст (возможное отсутствие значения).
Корни понятия уходят в теорию категорий.
Описанный выше класс Maybe явлется примером монады в Scala.
Монада - это способ решения следующей проблемы: если у нас есть значение A в контексте M, то как нам работать с этим значением не теряя контекста.
Попросту говоря, в Scala монадой называется тип M[A] у которого определены методы map и flatMap со следующими сигнатурами:
def map[B](f: A => B): M[B]
def flatMap[B](f: A => M[B]): M[B]
В примерах выше контекстом M является класс Maybe.
Самые распространенные примеры монад в языке Scala:
Названия методов map и flatMap в примере с Maybe выше выбраны неспроста.
Scala позволяет работать с ними при помощи выражения for:
val optionalX: Maybe[Int] = Just(5)
val optionalY: Maybe[Int] = Just(10)
val optionalZ: Maybe[Int] = Just(12)
val b: Maybe[Boolean] = for {
x <- optionalX
y <- optionalY
z <- optionalZ
} yield (x + y) > z
Это разворачивается компилятором в следующее выражение:
val b: Maybe[Boolean] =
optionalX.flatMap{ x =>
optionalY.flatMap { y =>
optionalZ.map(z => (x + y) > z)
}
}
По правилам:
Выражения “сохраняющие контекст” встречаются в Scala повсеместно и for-выражения - общепринятое средство работы с ними. Хоть и не сразу кажущееся практичным, понимание монад будет впоследствии очень полезно в функциональном программировании, особенно в проектировании библиотек.