Last active
June 8, 2019 10:24
-
-
Save p-pavel/54ebfd50d9568eb3158efd77a7d821a8 to your computer and use it in GitHub Desktop.
О свободных монадах
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
О Свободной Алгебре. | |
-------------------- | |
Возможно, кому-то это будет интересно, или даже проложит путь в Scala, которая умеет в котов. | |
Проблема: существует callback из внешнего мира, который передает нам DirectBuffer | |
DirectBuffer это, грубо говоря, кусок памяти. Он предоставляет удобные операции для доступа к этому куску. | |
Но сам кусок не будет валиден после возврата из коллбэка. | |
Раздавать направо и налево (складывать в переменные и т.п.) этот DirectBuffer не хотелось бы (люди передадут | |
дальше по потоку управления, сложат в переменную и т.д., люди злые) | |
Какое решение? В установщик callback нужно передавать "программу действий" -- что сделать с поступившим | |
DirectBuffer. | |
Что такое программа? Программа -- это монада с некоторым дополнительным набором операций. В рамках программы | |
можно читать из DirectBuffer (у нас есть операции для этого, см. getStringUtf8, но нет доступа к самому | |
DirectBuffer. Для выполнения программы нужен DirectBuffer, но в результате выполнения остаются только эффекты | |
программы, сам DirectBuffer программа никогда не видит. | |
Обработчик callback получает программу действий, интерпретирует её, предоставив DirectBuffer. Возвращает эффект | |
программы. | |
Ниже -- как сделать возможным писать программы над DirectBuffer (free monad on functor). | |
Разумеется, подход очень общий, с точностью до набора операций. Полиморфизм по DirectBuffer оставляется в качестве | |
упражнения. В конце концов -- это Scala, Scalable language. сделай объект, сделай трейт, добавь параметр типа в трейт. | |
Использованные концепции (помимо DirectBuffer из agrona) | |
- коты (cats ) | |
- свободно-бесплатные коты (cats-free ) | |
- эффектные коты (cats-effects) | |
Ну и небольшое улучшательство к scala -- kind projections. | |
Кто заставит это скомпилироваться -- пойдет дальше по коду и ссылкам :) | |
Чтобы это заработало, нужно: | |
scalacOptions ++= Seq( | |
"-Ypartial-unification", // Говорят, что нужно, но я пока не столкнулся. Частичное применение типов | |
"-language:higherKinds", // Любая система начинается с import language.higherKinds, но в каждом файле лень | |
"-feature", // Что опять я такого нехорошего сделал? | |
"-Xfatal-warnings", // Я очень серьезно отношусь к warnings | |
) | |
// Ибо задолбало. Позволяет писать лямбда-выражения над типами. Должно быть в любом проекте | |
resolvers += Resolver.sonatypeRepo("releases") | |
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.10.1") | |
// Можно получить DirectBuffer непосредственно из argrona, но мы не ищем лёгких путей | |
libraryDependencies += "io.aeron" % "aeron-all" % "1.17.0" | |
// Ядро котов затаскивает cats-effects | |
libraryDependencies += "org.typelevel" %% "cats-effect" % "1.3.0" // эффектные коты | |
libraryDependencies += "org.typelevel" %% "cats-free" % "2.0.0-M1" // свободно бесплатные коты | |
P.S. спасибо товарищам из https://typelevel.org за наше счастливое детство | |
*/ | |
import cats.{Applicative, Monad, ~>} | |
import cats.arrow.FunctionK | |
import cats.data.{Kleisli, ReaderT} | |
import cats.effect.{IO, Sync} | |
import cats.free.FreeT | |
import org.agrona.DirectBuffer | |
import DirectBufferProg._ | |
/** Программа действий над DirectBuffer с эффектами в F[_] */ | |
final case class DirectBufferProg[F[_],A](private val free: FreeOp[F,A]) extends AnyVal { | |
/** Если в F возможно delay, то любая программа может быть преобразована в ReaderT[F,DirectBuffer,A] */ | |
def foldMap(implicit F: Sync[F]): DirectBufferReader[F,A] = free.foldMap(functionK) | |
/** Если предоставить [[org.agrona.DirectBuffer]], то мы можем выполнить программу */ | |
def run(d: DirectBuffer)(implicit F: Sync[F]): F[A] = foldMap.run(d) | |
} | |
object DirectBufferProg { | |
/** Monad transformer: преобразует эффект F в функцию, принимающую DirectBuffer и возвращающую эффект в F */ | |
type DirectBufferReader[F[_],A] = ReaderT[F,DirectBuffer,A] | |
/** Некоторая операция в алгебре манипуляций с [[org.agrona.DirectBuffer]] */ | |
sealed trait Op[A] { | |
/** Любую операцию можно выполнить, если иметь DirectBuffer */ | |
def compile[F[_]:Sync]: DirectBufferReader[F,A] | |
} | |
/** Таким образом мы создаем монаду из операций [[Op]] -- используем monad transformer FreeT */ | |
type FreeOp[F[_],A] = FreeT[Op, DirectBufferReader[F,?], A] | |
/** | |
* Для любого F, в котором существует delay, существует натуральное преобразование [[Op]] ~> [[DirectBufferReader]][F,?] | |
*/ | |
def functionK[F[_]: Sync]: Op ~> DirectBufferReader[F, ?] = | |
new FunctionK[Op, DirectBufferReader[F, ?]] { | |
override def apply[A](fa: Op[A]): DirectBufferReader[F, A] = fa.compile | |
} | |
/** Получить способ формирования программ операций над [[org.agrona.DirectBuffer]] в некотором эффекте F */ | |
def apply[F[_]: Sync]: Ops[F]= new Ops[F](implicitly) | |
/** Способ формирвания [[apidae.fs2aeron.DirectBufferProg]] для конкретного F */ | |
class Ops[F[_]](val F: Sync[F]) extends AnyVal { | |
private implicit def sync: Sync[F] = F | |
private def liftOp[A](op: Op[A]): DirectBufferProg[F, A] = DirectBufferProg(FreeT.liftF[Op, DirectBufferReader[F, ?], A](op)) | |
private def liftImpure[A](f: DirectBuffer ⇒ A): DirectBufferProg[F, A] = liftOp[A](new Op[A] { | |
override def compile[G[_]: Sync]: DirectBufferReader[G, A] = Kleisli(d ⇒ Sync[G].delay(f(d))) | |
}) | |
/** Затащить не обремененный манипуляциями с DirectBuffer эффект в нашу программу */ | |
def liftF[A](fa: F[A]): DirectBufferProg[F, A] = DirectBufferProg(FreeT.liftT(Kleisli.liftF(fa))) | |
// ============= Трансляция операций DirectBuffer ==================== | |
def getStringUtf8(pos: Int): DirectBufferProg[F, String] = liftImpure(_.getStringUtf8(pos)) | |
def getInt(pos: Int): DirectBufferProg[F,Int] = liftImpure(_.getInt(pos)) | |
} | |
/** Monad is simply unpack and repack into [[DirectBufferProg]] */ | |
//TODO сделать через натуральные преобразования? | |
implicit def monad[F[_]:Applicative]: Monad[DirectBufferProg[F,?]] = new Monad[DirectBufferProg[F, ?]] { | |
private val mon = Monad[FreeOp[F,?]] | |
override def flatMap[A, B](fa: DirectBufferProg[F, A])(f: A ⇒ DirectBufferProg[F, B]): DirectBufferProg[F, B] = | |
DirectBufferProg(fa.free.flatMap(a ⇒ f(a).free)) | |
override def tailRecM[A, B](a: A)(f: A ⇒ DirectBufferProg[F, Either[A, B]]): DirectBufferProg[F, B] = { | |
DirectBufferProg(mon.tailRecM(a)(a ⇒ f(a).free)) | |
} | |
override def pure[A](x: A): DirectBufferProg[F, A] = DirectBufferProg(FreeT.pure(x)) | |
} | |
def print(a: Any): IO[Unit] = IO.delay(println(a)) | |
private val ioOps = DirectBufferProg[IO] | |
import cats.syntax.all._ | |
def exampleProg: DirectBufferProg[IO, String] = | |
for { | |
s1 ← ioOps.getStringUtf8(0) | |
_ ← ioOps.liftF(print("Got the first string")) | |
s2 ← ioOps.getStringUtf8(0) | |
_ ← ioOps.liftF(print("And even got the second one")) | |
} yield s"$s1 а потом $s2" | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment