Skip to content

Instantly share code, notes, and snippets.

@p-pavel
Last active June 8, 2019 10:24
Show Gist options
  • Save p-pavel/54ebfd50d9568eb3158efd77a7d821a8 to your computer and use it in GitHub Desktop.
Save p-pavel/54ebfd50d9568eb3158efd77a7d821a8 to your computer and use it in GitHub Desktop.
О свободных монадах
/*
О Свободной Алгебре.
--------------------
Возможно, кому-то это будет интересно, или даже проложит путь в 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