Безплатен Monad срещу Tagless Final

В тази публикация ще използвам Cats като моя библиотека по избор за внедряването на Free Monad.

Ще го направя предговор, като кажа, че не съм експерт, но намирам тези неща за интересни и исках да пиша за него.

Това може да е дълъг пост, така че ще скоча веднага. Ще започна със създаване на обикновен api, базиран на бъдещи. Тогава ще създам 2 DSL, интерпретатори и за двете, и след това ще покажа как да смесвам множество алгебри. Ще сравнявам подхода на Free Monad и окончателния подход на begless на всяка стъпка.

Безплатни монади

Техника за кодиране на вашето намерение като типове данни и след това сгъване над тези типове данни, за да се изгради окончателна структура, която ще бъде интерпретирана в известна монада по ваш избор. Самата Свободна монада не е монада, но ви дава възможност да заемате свойства на монади от други добре познати монади. В документацията за котките се казва, че е по-добре от мен:

Конкретно, това е просто умна конструкция, която ни позволява да изградим много проста Monad от всеки функтор

Без финал

Най-добрият начин да опиша тази техника е да се абстрахирам над монадата си по избор. Само за да е ясно, това е модел на кодиране и може да се постигне най-вече с помощта на ванилна скала. От мое разбиране, той се нарича „безпроблемно“, тъй като не са ви необходими маркери за изпълнение, за да изразявате ограничения за типовете на вашата алгебра. Очевидно терминът „безкраен финал“ е вид каламбур по дългото пътуване, за да стигнем до момент, в който маркерите за изпълнение вече не се изискват. Ето по-добро обяснение:

Така нареченият „типизиран финал за безпроблемно“… за представяне на въведени езици от по-висок ред в набран метаезик [вграден DSL], заедно с запазване на типа интерпретация, компилация и частична оценка. Подходът е алтернатива на традиционното, или „първоначалното“ кодиране на езика на обекта като (обобщен) алгебричен тип данни [GADT]. … Крайното кодиране представлява всички и само въведени термини на обекта, без да се прибягва до обобщени типове данни с алгебрични данни (GADT), зависими или други фантастични типове. Крайното кодиране ни позволява да добавяме нови езикови форми и интерпретации, без да нарушаваме съществуващите термини и интерпретатори.

Добре, сега на кода ...

Вашият стандартен файл на build.sbt:

scalacOptions ++ = Seq (
  "-Ypartial обединение"
)
libraryDependitions + = "org.typelevel" %% "котки-ядро"% "1.0.1"
libraryDependitions + = "org.typelevel" %% "без котки"% "1.0.1"

Внос:

импортиране на cat.data. {EitherK, EitherT}
импортиране на котки.безплатно
импортиране на котки. {InjectK, Monad, ~>}
импортиране на cat.implicits._

импортиране на scala.collection.mutable
import scala.concurrent. {Чакай, Бъдеще}
импортиране на scala.concurrent.duration._
импортиране на scala.concurrent.ExecutionContext.Implicits.global

Някои често срещани модели:

case class User (id: Long, име: String, age: Int)

клас DatabaseError разширява Throwable
case object ErrorFindingUser разширява DatabaseError
case object ErrorUpdatingUser разширява DatabaseError
case class ErrorDeletingUser (msg: String) разширява DatabaseError

Първоначалната api, която ще преобразуваме:

черта на база данни [T] {
  def create (t: T): бъдеще [Boolean]
  def read (id: Long): Бъдеще [Или [DatabaseError, T]]
  def delete (id: Long): Future [Или [DatabaseError, Unit]]
}

Добре досега толкова добре ...

Стъпка 1 - Създайте вашите DSL файлове

Без финал

черта DatabaseAlgebra [F [_], T] {
  def create (t: T): F [Булева]
  def read (id: Long): F [Или [DatabaseError, T]]
  def delete (id: Long): F [Или [DatabaseError, Unit]]
}

Това не е толкова лошо Току-що извадихме бъдещето и го преместихме нагоре към декларацията на чертата под формата на F [_] Сега вместо всичко да връща бъдещето [_], то връща F [_] и F може да бъде каквото искаш Monad. Например: Бъдеще, IO, Id, Задача и т.н. ...

Безплатно

/ ** Това е вашият ADT - конкретно, GADT за проницателния четец ;-) * /
запечатана черта DBFreeAlgebraT [T]
case class Create [T] (t: T) разширява DBFreeAlgebraT [Boolean]
case class Read [T] (id: Long) разширява DBFreeAlgebraT [Или [DatabaseError, T]]
case class Delete [T] (id: Long) разширява DBFreeAlgebraT [Или [DatabaseError, Unit]]
/ ************************************************* ******* /

обект DBFreeAlgebraT {
  тип DBFreeAlgebra [T] = Безплатен [DBFreeAlgebraT, T]

  // Умни конструктори
  def create [T] (t: T): DBFreeAlgebra [Boolean] =
    Free.liftF [DBFreeAlgebraT, Boolean] (Създаване (t))

  def read [T] (id: Дълго): DBFreeAlgebra [Или [DatabaseError, T]] =
    Free.liftF [DBFreeAlgebraT, Или [DatabaseError, T]] (Прочетете (id))

  def delete [T] (id: дълго): DBFreeAlgebra [Или [DatabaseError, Unit]] =
    Free.liftF [DBFreeAlgebraT, Или [DatabaseError, Unit]] (Изтриване (id))
}

Whoooo! Стъпка 1 е направена! Имаме си DSL. Както можете да видите, Безплатната реализация има малко повече котлони. Безплатно е кодирането на вашата алгебра като ADT, така че се нуждаем от всички класове. Имаме нужда и от интелигентните конструктори, тъй като нашите класове са твърде специфични. Интелигентните конструктори създават екземпляри от нашите класове на случаите, но типът връщане е по-обобщеният супер тип: DBFreeAlgebraT [T]. Те ще помогнат на скала компилатора с неявни търсения по-късно.

Нищо от това не прави нищо полезно. Трябва да създадем някои интерпретатори, които да интерпретират нашата алгебра в действия.

Стъпка 2 - Създайте преводачи

  • За този раздел ще създадем интерпретатори, които да изпълняват нашите действия, използвайки Futures

Без финал

обект DatabaseAlgebra {

  val FutureInterpreter: DatabaseAlgebra [Бъдеще, потребител] =
    нова база данниAlgebra [Бъдеще, потребител] {
      потребители на val: mutable.Map [Long, User] = mutable.Map.empty

      override def create (потребител: Потребител): Future [Boolean] = {
        val вмъкнат = users.put (user.id, потребител)
        Future.successful (insert.isEmpty || insert.isDefined)
      }

      override def read (id: Long): Future [Или [DatabaseError, User]] =
        Future.successful (users.get (Id) .toRight (ErrorFindingUser))

      override def delete (id: Long): Future [Или [DatabaseError, Unit]] = {
        импортиране на cats.syntax.either._ // за .asLeft []. Това е и друг интелигентен конструктор, който да помогне на компилатора.
        val изтрит = users.remove (id)
        Future.successful (
          delete.fold (ErrorDeletingUser (s „Потребител с Id ($ id) не беше там“). asLeft [Unit]) (_ => Вдясно (())))
      }
    }

}

Това трябва да е доста направо. Буквално отпадаме във Future for F [_] и попълваме изпълнението. За тези, които използват intellij, IDE ви дава основен скелет, просто го попълвате.

Безплатно

