Что такое Shapeless, и зачем это нужно?
Взято из README Shapeless:
Shapeless is a type class and dependent type based generic programming library for Scala.
Для меня Shapeless - это инструментарий, который заставляет работать систему типов Scala на вас. Вы можете использовать его для получения более "точных" типов, например, списка статичного размера (списки, размер которых известен на этапе компиляции), также вы можете использовать HList как улучшенный кортеж.
В общем, Shapeless можно использовать для того, чтобы заставить компилятор делать работу за вас, избавиться от некоторого бойлерплейта и получить немного дополнительной типобезопасности.
Где найти документацию?
Ее пока не так много. Wiki - хорошее место для старта, особенно это место. Сообщество старается быть настолько вовлеченным, насколько это возможно, поэтому вы, вероятно, сможете найти помощь на gitter канале. Stackoverflow тоже очень эффективен.
Много примеров можно найти в самом исходном коде Shapeless. Эти примеры специально сделаны для обучения и демонстрируют практически все из Shapeless.
Неполное руководство по возможностям Shapeless:
Здесь приведен список возможностей Shapeless, которые я использую чаще всего, вместе с небольшим описанием и приземленными примерами. Конечно, много чего еще можно описать, но это базовые возможности, которые я сейчас считаю необходимыми в любом нетривиальном проекте.
HList
HList, безусловно, наиболее популярная возможность. HList это List, в котором тип каждого элемента статически известен во время компиляции. Вы можете рассматривать его как "кортеж на стероидах". Прелесть HList в сравнении с кортежами состоит в том, что вы можете найти в нем все необходимые методы, присущие типу List, такие как take, head, tail, map, flatMap, zip и т.д., а также набор методов, специфичных для HList.
Вот небольшой пример:
import shapeless.{ ::, HList, HNil }
case class User(name: String)
val demo = 42 :: "Hello" :: User("Julien") :: HNil
// select находит первый элемент данного типа в HList
// Заметьте, что scalac корректно выведет тип переменной s как String.
val s = demo.select[String] // возвращает "Hello".
demo.select[List[Int]] // Ошибка компиляции. demo не содержит List[Int]
// Снова i корректно выведено как Int
val i = demo.head // возвращает 42.
// Вы также можете использовать сопоставление с образцом с HList
val i :: s :: u :: HNil = demo
// i: Int = 42
// s: String = Hello
// u: User = User(Julien)
HList очень полезный. Может, пока вы этого не осознаете, но поверьте мне, скоро вы будете видеть HList везде. Я уже писал о "практичных" и "менее практичных" вариантах использования. Shapeless также предусматривает способ превращения любого case класса в HList (более подробно об этом позже).
Полиморфные функции
Чтобы объяснить полиморфные функции, давайте начнем с небольшого примера.
Возьмем наш объявленный ранее HList:
val demo = 42 :: "Hello" :: User("Julien") :: HNil
Что произойдет, если вы захотите использовать map на нем?
Первый элемент это Int, второй String, а третий User. Ваша функция map, наверное, будет выглядеть как-то так:
def map[A, B, C](h: Int :: String :: User :: HNil)(f0: Int => A, f1: String => B, f3: User => C)
Но необходимость передавать столько функций, сколько элементов находится в HList - это не практично. Кроме того, объявление map таким образом означает, что нужно будет несколько объявлений map. По одной для HList каждой размерности.
Что, если вы хотите передать функцию map, которая работает с Int, и String, и User и позволяет компилятору применить ее к каждому элементу HList. Что-то вроде этого:
def map[A, B, C](h: Int :: String :: User :: HNil)(f: (Int => A) & (String => B) & (User => C))
Очевидно, что f это полиморфная функция. Интересно, что если вы можете объявить такую функцию, то вы сможете объявить более общую функцию map, работающую для любого h типа H, где H <: HList.
К сожалению, такого оператора "&" в Scala не существует, я его выдумал. Язык предоставляет только мономорфные функции. Вы можете создать функцию, чей домен (тип ее параметров) это Int, но вы не можете создать функцию, чей домен это Int, и String, и User. В общем, вы не можете создать функцию, чей доменный тип это A для любого A. В качестве тривиального упражнения, попробуйте объявить функцию identity, присвоенную переменной, с типом A => A. Это невозможно.
Вернемся сейчас к нашей функции map. Конечно, f может быть функцией, которая обрабатывает минимальную верхнюю грань всех элементов HList. В нашем примере типом f будет Any => Any. В целом, функция такого типа не очень удобна.
Я уже упомянул, что map определена для HList, что означает, что Shapeless предоставляет полиморфные функции. Вот простой пример:
import shapeless.Poly1
object plusOne extends Poly1 {
implicit def caseInt =
at[Int]{ _ + 1 }
implicit def caseString =
at[String]{ _ + 1 }
implicit def caseUser =
at[User]{ case User(name) =>
User(name + 1)
}
}
demo.map(plusOne) // возвращает 43 :: Hello1 :: User(Julien1) :: HNil
object incomplete extends Poly1 {
implicit def caseInt = at[Int]{ _ + 1 }
implicit def caseString = at[String]{ _ + 1 }
}
demo.map(incomplete) // ошибка компиляции. Наша неполная функция не обрабатывает User
Я думаю, код довольно прост для понимания. Заметьте, что полиморфные функции совершенно типобезопасны. Будьте внимательны, чтобы не забыть ключевое слово implicit. Это глупая ошибка, но я делаю ее время от времени. И иногда это отнимает некоторое время, чтобы понять, почему scalac отказывается применять map к HList.
Заметьте, что полиморфная функция может использовать неявные параметры:
import shapeless._
import cats._
import shapeless.ops.hlist._
case class User(name: String)
val demo = 42 :: "Hello" :: User("Julien") :: HNil
implicit val stringShow = Show.fromToString[String] // Show импортирован из Cats
implicit val intShow = Show.fromToString[Int]
implicit val userShow = Show.fromToString[User]
object show extends Poly1 {
implicit def showa[A](implicit s: Show[A]) = at[A]{ a => "Showing " + s.show(a) }
}
demo.map(show) // Showing 42 :: Showing Hello :: Showing User(Julien) :: HNil
Generic
Generic это простой способ конвертировать case классы и типы произведения (product types - например, кортежи) в HList и обратно:
import shapeless.Generic
case class UserWithAge(name: String, age: Int)
val gen = Generic[UserWithAge]
val u = UserWithAge("Julien", 30)
val h = gen.to(u) // возвращает Julien :: 30 :: HNil
gen.from(h) // возвращает UserWithAge("Julien", 30)
Снова код достаточно прост. Generic часто используется для автоматического извлечения экземпляров классов типов для case классов. Посмотрите мой другой пост "Типизируйте все вещи" для примеров из реального мира. Generic это замечательный способ для избежания написания макросов. И это замечательно! Я не хочу поддерживать мои плохо написанные макросы.
Кортежи
Shapeless предоставляет синтаксис для кортежей, так что вы можете использовать методы HList с кортежами.
(1, "foo", 12.3).tail
// :14: error: value tail is not a member of (Int, String, Double)
// (1, "foo", 12.3).tail
import shapeless.syntax.std.tuple._
(1, "foo", 12.3).tail // returns (foo,12.3)
Код довольно очевиден. Большинство методов HList становятся доступными для кортежей путем простого импортирования import shapeless.syntax.std.tuple._. Очень изящно!
Линзы
Shapeless предоставляет простую реализацию линз. Это базовый пример, напрямую взятый из примеров Shapeless:
// Пара обычных case классов ...
case class Address(street : String, city : String, postcode : String)
case class Person(name : String, age : Int, address : Address)
// Линзы для Person/Address ...
val ageLens = lens[Person].age
// Начальное значение
val person = Person("Joe Grey", 37, Address("Southover Street", "Brighton", "BN2 9UA"))
// Прочитать поле
val age1 = ageLens.get(person) // Выведенный тип - Int
typed[Int](age1)
assert(age1 == 37)
// Обновить поле
val person2 = ageLens.set(person)(38)
assert(person2.age == 38)
Если вам время от времени нужны линзы, и в вашем проекте уже есть Shapeless, это может быть удобно. Для более продвинутого использования посмотрите на отдельные библиотеки вроде monocle.
Абстракция над арностью:
Это не совсем специфическая возможность, но основываясь на HList и Generic, Shapeless предоставляет способ создания функций произвольной арности.
Скажем, вы создали класс, который содержит HList.
import shapeless.HList
class MyClass[H <: HList](hs: H)
Возможно, вы не хотите принуждать ваших пользователей к HList. Так, как же вы создаете экземпляры класса MyClass без прямого использования HList? Хорошо, вы можете предоставить набор методов apply:
object MyClass {
def apply[A](a: A) = new MyClass(a :: HNil)
def apply[A, B](a: A, b: B) = new MyClass(a :: b :: HNil)
def apply[A, B, C](a: A, b: B, c: C) = new MyClass(a :: b :: c :: HNil)
// и т.д.
}
Но это довольно надоедливо для написания. Вместо этого, вы можете сделать так:
object MyClass {
import shapeless.Generic
def apply[P <: Product, L <: HList](p: P)(implicit gen: Generic.Aux[P, L]) =
new MyClass[L](gen.to(p))
}
MyClass(1, "Hello") // MyClass[Int :: String :: HNil]
MyClass(1, "Hello", 12.6) // MyClass[Int :: String :: Double :: HNil]
Заметьте, что в действительности вы передаете в метод apply кортеж. С более строгими параметрами компилятора, вам понадобится пара скобок: MyClass((1, "Hello", 12.6)).
Пусть исходники будут с вами, всегда.
Если вы зашли настолько далеко в этом посте, вероятно, вы хотите узнать больше о Shapeless. Поэтому, учитывая малое количество доступной в настоящий момент документации, вам придется прибегнуть к чтению исходного кода для того, чтобы узнать больше. К счастью, ориентироваться в исходниках Shapeless очень просто, однажды поняв, как они организованы.
Навигация по исходникам
Исходники Shapeless разделены на 3:
- /core/src/main/scala/shapeless содержит все определения базовых структур данных, каждое в своем файле. Например, hlist.scala это определение HList.
- /core/src/main/scala/shapeless/ops содержат все классы типов, используемые этими структурами. Снова, каждая структура данных имеет свой файл. hlist.scala содержит все классы типов для HList.
- /core/src/main/scala/shapeless/syntax содержит все методы, пригодные к использованию с каждой структурой данных. И снова каждая структура данных имеет свой файл. hlist.scala содержит все методы, определенные для HList. Если вы хотите посмотреть на определение map для HList, оно здесь.
Этого должно быть достаточно для того, чтобы найти практически все, что вам нужно знать самому.
Понимание исходников
Все в Shapeless (кроме макросов), в основном, работает по схожей модели. Если вы хотите понять, как работает HList, я уже написал об этом в моей статье Typelevel quicksort in Scala. Как только вы поймете HList, поймете и все остальное. Я советую выделить время для того, чтобы понять, как устроен HList, и как вы проходите по HList функцией map, даже если вы не планируете использовать Shapeless.
Оригинал: http://jto.github.io/articles/getting-started-with-shapeless
На момент написания статьи, версия Shapeless 2.3.2.