Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CodeFest 2019. Иван Фастов (Tinkoff.ru) — May the F[_] be with You. Функциональный подход к проектированию API

CodeFest
April 05, 2019

CodeFest 2019. Иван Фастов (Tinkoff.ru) — May the F[_] be with You. Функциональный подход к проектированию API

Про пользу функционального программирования при разработке web-сервисов не слышал, пожалуй, только ленивый. Какие ещё плюсы можем извлечь из компилятора scala?

В этом докладе на примере написания scala REPL бота рассказываем о нашем zero-cost подходе документирования API. Покажем реальную пользу от современных практик ФП на scala:
— размываем грань между ФП и ООП;
— собираем алгоритм из составных частей без ущерба для гибкости приложения;
— Dependency Injection без фрэймворков;
— пишем асинхронный код без асинхронных unit-тестов;
— решаем проблему нэйминга раз и навсегда.

CodeFest

April 05, 2019
Tweet

More Decks by CodeFest

Other Decks in Technology

Transcript

  1. интерактив scaREbot доступен в telegram команды для бота будут на

    слайдах сниппеты можно смотреть в нём 2
  2. без типизации легко let iphone = { price: 1000 };

    let macbook = { price: "2000" }; let items = [ iphone, macbook ]; function total(items) { return items.map(item => item.price).reduce((a, b) => a + b); } // total(items) - "10002000" 4
  3. нужно больше тестов function applyDisounts(discounts, order) { // Что такое

    discounts? // Что такое order? // Как мне это обрабатывать? // Что возвращать в качестве результата? } 5
  4. статическая типизация def fibonacci(n: Int): Int = n match {

    case 0 => 0 case 1 => 1 case n => fibonacci(n-2) + fibonacci(n-1) } val someNumbers: Seq[Int] = 0.to(10).map(fibonacci) // Vector(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55) val position: Int = -1 fibonacci(position) // java.lang.StackOverflowError someNumbers(position) // java.lang.IndexOutOfBoundsException: -1 6
  5. запускаем спутник на scala final case class Meters(value: Double) extends

    AnyVal final case class Newton(value: Double) extends AnyVal final case class Config(force: Newton, distance: Meters) Config(Newton(1.0), Meters(2.0)) Config(Meters(2.0), Newton(1.0)) // Не компилится Config(2.0, 1.0) // Не компилится /units_sample
  6. refined type Name = String Refined And[NonEmpty, Trimmed] type Age

    = Int Refined Interval.Open[W.`16`.T, W.`120`.T] type EmailRegex = W.`"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"`.T type Email = String Refined MatchesRegex[EmailRegex] case class Person(name: Name, age: Age, email: Email) 9
  7. refined много встроенных предикатов можно их комбинироват ь compile-time проверка

    литералов в рантайме не будет выброшено исключение 10 интеграция с (де) сериализаторами и ORM /refined_sample
  8. функция - черный ящик def send(from: User, to: User, message:

    Message): Unit def send(from: User, to: User, message: Message): Unit = { logToDb(from, to, message) doSend(from, to, message) log(???) } def doSend(from: User, to: User, message: Message): Unit = ??? def logToDb(from: User, to: User, message: Message): Unit = ??? 12
  9. эффекты явно в сигнатуре def send( smtp: SmtpService, audit: AuditService,

    log: Logger )( from: User, to: User, message: Message ): Unit 13
  10. implicit спешит на помощь def send(from: User, to: User, message:

    Message)( implicit smtp: SmtpService, audit: AuditService, log: Logger ): Unit = { audit.logToDb(from, to, message) smtp.send(from, to, message) log.log(???) } 14
  11. пример использования // Располагаем в области видимости компилятора implicit val

    smtp: SmtpService = ??? implicit val audit: AuditService = ??? implicit val log: Logger = ??? // Используем без указания явных значений параметров val from: User = ??? val to: User = ??? val message: Message = ??? send(from, to, message) 15
  12. асинхронный мир взаимодействие с сервисами редко бывает синхронным но всё

    же бывает как совмещать и каждый раз адаптировать в асинхронное? асинхронные юнит-тесты иногда ломаются по таймаутам 16
  13. побочные эффекты // Из-за побочных эффектов легко выстрелить себе в

    ногу val performSend = send(from, to, message) def doAction(action: => A) = try { action } catch { case e: Throwable => action } doAction(performSend) != doAction(send(from, to, message)) 17
  14. превращаем код в данные class IO[+A](val unsafeRunSync: () => A)

    object IO { def apply[A](body: => A): IO[A] = new IO[A](() => body) } val printSome = IO(println(123)) printSome.unsafeRunSync() val printSomeMore = IO(println(456)) printSomeMore.unsafeRunSync() 18
  15. слово из 3-х букв class IO[+A](val unsafeRunSync: () => A)

    { def map[B](f: A => B): IO[B] = new IO[B](() => f(unsafeRunSync())) def flatMap[B](f: A => IO[B]): IO[B] = map(f).map(_.unsafeRunSync()) } val program = for { _ <- IO(println(123)) _ <- IO(println(456)) } yield () program // Эффекты не запускаются. Можем передавать как значение куда угодно program.unsafeRunSync() // Вызвать эффекты 19
  16. от конкретного IO к F[_] // Реализация для произвольного typeclass

    trait FlatMap[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] } // Предоставляем синтаксис для for-comprehension implicit class FlatMapOps[A, F[_]: FlatMap](m: F[A]) { private val M = implicitly[FlatMap[F]] def map[B](f: A => B): F[B] = M.map(m)(f) def flatMap[B](f: A => F[B]): F[B] = M.flatMap(m)(f) } /ad_hoc_sample 20
  17. от конкретного IO к F[_] // Реализация для конкретного IO

    implicit val ioInstance: FlatMap[IO] = new FlatMap[IO] { override def map[A, B](fa: IO[A])(f: A => B): IO[B] = new IO[B](() => f(fa.unsafeRunSync())) override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = fa.map(f).map(_.unsafeRunSync()) } 21
  18. знакомый императивный код :) // Реализация выглядит как синхронный код

    def send[F[_]: SmtpService: AuditService: Logger: FlatMap]( from: User, to: User, message: Message ): F[Unit] = for { _ <- AuditService[F].logToDb(from, to, message) _ <- SmtpService[F].send(from, to, message) _ <- Logger[F].log(???) } yield () /io_sample 22
  19. профит можно писать синхронные тесты сигнатура методов описывает поведение программа

    = древовидная структура данных + интерпретатор бизнес-логика отделена от технических деталей referential transparency для облегчения доказательства корректности 23
  20. подходы к документированию схема потом код схема - источник правды

    какие гарантии, что API соответствует схеме? как синхронизировать? код потом схема пишем код генерируем документацию из кода 25
  21. typed schema. зачем? • определение сервиса изолировано от имплементации •

    спецификация экспортируется из определения сервиса • реализация эффектов (future/task/IO) не влияет на API • конкретный фреймворк - это не бизнес- логика • compile-time проверка спецификации определению сервиса 26
  22. как это работает? DSL все элементы этого языка по сути

    просто возвращают типы для построения дерева. def api = get |> operation('hello) |> capture[String]('name) |> $$[String] как работает: • проверяем чтобы метод был GET • URI должен содержать /hello • сохраняем следующую за /hello часть URI под именем name • наш сервис возвращает результат типа String 27
  23. 29

  24. альтернативы tapir, or Typed API descRiptions • чуть больше бойлерплейта

    • описание полей модели задаются в коде • появилось в декабре 2018 ρ: A DSL for building HTTP services with http4s • только http4s 30
  25. итого компилятор - наш главный помощник абстракции могут быть “бесплатными”

    изменение технических аспектов не затрагивает бизнес-логику всегда актуальная документация API 31