обект DBFreeAlgebraT {
  / ** предишен код отгоре отива тук ** /
  / ** предишен код отгоре отива тук ** /
  / ** предишен код отгоре отива тук ** /
    val FutureInterpreter = нов (DBFreeAlgebraT ~> Бъдеще) {
    / **
    * Горното е еквивалентно на подписа по-долу
    * `val FutureInterpreter = нова функцияK [DBFreeAlgebraT, бъдеще]`
    * Това е като функция на стойности от A => B
    * Единствената разлика това работи на "Kinds", следователно "FunctionK"
    * Това основно казва, че създавате функция от F [A] => F [B]
    * Преобразуваме нашата свободна структура (F [A]) в известна Monad (F [B])
    * В този случай ние преобразуваме DBFreeAlgebra [T] => Бъдеще [T]
    * /
        потребители на val: mutable.Map [Long, User] = mutable.Map.empty
    
        отменя дефинирайте прилагането [A] (фа: DBFreeAlgebraT [A]): ​​бъдеще [A] =
          фа мач {
            случай Създаване (потребител) => // F [A]
              val castedUser = user.asInstanceOf [Потребител]
              val вмъкнат = users.put (castedUser.id, castedUser)
              Future.successful (insert.isEmpty || insert.isDefined) .asInstanceOf [Future [A]] // F [B]
            случай Прочетете (id) => // F [A]
              Future.successful (users.get (id) .toRight (ErrorFindingUser)). AsInstanceOf [Future [A]] // F [B]
            изтриване (id) => {// F [A]
              импортиране на котки.syntax.either._
              val изтрит = users.remove (id)
              Future.successful (
                delete.fold (ErrorDeletingUser (s „Потребител с Id ($ id) не беше там“). asLeft [Unit]) (_ => Вдясно (()))
              ) .asInstanceOf [Бъдеще [A]] // F [B]
            }
          }
      }
}

Свободната монада също е права, но малко повече. Трябва да направим малко кастинг по пътя поради ограничената поддръжка на Scala за GADT (.asInstanceOf [Future [A]]). <- Може да греша в това, но грешките на компилатора по-горе казват, че в противен случай ще обясня user.asInstanceOf [User] малко по-късно

Сега, когато кодът ни всъщност може да свърши работа, нека напишем някои репозиции, за да увием нашия DB код от ниско ниво:

Без финал

клас UserRepo [F [_]] (DB: DatabaseAlgebra [F, User]) (неявно M: Monad [F]) {

  def getUser (id: Long): F [Или [DatabaseError, потребител]] = DB.read (id)
  def addUser (потребител: Потребител): F [Boolean] = DB.create (потребител)

  def updateUser (потребител: Потребител): F [Или [DatabaseError, Boolean]] = {
    (за {
      userFromDB <- EitherT (getUser (user.id))
      успешно добавен <- EitherT.liftF [F, DatabaseError, Boolean] (addUser (потребител))
    } добив успешноДобавен). стойност
  }

}

Безплатно

клас FreeUserRepo {
  val DB = DBFreeAlgebraT
  импортира DBFreeAlgebraT.DBFreeAlgebra

  def getUser (id: Long): DBFreeAlgebra [Или [DatabaseError, User]] = DB.read (id) // това е програма
  def addUser (потребител: Потребител): DBFreeAlgebra [Boolean] = DB.create (потребител) // това е програма

// това е програма
  def updateUser (потребител: Потребител): DBFreeAlgebra [Или [DatabaseError, Boolean]] = (за {
    userFromDB <- EitherT (getUser (user.id))
    успешно добавен <- EitherT.liftF [DBFreeAlgebra, DatabaseError, Boolean] (addUser (потребител))
  } добив успешноДобавен). стойност
}

