Category: эзотерика

Category was added automatically. Read all entries about "эзотерика".

Scalaz для сельских механизаторов - 2

В предыдущей части я немного увлёкся и начал рассказывать про стрелки Клейсли, хотя прежде следует рассказать, зачем нужна композиция "монадичных" функций (т.е. тех, которые возвращают монаду).

Любые замечания и исправления приветствуются.

--

Композиция нечистых функций, Ленивость и Separation of Concerns

Итак в предыдущей части мы увидели, как можно превратить "нечистую" функцию в "чистую", если переписать её так, чтобы она возвращала монаду. Например функция, которая может вернуть ошибку типа E вместо значения типа А, должна возвращать E\/A. По типу возвращаемой монады легко определить, что за "эффект" производит функция, и трудно его проигноировать в коде.

Мы также видели, что функции, которые возвращает одну и ту же монаду легко "композируются"
val amb: A => M[B] = ...
val bmc: B => M[C] = ...
val amc: A => M[C] = a => amb(a) flatMap bmc
Однако возникает вопрос: а зачем композировать amb и bmc и создавать новую функцию amc. Не является ли эта функция amc лишней ?

Для ответа рассмотрим конкретный пример: допустим, нам нужно написать библиотечку для вызова REST сервисов. У нас есть функция для простого синхронного HTTP вызова:
val invoke: HttpRequest => Throwable\/HttpResponse = ...
У нас также есть функции:
val encodeA: А => HttpRequest = ...
val encodeC: C => HttpRequest = ...
а также:
val decodeB: HttpResponse => B = ...
val decodeD: HttpRepsonse => D = ...
Нам удобно их скомпозировать, чтобы получить функции вида
type Service[X, Y] = X => Throwable\/Y
которые станут интерфейсом нашей библиотечки.
val ab: A => Throwable\/B = a => 
  for {
    req  <- encodeA(a).right; 
    resp <- invoke(req); 
    b    <- decodeB(resp) 
  } yield b

val bc: B => Throwable\/C = ...
Если требуется вызвать сначала ac, а потом bc, то это можно скомпозировать так:
val ac: A => Throwable\/C = a => for { b <- ab(a); c <- bc(b) } yield c
Теперь для аппликации, которая вызывает эти интерфейсы, можно написать одну универсальную функцию invokeAndHandleErrors[X, Y], которая исполняет X => Throwable\/Y и обрабатывает Throwables. Обработка Throwable т.е. обработка эффекта, специфична именно для вызывающей стороны: например запись в лог, сообщение системе мониторинга и проч. Вся эта обработка ортогонально нашей библиотечке.

Таким образом с помощью композиции функций мы смогли разделить сами по себе REST вызовы и обработку ошибок этих вызовов с использованием специфичных для вызывающей стороны средств.

Scalaz для сельских механизаторов

Меня тут попросили выступить перед сельскими механизаторами программистами на джаве/скале с презентацией про scalaz. У аудитории (как и у меня) нет математического образования, а у большинства вообще нет никакого кроме армейских курсов. Идея презентации -- показать как абстракции из scalaz (а именно моноиды, функторы, монады, аппликативные функторы и стрелки Клейсли) помогают писать "хороший", с точки зрения הנדסת תוכנה, код.

Просьба покритиковать нижеследующую презентацию:

---

Итак, что значит "хороший" код ? Это в частности код который состоит из небольшого количества примитивов и правил для их композиции. Так, что эти примитивы можно "композировать" при помощи этих правил, чтобы построить всё, что нужно.

Композиция Функций

Вот например "чистая функция". Такие функции легко композировать с помощью compose. Скажем, функции ab: A => B и bc: B => C можно скомпозировать, чтобы получить ac: A => C = bc compose ab.

А что делать, если функции ab и boc "нечистые" ? Например определены не для всех аргументов. Как скомпозировать aob: A => Option[B] и boc: B => Option[C] ?

val aoc: A => Option[C] = a => aob(a) flatMap boc

(здесь нужен какой-нибудь практический пример)

А как написать compose, чтобы скомпозировать эти функции, опуская аргументы ?

def compose[A, B, C](aob: A => Option[B], aob: B => Option[C]): A => Option[C] = ...

Легко заметить, что это наша функция compose использует только flatMap метод Option'а. То есть нашу compose можно обобщить для любого M[_], у которого есть flatMap.

val amb: A => M[B] = ...
val bmc: A => M[C] = ...
val amc: A => M[C] = bmc compose amb

(такой способ композиции называется "бесточечным")

К сожалению этот пример не скомпилируется, но мы скоро увидим, как такую композицию можно написать на scalaz.

Стрелки Клейсли и Монады

Чтобы наш compose был совсем похож на настоящий compose для чистых функций нам нужно, чтобы он был ассоциативен.

(здесь нужен какой-нибудь практический пример)

А кроме того, нам нужно уметь превращать "чистую функцию" A => B в A => M[B], чтобы композировать её с B => M[C]

(здесь нужен какой-нибудь практический пример)

Из этих двух требований -- ассоциативности compose и превращения чистой функции A => B в A => M[B] -- следуют требования к flatMap, которые называются "монадными законами", а M[_] с flatMap, удовлетворяющей этим законам, называется "монадой".

Функции вида A => M[B] называются "стрелками Клейсли", и в scalaz для них есть специальная "обёрткa":

class Kleisli[M[_], A, B](run: A => M[B]) { def compose[C](other: Kleisli[M, B, C])(implicit M: Monad[M]): Kleisli[M, A, C] = ... }

Обратите внимание на использование implicit argument Monad[M]. Это трюк называется "тайпклассом", и вся scalaz на нём построена. Как видим, монада и прочие вышеперечисленные абстракции (моноид, функтор и проч.) имплементированы в scalaz как тайпклассы. Наверное, можно сказать, что scalaz по сути и есть набор тайпклассов.

(вероятно тут нужно рассказать об тайпклассах поподробнее)

И вот теперь мы сможем, наконец, скомпозировать aob: A => Option[B] и boc: B => Option[C] при помощи Kleisli[Option, A, B]

val aob: Kleisli[Option, A, B] = Kleisli {a => ... // Option[B] } // A => Option[B]
val boc: Kleisli[Option, B, C] = Kleisli {b => ... // Option[C] } // B => Option[C]
val aoc = boc compose aob // Kleisli[Option, A, C]

Продолжение следует

Тут, наверное, возникает закономерный вопрос, a на хера козе баян неужели scalaz нужна только для "бесточечной" записи.
Попробуем ответить на него в следующей части.