CodeFest 2018. Роберт Губин (2ГИС) — Непрерывный рефакторинг и функциональное программирование

CodeFest 2018. Роберт Губин (2ГИС) — Непрерывный рефакторинг и функциональное программирование

Посмотрите выступление Роберта: https://2018.codefest.ru/lecture/1274/

В настоящее время существуют люди и даже целые компании, которые рассматривают код как некий актив или священную корову. То, к чему лучше не прикасаться, пока оно работает. Я считаю такой подход в корне неверным. Код — лишь инструмент в достижении целей компании. Активом является непосредственная функциональность приложения, следовательно скорость доставки и качество этой функциональности — основная цель программирования.

В докладе я расскажу, как перестать бояться и начать рефакторить, как функциональная парадигма может в этом помочь, и почему вам не нужна Scala.

16b6c87229eaf58768d25ed7b2bbbf52?s=128

CodeFest

April 05, 2018
Tweet

Transcript

  1. Непрерывный рефакторинг и функциональное программирование Или как мы вляпались в

    Scala Роберт Губин Scala-разработчик 2ГИС
  2. None
  3. None
  4. None
  5. None
  6. None
  7. map fmap flatMap bind

  8. cat path | grep val | wc -l

  9. type File[T] = Option[T] def cat(path: String): File[List[String]] = Some(Source.fromFile(path).getLines.toList)

    def grep(pattern: String)(lines: List[String]): File[List[String]] = Some(lines.filter(_.contains(pattern))) def wc(lines: List[String]): File[Int] = Some(lines.size) cat(path) flatMap grep("val") flatMap wc
  10. cat path | grep val | wc -l cat(path) flatMap

    grep("val") flatMap wc
  11. <?php $trash = [ null, 1, 2 ]; $numbers =

    []; foreach ($trash as $n) { if ($n) { $numbers[] = $n + 1; } } List(None, Some(1), Some(2)) .flatMap{ numberOption => numberOption.map(n => n + 1) }
  12. None
  13. case class Response(ids: List[Long]) case class User(id: Long, name: String,

    age: Int) def isAdult(u: User): Boolean = u.age >= 18
  14. Катаемся на самолетах

  15. def someRemoteCall(param: Int): Response = { if (param == 1)

    { Response(List(1l ,2l ,3l)) } else if (param ==2 ) { Response(List(4l ,5l ,6l)) } else { throw new RuntimeException("Everything blown up.") } }
  16. def dbCall(ids: mutable.MutableList[Long]): mutable.MutableList[User] = { if (ids.nonEmpty) { val

    users = List(User(1l, "Billy", 32), User(6l, "Dolly", 11)) .map(user => (user.id, user)).toMap ids.flatMap(users.get) } else { throw new RuntimeException("No ids, no users.") } }
  17. val response1 = someRemoteCall(1) val response2 = someRemoteCall(2) val idList

    = mutable.MutableList[Long]() idList ++= response1.ids idList ++= response2.ids val users = dbCall(idList)
  18. val adults = mutable.MutableList[User]() for { user <- users }

    yield { if (isAdult(user)) { adults += user } }
  19. — Мутабельность

  20. — Мутабельность — Отсутствие информации об ошибках в типах

  21. — Мутабельность — Отсутствие информации об ошибках в типах —

    Синхронность
  22. Заменим мутабельное на иммутабельное

  23. def dbCall(ids: List[Long]): List[User] = … val response1 = someRemoteCall(1)

    val response2 = someRemoteCall(2) val idList = response1.ids ++ response2.ids val users = dbCall(idList) val adults = users.filter(isAdult)
  24. Добавим информацию об ошибках в типы

  25. def someRemoteCall(param: Int): Try[Response] = { Try{ ... } }

  26. def dbCall(ids: List[Long]): Try[List[User]] = { Try{ ... } }

  27. val responseTry1 = someRemoteCall(1) val responseTry2 = someRemoteCall(2) val idList

    = responseTry1.flatMap{ response1 => responseTry2.map{ response2 => response1.ids ++ response2.ids } }
  28. val users = idList.flatMap(ids => dbCall(ids)) val adults = users.map(users

    => users.filter(isAdult))
  29. for { response1 <- someRemoteCall(1) response2 <- someRemoteCall(2) users <-

    dbCall(response1.ids ++ response2.ids) } yield { users.filter(isAdult) }
  30. Как-то падает :(

  31. sealed trait Error { val message: String } case class

    RemoteServiceError(message: String) extends Error case class DatabaseError(message: String) extends Error
  32. None
  33. def someRemoteCall(param: Int): Either[Error, Response] = { param match {

    case 1 => Right(Response(List(1l ,2l ,3l))) case 2 => Right(Response(List(4l ,5l ,6l))) case _ => Left(RemoteServiceError("Everything blown up.")) } }
  34. def dbCall(ids: List[Long]): Either[Error, List[User]] = { if (ids.nonEmpty) {

    val users = List(User(1l, "Billy", 32), User(6l, "Dolly", 11)) .map(user => (user.id, user)).toMap Right(ids.flatMap(users.get)) } else { Left(DatabaseError("No ids, no users.")) } }
  35. for { response1 <- someRemoteCall(1) response2 <- someRemoteCall(2) users <-

    dbCall(response1.ids ++ response2.ids) } yield { users.filter(isAdult) }
  36. Синхронненько :(

  37. None
  38. def someRemoteCall(param: Int): Future[Either[Error, Response] ] = { Future{ ...

    } }
  39. def dbCall(ids: List[Long]): Future[Either[Error, List[User]]] = { Future{ ... }

    }
  40. val responseF1 = someRemoteCall(1) val responseF2 = someRemoteCall(2)

  41. for { errOrResponse1 <- responseF1 errOrResponse2 <- responseF2 } yield

    for { response1 <- errOrResponse1 response2 <- errOrResponse2 } yield { dbCall(response1.ids ++ response2.ids) }
  42. adultsFutureEither.flatMap{ case Right(dbResult) => dbResult.map(_.map(_.filter(isAdult))) case Left(e) => Future.successful(Left(e)) }

  43. Некрасиво :(

  44. Пора взлетать понемногу

  45. None
  46. def someRemoteCall(param: Int): EitherT[Future, Error, Response] ] = { EitherT

    { Future { ... } } }
  47. def dbCall(ids: List[Long]): EitherT[Future, Error, List[User]]] = { EitherT {

    Future { ... } } }
  48. val responseET1 = someRemoteCall(1) val responseET2 = someRemoteCall(2)

  49. for { response1 <- responseET1 response2 <- responseET2 dbResult <-

    dbCall(response1.ids ++ response2.ids) } yield { dbResult.filter(isAdult) }
  50. А что это за проверка на пустоту?

  51. def dbCall(ids: List[Long]): EitherT[Future, Error, List[User]] = { ... if

    (ids.nonEmpty) { ... } else { ... } ... }
  52. def dbCall(ids: NonEmptyList[Long]): Future[List[User]] = { val users = List(User(1l,

    "Billy", 32), User(6l, "Dolly", 11)) .map(user => (user.id, user)).toMap ids.map(usersMap.get).collect { case Some(x) => x } }
  53. for { response1 <- responseET1 response2 <- responseET2 dbResultOpt =

    NonEmptyList.fromList(response1.ids ++ response2.ids).traverse(dbCall) dbResult <- EitherT.fromOptionF(dbResultOpt, DatabaseError("...")) } yield { dbResult.filter(isAdult) }
  54. Как-то так

  55. None
  56. None
  57. None
  58. None
  59. «Мы понимаем новые вещи в контексте того, что мы уже

    знаем, а большая часть того, что мы уже знаем — конкретная.» Дэниэл Уиллингэм
  60. None
  61. None
  62. None
  63. Вопросы? r.gubin@2gis.ru https://medium.com/@gubinrobert/ medium: e-mail: Роберт Губин, 2ГИС