Двете репозиции изглеждат почти еднакви. Забележете, че окончателната версия на tagless приема своя интерпретатор като аргумент, докато безплатната версия не го прави. Едната голяма разлика е, че финалът без етикети всъщност изпълнява вашия код и връща очаквания резултат. Безплатната версия просто изгражда рекурсивна структура. Нещо като Suspend (Suspend (Pure (...)). Не ми цитирайте това. Структурата се разгражда и преминава малко по-късно в основния метод чрез преводача.

Кодът, извикващ всичко това:

Без финал

обект UserRepoRunner разширява приложението {

  val repo = ново UserRepo (DatabaseAlgebra.FutureInterpreter)

  println (Await.result (
    (за {
      _ <- repo.addUser (Потребител (1, "Боб", 31))
      dbErrorOrSuccessfullyUpdated <- repo.updateUser (Потребител (1, "Bobby", 31))
    } добив dbErrorOrSuccessfullyUpdated),
    1 секунда))

}

Безплатно

обект DBFreeAlgebraRunner разширява приложението {

  val repo = ново FreeUserRepo
  
  println (Await.result (
    (за {
      _ <- repo.addUser (Потребител (2, "Боб", 31))
      dbErrorOrSuccessfullyUpdated <- repo.updateUser (Потребител (2, "Bobby", 31))
    } добив dbErrorOrSuccessfullyUpdated) .foldMap (FutureInterpreter), // забележете foldMap (..) тук
    1 секунда))

}

изход:

sbt: sample-blog-code> run

 [1] DBFreeAlgebraRunner
 [2] UserRepoRunner
[info] Опаковане /Users/anthony.garo/git/sample-blog-code/target/scala-2.12/sample-blog-code_2.12-0.1.jar ...
[информация] Готово опаковане.

Въведете номер: 1

[информация] Работещ DBFreeAlgebraRunner
Право (вярно)

sbt: sample-blog-code> run
[предупреди] Открити са няколко основни класа. Изпълнете „покажи откритиMainClasses“, за да видите списъка

Открити са няколко основни класа, изберете един, който да стартирате:

 [1] DBFreeAlgebraRunner
 [2] UserRepoRunner
[info] Опаковане /Users/anthony.garo/git/sample-blog-code/target/scala-2.12/sample-blog-code_2.12-0.1.jar ...
[информация] Готово опаковане.

Въведете номер: 2

[info] Работещ UserRepoRunner
Право (вярно)

Основните методи изглеждат почти идентични. И двамата включват някъде FutureInterpreter. В окончателната версия на Tagless просто го предаваме в репо. В безплатната версия го предаваме, когато прегънем структурата си и я преобразуваме в бъдеще. В безплатната версия нашият код STILL всъщност не прави нищо, докато не се обадите на .foldMap. Обажданията repo.addUser (Потребител (2, "Боб", 31)) и repo.updateUser (Потребител (2, "Боби", 31)) просто изграждат рекурсивна структура на данните. Това може да нарасне много голямо и това е добре, тъй като прилагането на котки е безопасно. Комбинаторът .foldMap сгъва над нашата структура всяка стъпка и преобразува тази стъпка към познатата ни Monad, използвайки FutureInterpreter.

И това е!!!!

Това само по себе си може да ви отведе доста далеч, но в един момент ще трябва да смесите в друга алгебра в програмата си.

Ще изиграя сценария, в който искате да влезете. Това е много измислено, но си представете, че е нещо по-сложно като изпращане на имейл.

Ето как изглежда api / DSL за това:

// ванилия апи:
черта ConsoleAlgebra {
  def putLine [T] (t: T): Единица
}

/ ** Финал на Tagless * /
черта ConsoleAlgebra [F [_]] {
  def putLine [T] (t: T): F [Единица]
}

/** Безплатно */
запечатана черта ConsoleFreeAlgebraT [T]
клас на случая PutLine [T] (t: T) разширява ConsoleFreeAlgebraT [Unit]

преводачи:

/ ** Финал на Tagless * /
обект ConsoleAlgebra {
  неявен обект FutureInterpreter разширява ConsoleAlgebra [Бъдеще] {
    замени def putLine [T] (t: T): бъдеще [единица] = бъдеще.успешно {
      println (т)
    }
  }
}

/** Безплатно */
обект ConsoleFreeAlgebraT {
  тип ConsoleAlgebra [T] = Безплатен [ConsoleFreeAlgebraT, T]

  def putLine [T] (t: T): ConsoleFreeAlgebraT [Unit] = PutLine (t)

  val FutureInterpreter = нов (ConsoleFreeAlgebraT ~> Бъдеще) {
    отмени def прилага [A] (fa: ConsoleFreeAlgebraT [A]): ​​бъдеще [A] =
      фа мач {
        случай PutLine (t) =>
          Future.successful (println (т)). AsInstanceOf [Future [A]]
      }
  }
  
}

Сега ... трудната част. Комбиниране на алгебри:

Без финал

клас UserRepo [F [_]] (DB: DatabaseAlgebra [F, User],
                     В: ConsoleAlgebra [F]) // тук има само разлика
                    (неявно M: Monad [F]) {

  def getUser (id: Long): F [Или [DatabaseError, потребител]] = DB.read (id)
  def addUser (потребител: Потребител): F [Boolean] = DB.create (потребител)

  def updateUser (потребител: Потребител): F [Или [DatabaseError, Boolean]] = {
    (за {
      userFromDB <- EitherT (getUser (user.id))
      _ <- EitherT.liftF (C.putLine (s "Намерихме потребител ($ userFromDB) !!")) // това е ново
      успешно добавен <- EitherT.liftF [F, DatabaseError, Boolean] (addUser (потребител))
    } добив успешноДобавен). стойност
  }

}

Безплатно

С Free е малко по-ангажирано. Тъй като нашите алгебри са множество ADT, трябва да кажем на нашия код, че всеки даден ред код в нашата програма може да бъде от нашата DB алгебра ИЛИ от конзолната алгебра. По същество това е едно от тези. Това може да бъде 2 или повече алгебри. Но преди дори да стигнем до този момент, трябва да добавим малко котло. Трябва да направим нашите различни алгебри да имат общ чадър, под който да попадат: Безплатно [_, _] За това имаме интелигентни конструктори за :-)

