- PVSM.RU - https://www.pvsm.ru -
В последнее время у меня было несколько разговоров с друзьями из Java мира об их опыте использования Scala. Большинство использовали Scala, как улучшенную Java и, в итоге, были разочарованы. Основная критика была направлена но то, что Scala слишком мощный язык с высоким уровнем свободы, где одно и тоже можно реализовать различными способами. Ну и вишенкой на торте недовольства являются, конечно же, implicit'ы. Я соглашусь, что implicit'ы одна из самых спорных фич языка, особенно для новичков. Само название «неявные», как бы намекает. В неопытных руках implicit'ы могут стать причиной плохого дизайна приложения и множества ошибок. Я думаю каждый, работающий со Scala, хотя бы раз сталкивался с ошибками разрешения ипмлиситных зависимостей и первые мысли были что делать? куда смотреть? как решить проблему? В результате приходилось гуглить или даже читать документацию к библиотеке, если она есть, конечно же. Обычно решение находится импортом необходимых зависимостей и проблема забывается до следующего раза.
В этом посте я бы хотел рассказать о некоторых распространенных практиках использования имплиситов и помочь их сделать более «явными» и понятными. Наиболее распространенные варианты их использования:
В сети много статей, документации и докладов, посвященных этой теме. Я, однако, хотел бы остановиться на их практическом применении на примере создания Scala-friendly API для замечательной Java библиотеки Typesafe Lightbend Config [1]. Для начала нужно ответить на вопрос, а что, собственно, не так с родным API? Давайте взглянем на пример из документации.
import com.typesafe.config.ConfigFactory
val conf = ConfigFactory.load();
val foo = config.getString("simple-lib.foo")
val bar = config.getInt("simple-lib.bar")
Я вижу здесь, как минимум, две проблемы:
getInt
не сможет вернуть значение нужного типа, то будет брошено исключение. А мы хотим писать «чистый» код, без исключений.Давайте начнем со второй проблемы. Стандартное Java решение — наследование. Мы можем расширить функциональность базового класса путем добавления новых методов. Обычно это не является проблемой, если вы владеете кодом, но что делать если это сторонняя библиотека? «Наивный» путь решения в Scala будет через использование неявных классов или «Pimp My Library» паттерна.
implicit class RichConfig(val config: Config) extends AnyVal {
def getLocalDate(path: String): LocalDate = LocalDate.parse(config.getString(path), DateTimeFormatter.ISO_DATE)
}
Теперь мы можем использовать метод getLocalDate
, как если бы он был определен в исходном классе. Неплохо. Но мы решили проблему только локально и мы должны поддерживать всю новую функциональность в одном RichConfig
классе или потенциально иметь ошибку «Ambiguous implicit values», если одинаковые методы будут определены в разных неявных классах.
Можно ли как-то это улучшить? Здесь давайте вспомним, что обычно в Java, наследование используется для реализации полиморфизма. На самом деле, полиморфизм бывает разных видов:
Наследование используется для реализации полиморфизма подтипов. Нас же интересует ad hoc полиморфизм. Он означает, что мы будем использовать другую реализацию в зависимости от типа параметра. В Java это реализуется при помощи перегрузки методов. В Scala его можно дополнительно реализовать при помощи тайп классов. Эта концепция пришла из Haskel, где является встроенной в язык, а в Scala это паттерн, который требует implicit'ов для реализации. Если описать вкратце, то тайп класс — это некоторый контракт, например трейт Foo[T]
, параметризованный типом T
, который используется в разрешении неявных зависимостей и нужная имплементация контракта выбирается по типу. Звучит запутано, но на самом деле это просто.
Давайте рассмотрим на примере. Для нашего случая, определим контракт для чтения значения из конфига:
trait Reader[A] {
def read(config: Config, path: String): Either[Throwable, A]
}
Как мы видим, трейт Reader
параметризирован типом A
. Для решения первой проблемы мы возвращаем Either
. Больше никаких исключений. Для упрощения кода можем написать тайп алиас.
trait Reader[A] {
def read(config: Config, path: String): Reader.Result[A]
}
object Reader {
type Result[A] = Either[Throwable, A]
def apply[A](read: (Config, String) => A): Reader[A] = new Reader[A] {
def read[A](config: Config, path: String): Result[A] = Try(read(config, path)).toEither
}
implicit val intReader = Reader[Int]((config: Config, path: String) => config.getInt(path))
implicit val stringReader = Reader[String]((config: Config, path: String) => config.getString(path))
implicit val localDateReader = Reader[LocalDate]((config: Config, path: String) => LocalDate.parse(config.getString(path), DateTimeFormatter.ISO_DATE);)
}
Мы определили тайп класс Reader и добавили несколько реализаций для типов Int
, String
, LocalDate
. Теперь нужно научить Config
работать с нашим тайп классом. И здесь уже пригодится «Pimp My Library» паттерн и неявные аргументы:
implicit class ConfigSyntax(config: Config) extends AnyVal {
def as[A](path: String)(implicit reader: Reader[A]): Reader.Result[A] = reader.read(config, path)
}
Мы можем переписать более кратко при помощи ограничения контекста(context bounds):
implicit class ConfigSyntax(config: Config) extends AnyVal {
def as[A : Reader](path: String): Reader.Result[A] = implicitly[Reader[A]].read(config, path)
}
И теперь, пример использования:
val foo = config.as[String]("simple-lib.foo")
val bar = config.as[Int]("simple-lib.bar")
Тайп классы — очень мощный механизм, который позволяет писать легко расширяемый код. Если требуется поддержка новых типов, то можно просто написать реализацию нужного тайп класса и поместить её в контекст. Также, используя приоритет [2] в разрешении неявных зависимостей, можно переопределять стандартную реализацию. Например, можно определить другой вариант LocalDate
ридера:
implicit val localDateReader2 = Reader[LocalDate]((config: Config, path: String) =>
Instant
.ofEpochMilli(config.getLong(path))
.atZone(ZoneId.systemDefault())
.toLocalDate()
)
Как мы видим, implicit'ы, при правильном использовании, позволяют писать чистый и расширяемый код. Они позволяют расширить функциональность сторонних библиотек, без изменения исходного кода. Позволяют писать обобщённый код и использовать ad hoc полиморфизм при помощи тайп классов. Нет необходимости беспокоиться о сложной иерархии классов, можно просто разделить функциональность на части и реализовывать их отдельно. Принцип разделяй и властвуй в действии.
Github [3] проект с примерами.
Автор: andr1983
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/funktsional-noe-programmirovanie/278489
Ссылки в тексте:
[1] Lightbend Config: https://github.com/lightbend/config
[2] приоритет: https://www.scala-lang.org/files/archive/spec/2.12/06-expressions.html#overloading-resolution
[3] Github : https://github.com/andr83/scalaconfig
[4] Источник: https://habrahabr.ru/post/354028/?utm_source=habrahabr&utm_medium=rss&utm_campaign=354028
Нажмите здесь для печати.