/ ** Стъпка 1 - Увийте своите интелигентни конструктори в клас с неявен `InjectK` * /
клас DBFreeAlgebraTI [F [_]] (неявен I: InjectK [DBFreeAlgebraT, F]) {

    / ** Стъпка 2 - вместо да извиквате `.liftF`, вие извиквате` .inject` * /
    def create [T] (t: T): Безплатно [F, Boolean] = // <- Всички алгебри са `Безплатни` сега
      Free.inject [DBFreeAlgebraT, F] (Създаване (t))

    def read [T] (id: Дълго): Безплатно [F, Или [DatabaseError, потребител]] = // <- Всички алгебри са `Безплатни` сега
      Free.inject [DBFreeAlgebraT, F] (Прочетете (id))

    def delete [T] (id: Дълго): Безплатно [F, Или [DatabaseError, Unit]] = // <- Всички алгебри са `Безплатни` сега
      Free.inject [DBFreeAlgebraT, F] (Изтриване (id))
  }
  
  / ** Стъпка 3 - създайте неявен екземпляр от вашия нов клас * /
  неявен деф dBFreeAlgebraTI [F [_]] (неявен I: InjectK [DBFreeAlgebraT, F]): DBFreeAlgebraTI [F] =
      нова DBFreeAlgebraTI [F]
      
  / ** Изплакнете и повторете за нашата конзолна алгебра * /
  клас ConsoleFreeAlgebraTI [F [_]] (неявен I: InjectK [ConsoleFreeAlgebraT, F]) {
      def putLine [T] (t: T): Free [F, Unit] = Free.inject [ConsoleFreeAlgebraT, F] (PutLine (t)) // <- Всички алгебри са `Безплатни` сега
  }
  
  неявна защита на конзолатаFreeAlgebraTI [F [_]] (неявен I: InjectK [ConsoleFreeAlgebraT, F]): ConsoleFreeAlgebraTI [F] =
      нова ConsoleFreeAlgebraTI [F]

Искам да отделя секунда, за да обсъдим InjectK. Точно като FunctionK, InjectK работи върху видове. И така, какво прави точно това? Ще се опитам да обясня. Ето ...

Нека разгледаме подписа на типа: абстрактен клас InjectK [F [_], G [_]]

InjectK взема вашата алгебра F [_] и я инжектира в някаква друга алгебра G [_] Така че в нашите случаи:

def create [T] (t: T): Безплатно [F, Boolean]
      Free.inject [DBFreeAlgebraT, F] (Създаване (t))

Просто увеличаване: Free.inject [DBFreeAlgebraT, F] Ние инжектираме DBFreeAlgebraT в друга алгебра F. В нашия случай F ще бъде комбинираната алгебра: DbAndConsoleAlgebra, която виждате по-долу.

Този блог обобщава по-добре от мен:

  1. неявно разрешаване на инстанция на Inject [DBFreeAlgebraT, F]
  2. използвайки го за инжектиране на DBFreeAlgebraT във F [_] и накрая
  3. повдигане на F [_] в по-обобщената Свободна монада

Ок сега, когато котелът не е на път, можем да кажем на нашия код, че имаме много алгебри.

обект комбиниран {
  тип DbAndConsoleAlgebra [T] = EitherK [DBFreeAlgebraT, ConsoleFreeAlgebraT, T]

  val FutureInterpreter: DbAndConsoleAlgebra ~> Future =
    DBFreeAlgebraT.FutureInterpreter или ConsoleFreeAlgebraT.FutureInterpreter
}

EitherK работи върху Kinds като всичко останало с наставката „K“. Или казва, че това е или един контейнер (с неща в него) или друг. За нас това означава, че комбинираната ни алгебра е или DBFreeAlgebraT или ConsoleFreeAlgebraT.

Така че нещо като: SomeSpecificAlgebra (DB или Console) (инжектиране) ~> CombinedAlgebra (EitherK [,, _, ...]) (liftF) ~> Безплатно (така че всичко е под 1 общ чадър)

Сега най-накрая можем да стигнем до момента, в който крайната версия на кода без етикети е по-горе:

клас FreeUserRepo (неявно)
                   DB: DBFreeAlgebraT.DBFreeAlgebraTI [Combined.DbAndConsoleAlgebra],
                   C: ConsoleFreeAlgebraT.ConsoleFreeAlgebraTI [Комбиниран.DbAndConsoleAlgebra]) {

  def getUser (id: Long): безплатно [Combined.DbAndConsoleAlgebra, или [DatabaseError, User]] = DB.read (id)
  def addUser (потребител: Потребител): Безплатен [Combined.DbAndConsoleAlgebra, Boolean] = DB.create (потребител)

  / **
    * EitherT.liftF има следния подпис:
    * def liftF [F [_], A, B] (fb: F [B])
    *
    * Имаме нужда от този тип псевдоним поради факта, че той приема само 'F [_] `
    * и преминаваме в „Безплатно [DbAndConsoleAlgebra, A]“
    * Псевдонимът на типа фиксира 1 параметър, за да пасне на формата на "F [_]"
    * /
  тип DbAndConsoleAlgebraContainer [A] = Безплатен [Combined.DbAndConsoleAlgebra, A]

  def updateUser (потребител: Потребител): Безплатно [Combined.DbAndConsoleAlgebra, или [DatabaseError, Boolean]] = (за {
    userFromDB <- EitherT (getUser (user.id))
    _ <- EitherT.liftF (C.putLine (s "Намерихме потребител ($ userFromDB) !!"))
    успешно добавен <- EitherT.liftF [DbAndConsoleAlgebraContainer, DatabaseError, Boolean] (addUser (потребител))
  } добив успешноДобавен). стойност

}

Основните методи са почти идентични:

/ ** Финал на Tagless * /
обект UserRepoRunner разширява приложението {

  val repo = ново UserRepo (DatabaseAlgebra.FutureInterpreter, ConsoleAlgebra.FutureInterpreter) // <- Конзолният преводач просто е добавен в

  println (Await.result (
    (за {
      _ <- repo.addUser (Потребител (1, "Боб", 31))
      dbErrorOrSuccessfullyUpdated <- repo.updateUser (Потребител (1, "Bobby", 31))
    } добив dbErrorOrSuccessfullyUpdated),
    1 секунда))

}

/** Безплатно */
обект DBFreeAlgebraRunner разширява приложението {

  val repo = ново FreeUserRepo

  println (Await.result (
    (за {
      _ <- repo.addUser (Потребител (1, "Боб", 31))
      dbErrorOrSuccessfullyUpdated <- repo.updateUser (Потребител (1, "Bobby", 31))
    } добив dbErrorOrSuccessfullyUpdated) .foldMap (Combined.FutureInterpreter), // <- Нов лъскав преводач
    1 секунда))

}

изход:

sbt: sample-blog-code> run
[предупреди] Открити са няколко основни класа. Изпълнете „покажи откритиMainClasses“, за да видите списъка

Открити са няколко основни класа, изберете един, който да стартирате:

 [1] DBFreeAlgebraRunner
 [2] UserRepoRunner

Въведете номер: 1

[информация] Работещ DBFreeAlgebraRunner
Намерихме потребител (Потребител (1, Боб, 31)) !!
Право (вярно)

sbt: sample-blog-code> run
[предупреди] Открити са няколко основни класа. Изпълнете „покажи откритиMainClasses“, за да видите списъка

Открити са няколко основни класа, изберете един, който да стартирате:

 [1] DBFreeAlgebraRunner
 [2] UserRepoRunner

Въведете номер: 2

[info] Работещ UserRepoRunner
Намерихме потребител (Потребител (1, Боб, 31)) !!
Право (вярно)

ТОВА Е!!!! УСПЯХМЕ!!!!

  • Обещах, че ще спомена val castedUser = user.asInstanceOf [Потребител] И така, трябва да дойда чист и да призная нещо. Опитах се да бъда гладък и да направя моя DBAlgebra общ на един от неговите типове тук: def create (t: T): Future [Boolean]

Обикновено DSL няма да е това общо и ще посочи T, за да бъде потребител като: def create (t: User): Future [Boolean]

Все пак отидох за това, за да видя дали мога да го накарам да работи и се оказа добре. Това беше повече доказателство за мен, че можете да използвате техниките и да се опитате да поддържате някакво ниво на генерични продукти.

Също така исках да предизвикам себе си и да демонстрирам, че можете да напишете код като този и все още да използвате модели за обработка на грешки като Either. Чувствам се като повечето примери, по-специално около Free Monads, не демонстрират как обработката на грешки може да се използва във вашия код. Надявам се, че това може да даде основен пример за някакъв реален сценарий на живот около работа с грешки с тези техники.

Каква е разликата между двата подхода?

Най-основният отговор, за който се сещам, е безкрайният финал изразява api чрез функции Free Monads изразяват api чрез ADTs

Кое да изберете?

За съжаление тук липсата ми на опит ограничава как мога да отговоря на това. Най-голямата разлика, която виждам е, че Free Monads ви позволяват да инспектирате ADT, които са изградени по пътя. Това ви дава достъп до цялата структура на повиквания, преди да бъде интерпретирана и действията да бъдат изпълнени. Вярвам, че това дава възможност за допълнителни оптимизации на вашите програми.

Вижте изображението по-долу:

Крайният модел на безизходица се поддава на по-малко код, за да се постигне крайният краен резултат.

Какво е готино в тези неща?

Основната причина, която ме интересуваше идеята да се абстрахирам над монадите ви, беше нещо, което чух преди няколко години при разговор в Скала. Ораторът нарисува сценарий, при който разработчикът иска да отстрани грешки в кода си, но той е асинхронен. Използвайки подобен модел, разработчикът може да напише тест за интеграция, да създаде интерпретатор, да присвои Id monad и да насочи теста си към валидна среда и да гледа кода да се изпълнява синхронно и да преглежда журналите по подреден начин.

Мислех, че това е завладяващо понятие и е нещо, с което мога да се свържа.

Добре, това е достатъчно от мен. До следващия път!

Крайният код можете да намерите тук:
https://gist.github.com/agaro1121/969040886d64e0dc1ded053341631490