├── .gitignore ├── project └── build.properties ├── README.md ├── .scalafmt.conf └── src └── main ├── scala └── ru │ └── tinkoff │ └── tagless │ ├── tagged │ ├── package.scala │ ├── CoalgT.scala │ ├── ParseApp.scala │ ├── InterpretApp.scala │ ├── Parser.scala │ ├── ParserTF.scala │ ├── ParseLF.scala │ ├── Interpreter.scala │ └── Operation.scala │ ├── functor │ ├── Transactional.scala │ ├── MapErrors.scala │ ├── Raise.scala │ ├── Server.scala │ ├── AccountsModule.scala │ └── Accounts.scala │ ├── console │ ├── Output.scala │ ├── functionK.scala │ ├── Input.scala │ ├── ExecuteCommand.scala │ ├── Storage.scala │ ├── Parser.scala │ ├── DiApp.scala │ └── AccountService.scala │ └── either │ ├── EitherApp.scala │ ├── Xor.scala │ └── Choice.scala └── resources └── operations.list /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.1.6 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FDyrka 2 | 3 | Shitty repo created for some guy with anime profile pic -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | binPack.parentConstructors = true 2 | maxColumn = 120 3 | includeCurlyBraceInSelectChains = false 4 | align = most 5 | rewrite.rules = [ 6 | asciisortimports 7 | ] -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless 2 | 3 | package object tagged { 4 | type Algebra[F[_], A] = F[A] => A 5 | type Coalgebra[F[_], A] = A => F[A] 6 | } 7 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/functor/Transactional.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.functor 2 | import mainecoon.{autoFunctorK, autoInvariantK} 3 | 4 | trait Transactional[I, F[_]] { 5 | def begin: F[(I, F[Boolean])] 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/operations.list: -------------------------------------------------------------------------------- 1 | create Oleg 2 | change Oleg 1000 3 | create Belorus 4 | change Belorus 200 5 | change Belorus 35.5 6 | begin 7 | change Oleg -1000 8 | change Belorus +1000 9 | end 10 | begin 11 | create Nikita 12 | change Nikita +100 13 | change Oleg -100 14 | end 15 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/console/Output.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.console 2 | 3 | import cats.effect.Sync 4 | import mainecoon.autoFunctorK 5 | 6 | @autoFunctorK 7 | trait Output[F[_]] { 8 | def putLine(s: String): F[Unit] 9 | } 10 | 11 | object Output { 12 | implicit def console[F[_]: Sync]: Output[F] = 13 | s => Sync[F].delay(println(s)) 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/functor/MapErrors.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.functor 2 | import cats.{ApplicativeError, MonadError} 3 | import org.http4s.{HttpRoutes, Response} 4 | 5 | 6 | object HandleErrors { 7 | def apply[F[_]: MonadError[?[_], Throwable]](httpService: HttpRoutes[F])(f: PartialFunction[Throwable, Response[F]]): HttpRoutes[F] = 8 | httpService 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/CoalgT.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.tagged 2 | import cats.Id 3 | import cats.arrow.FunctionK 4 | 5 | trait CoalgT[PreAlg[_, _], A] extends (A => FunctionK[PreAlg[A, ?], Id]) { 6 | def run[B](a: A, alg: PreAlg[A, B]): B 7 | override def apply(v1: A): FunctionK[PreAlg[A, ?], Id] = new FunctionK[PreAlg[A, ?], Id] { 8 | override def apply[B](fa: PreAlg[A, B]): Id[B] = run(v1, fa) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/console/functionK.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.console 2 | 3 | import cats.arrow.FunctionK 4 | 5 | object functionK { 6 | def apply[F[_]]: MkFunctionK[F] = new MkFunctionK 7 | 8 | final class MkFunctionK[F[_]](val dummy: Boolean = true) extends AnyVal { 9 | type T 10 | 11 | def apply[G[_]](f: F[T] => G[T]): FunctionK[F, G] = new FunctionK[F, G] { 12 | def apply[A](fa: F[A]): G[A] = f(fa.asInstanceOf[F[T]]).asInstanceOf[G[A]] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/console/Input.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.console 2 | 3 | import cats.effect.Sync 4 | import cats.instances.either._ 5 | import cats.instances.list._ 6 | import cats.instances.option._ 7 | import cats.syntax.either._ 8 | import mainecoon.autoFunctorK 9 | 10 | import scala.io.StdIn 11 | 12 | @autoFunctorK 13 | sealed trait Input[F[_]] { 14 | def readLine: F[String] 15 | } 16 | 17 | object Input { 18 | def console[F[_]: Sync]: Input[F] = new Input[F] { 19 | override def readLine: F[String] = Sync[F].delay(StdIn.readLine()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/either/EitherApp.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.either 2 | 3 | object EitherApp { 4 | def sqrt(x: Double): Either[String, Double] = 5 | if (x < 0) Left(s"$x < 0 under sqrt") else Right(Math.sqrt(x)) 6 | 7 | def div(x: Double, y: Double): Either[String, Double] = 8 | if (y == 0) Left(s"division by zero") else Right(x / y) 9 | 10 | def calc(x: Double, y: Double): Either[String, Double] = 11 | for { 12 | xq <- sqrt(x) 13 | yq <- sqrt(y) 14 | sq <- sqrt(x + y) 15 | res <- div(xq + yq, sq) 16 | } yield res 17 | 18 | def main(args: Array[String]): Unit = { 19 | println(calc(1, 2)) 20 | println(calc(1, -2)) 21 | println(calc(0, 0)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/ParseApp.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.tagged 2 | 3 | import cats.data.OptionT 4 | import cats.effect.{ExitCode, IO, IOApp} 5 | import fs2.{io, text} 6 | 7 | object ParseApp extends IOApp { 8 | val Resource = "/operations.list" 9 | val src = 10 | OptionT(IO(Option(getClass.getResourceAsStream(Resource)))) 11 | .getOrElseF(IO.raiseError(new Exception(s"$Resource not found"))) 12 | 13 | val lines = 14 | io.readInputStream(src, 100) 15 | .through(text.utf8Decode) 16 | .through(text.lines) 17 | .compile 18 | .toList 19 | 20 | override def run(args: List[String]): IO[ExitCode] = 21 | for { 22 | ls <- lines 23 | _ <- IO(println(Parser.parse(ls))) 24 | } yield ExitCode.Success 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/functor/Raise.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.functor 2 | 3 | import cats.{Applicative, ApplicativeError} 4 | 5 | trait Raise[F[_], E] { 6 | def raise[A](err: E): F[A] 7 | } 8 | 9 | object Raise { 10 | implicit def raiseApplicativeError[F[_], E, E1](implicit appErr: ApplicativeError[F, E], sub: E1 <:< E): Raise[F, E1] = 11 | new Raise[F, E1] { 12 | override def raise[A](err: E1): F[A] = appErr.raiseError(err) 13 | } 14 | 15 | object syntax{ 16 | final implicit class RaiseOps[E](val err: E) extends AnyVal{ 17 | def raise[F[_], A](implicit raise: Raise[F, E]): F[A] = raise.raise(err) 18 | } 19 | 20 | final implicit class RaiseOptionOps[A](val opt: Option[A]) extends AnyVal { 21 | def liftTo[F[_]] = new RaiseLiftToApplied[F, A](opt) 22 | } 23 | } 24 | 25 | class RaiseLiftToApplied[F[_], A](val opt: Option[A]) extends AnyVal { 26 | def apply[E](err: => E)(implicit raise: Raise[F, E], app: Applicative[F]): F[A] = 27 | opt match { 28 | case None => raise.raise(err) 29 | case Some(a) => app.pure(a) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/InterpretApp.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.tagged 2 | 3 | import cats.data.OptionT 4 | import cats.effect.{ExitCode, IO, IOApp} 5 | import fs2.{io, text} 6 | import Interpreter.implicits._ 7 | import cats.implicits._ 8 | import ru.tinkoff.tagless.tagged.Interpreter.Transaction 9 | 10 | object InterpretApp extends IOApp { 11 | val Resource = "/operations.list" 12 | val src = 13 | OptionT(IO(Option(getClass.getResourceAsStream(Resource)))) 14 | .getOrElseF(IO.raiseError(new Exception(s"$Resource not found"))) 15 | 16 | val lines = 17 | io.readInputStream(src, 100) 18 | .through(text.utf8Decode) 19 | .through(text.lines) 20 | .compile 21 | .toList 22 | 23 | override def run(args: List[String]): IO[ExitCode] = 24 | for { 25 | ls <- lines 26 | result = ParserTF.parse[Interpreter.Result[Unit]](ls) 27 | Transaction(acc, errors) = result.runS(Transaction()).value 28 | _ <- errors.traverse_(s => IO(println(s))) 29 | _ <- IO(println(s"final state: $acc")) 30 | } yield ExitCode.Success 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/console/ExecuteCommand.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.console 2 | 3 | import cats.FlatMap 4 | import cats.instances.either._ 5 | import cats.instances.list._ 6 | import cats.instances.option._ 7 | import cats.syntax.either._ 8 | import cats.syntax.flatMap._ 9 | import cats.syntax.functor._ 10 | import mainecoon.autoFunctorK 11 | 12 | @autoFunctorK 13 | trait ExecuteCommand[F[_]] { 14 | def create(id: String): F[Unit] 15 | def transfer(from: String, to: String)(amount: BigDecimal): F[Unit] 16 | def get(id: String): F[Unit] 17 | def help(str: String): F[Unit] 18 | } 19 | 20 | object ExecuteCommand { 21 | def account[F[_]: FlatMap](svc: AccountService[F], output: Output[F]): ExecuteCommand[F] = 22 | new ExecuteCommand[F] { 23 | override def create(id: String): F[Unit] = svc.create(Account(id)) 24 | override def transfer(from: String, to: String)(amount: BigDecimal): F[Unit] = 25 | svc.transfer(Account(from), Account(to), Amount(amount)) 26 | override def get(id: String): F[Unit] = 27 | svc.read(Account(id)).map(_.value.toString) >>= output.putLine 28 | override def help(str: String): F[Unit] = output.putLine(str) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/functor/Server.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.functor 2 | import cats.effect.{ConcurrentEffect, IO, Sync} 3 | import fs2.StreamApp 4 | import fs2.StreamApp.ExitCode 5 | import org.http4s.dsl.Http4sDsl 6 | import org.http4s.server.blaze.BlazeBuilder 7 | import cats.syntax.functor._ 8 | import cats.syntax.flatMap._ 9 | 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | 12 | case class Server[F[_]: ConcurrentEffect]() { 13 | implicit val dsl: Http4sDsl[F] = new Http4sDsl[F] {} 14 | 15 | def init: F[fs2.Stream[F, ExitCode]] = 16 | for { 17 | accountsService <- Accounts[F] 18 | accountsModule = AccountsModule(accountsService) 19 | _ <- Sync[F].delay(println("server started and http://localhost:8383")) 20 | } yield 21 | BlazeBuilder[F] 22 | .bindHttp(8383, "localhost") 23 | .mountService(accountsModule.service, "/accounts") 24 | .serve 25 | 26 | def serve = fs2.Stream.force(init) 27 | 28 | } 29 | 30 | object Server extends StreamApp[IO] { 31 | val server = Server[IO]() 32 | 33 | override def stream(args: List[String], requestShutdown: IO[Unit]): fs2.Stream[IO, StreamApp.ExitCode] = server.serve 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/console/Storage.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.console 2 | 3 | import cats.effect.concurrent.MVar 4 | import cats.effect.syntax.bracket._ 5 | import cats.effect.{Concurrent, ExitCase, Sync} 6 | import cats.instances.either._ 7 | import cats.instances.list._ 8 | import cats.instances.option._ 9 | import cats.syntax.either._ 10 | import cats.syntax.functor._ 11 | import mainecoon.autoFunctorK 12 | 13 | @autoFunctorK 14 | trait Storage[K, V, F[_]] { 15 | def get(id: K): F[Option[V]] 16 | def transact[A](job: (K => Option[V]) => (A, List[(K, V)])): F[A] 17 | } 18 | 19 | object Storage { 20 | def apply[F[_]: Concurrent, K, V]: F[Storage[K, V, F]] = for (mvar <- MVar.of(Map.empty[K, V])) yield Impl(mvar) 21 | 22 | private case class Impl[F[_], K, V](mvar: MVar[F, Map[K, V]])(implicit F: Sync[F]) extends Storage[K, V, F] { 23 | override def get(id: K): F[Option[V]] = mvar.read.map(_.get(id)) 24 | override def transact[A](job: (K => Option[V]) => (A, List[(K, V)])): F[A] = 25 | mvar.take.bracketCase { map => 26 | val (a, items) = job(map.lift) 27 | mvar.put(map ++ items) as a 28 | } { 29 | case (_, ExitCase.Completed) => F.unit 30 | case (old, _) => mvar.put(old) 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/functor/AccountsModule.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.functor 2 | import cats.data.{Kleisli, OptionT} 3 | import cats.effect.Sync 4 | import org.http4s.{HttpRoutes, QueryParamDecoder} 5 | import org.http4s.dsl.Http4sDsl 6 | import cats.syntax.flatMap._ 7 | import cats.syntax.applicativeError._ 8 | import org.http4s.dsl.impl.QueryParamDecoderMatcher 9 | import ru.tinkoff.tagless.functor.AccountsModule.DiffMatcher 10 | 11 | final case class AccountsModule[F[_]: Sync](accounts: Accounts[F])(implicit dsl: Http4sDsl[F]) { 12 | import dsl._ 13 | 14 | val service: HttpRoutes[F] = HttpRoutes 15 | .of[F] { 16 | case GET -> Root / "account" / id => accounts.get(id).flatMap(bd => Ok(bd.toString)) 17 | case POST -> Root / "account" / id :? DiffMatcher(diff) => 18 | accounts.modify(id, diff).flatMap(bd => Ok(bd.toString)) 19 | case PUT -> Root / "account" / id => accounts.create(id).flatMap(_ => Ok()) 20 | } 21 | .mapF(_.recoverWith { case AccountError.DoesNotExist(id) => OptionT.liftF(NotFound(s"account $id not found")) }) 22 | } 23 | 24 | object AccountsModule { 25 | object DiffMatcher extends QueryParamDecoderMatcher[BigDecimal]("diff") 26 | 27 | implicit val bigDecimalQueryParameter: QueryParamDecoder[BigDecimal] = 28 | QueryParamDecoder.fromUnsafeCast(v => BigDecimal(v.value))("BigDecimal") 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/either/Xor.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.either 2 | 3 | trait Xor[A, B] { self => 4 | def fold[X](l: A => X)(r: B => X): X 5 | 6 | def toEither = fold[Either[A, B]](Left(_))(Right(_)) 7 | 8 | def map[C](f: B => C): Xor[A, C] = new Xor[A, C] { 9 | override def fold[X](l: A => X)(r: C => X): X = self.fold(l)(b => r(f(b))) 10 | } 11 | 12 | def flatMap[C](f: B => Xor[A, C]): Xor[A, C] = new Xor[A, C] { 13 | override def fold[X](l: A => X)(r: C => X): X = self.fold(l)(b => f(b).fold(l)(r)) 14 | } 15 | override def toString: String = fold(a => s"left($a)")(b => s"right($b)") 16 | } 17 | 18 | object Xor { 19 | def fromEither[A, B](e: Either[A, B]): Xor[A, B] = new Xor[A, B] { 20 | override def fold[C](f: A => C)(g: B => C): C = e.fold(f, g) 21 | } 22 | 23 | def left[A, B](a: A): Xor[A, B] = new Xor[A, B] { 24 | override def fold[C](f: A => C)(g: B => C): C = f(a) 25 | } 26 | 27 | def right[A, B](b: B): Xor[A, B] = new Xor[A, B] { 28 | override def fold[C](f: A => C)(g: B => C): C = g(b) 29 | } 30 | } 31 | 32 | object XorApp { 33 | 34 | def sqrt(x: Double): Xor[String, Double] = 35 | if (x < 0) Xor.left(s"$x < 0 under sqrt") else Xor.right(Math.sqrt(x)) 36 | 37 | def div(x: Double, y: Double): Xor[String, Double] = 38 | if (y == 0) Xor.left(s"division by zero") else Xor.right(x / y) 39 | 40 | def calc(x: Double, y: Double): Xor[String, Double] = 41 | for { 42 | xq <- sqrt(x) 43 | yq <- sqrt(y) 44 | sq <- sqrt(x + y) 45 | res <- div(xq + yq, sq) 46 | } yield res 47 | 48 | def main(args: Array[String]): Unit = { 49 | println(calc(1, 2)) 50 | println(calc(1, -2)) 51 | println(calc(0, 0)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/console/Parser.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.console 2 | 3 | import cats.MonadError 4 | import cats.instances.either._ 5 | import cats.instances.list._ 6 | import cats.instances.option._ 7 | import cats.syntax.applicative._ 8 | import cats.syntax.either._ 9 | import cats.syntax.flatMap._ 10 | import cats.syntax.functor._ 11 | import mainecoon.autoFunctorK 12 | 13 | import scala.util.Try 14 | 15 | @autoFunctorK 16 | trait Parser[F[_]] { 17 | def parseAndRun(s: String): F[Boolean] 18 | } 19 | 20 | object Parser { 21 | import ParseError.NumberFormatError 22 | 23 | val Create = "create (\\w+)".r 24 | val Transfer = "transfer (\\w+) (\\w+) (\\d+)".r 25 | val Get = "get (\\w+)".r 26 | val Exit = "exit" 27 | val Help = "help" 28 | 29 | def apply[F[_]: MonadError[?[_], ParseError]](exec: ExecuteCommand[F]): Parser[F] = { 30 | case Create(name) => exec.create(name) as true 31 | case Transfer(from, to, countStr) => 32 | (Try(BigDecimal(countStr)).toEither 33 | .leftMap(ex => NumberFormatError(ex.getMessage): ParseError) 34 | .raiseOrPure[F] >>= 35 | exec.transfer(from, to)) as true 36 | 37 | case Get(name) => exec.get(name) as true 38 | case Exit => false.pure[F] 39 | case Help => 40 | exec help 41 | """available commands: 42 | |create : new account 43 | |transfer : transfer money 44 | |get : get account current balance 45 | |exit : exit application 46 | |help : print current help""".stripMargin as true 47 | } 48 | } 49 | 50 | sealed trait ParseError extends Product with Serializable 51 | 52 | object ParseError { 53 | final case class BadFormat(s: String) extends ParseError 54 | final case class NumberFormatError(s: String) extends ParseError 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/either/Choice.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.either 2 | 3 | trait Choice[A, B, C] { 4 | def left(a: A): C 5 | def right(b: B): C 6 | } 7 | 8 | object Choice { 9 | def apply[A, B, C](implicit choice: Choice[A, B, C]): Choice[A, B, C] = choice 10 | 11 | def left[A, B, C: Choice[A, B, ?]](a: A): C = Choice[A, B, C].left(a) 12 | def right[A, B, C: Choice[A, B, ?]](b: B): C = Choice[A, B, C].right(b) 13 | 14 | implicit def continuation[A, B, C, D](implicit choice: Choice[A, B, C]): Choice[A, D, D <|- C] = 15 | new Choice[A, D, D <|- C] { 16 | override def left(a: A) = _ => choice.left(a) 17 | override def right(d: D) = cont => cont(d) 18 | } 19 | 20 | type <|-[A, B] = (A => B) => B 21 | 22 | implicit class ContSyntax[A, B](val cont: A <|- B) extends AnyVal { 23 | def flatMap(f: A => B): B = cont(f) 24 | def map(f: A => B): B = cont(f) 25 | } 26 | } 27 | 28 | object AlternatingApp { 29 | import Choice.{<|-, ContSyntax, left, right} 30 | 31 | def sqrt[C: Choice[String, Double, ?]](x: Double): C = 32 | if (x < 0) left(s"$x < 0 under sqrt") else right(Math.sqrt(x)) 33 | 34 | def div[C: Choice[String, Double, ?]](x: Double, y: Double): C = 35 | if (y == 0) left(s"division by zero") else right(x / y) 36 | 37 | def calc[C: Choice[String, Double, ?]](x: Double, y: Double): C = 38 | for { 39 | xq <- sqrt[Double <|- C](x) 40 | yq <- sqrt[Double <|- C](y) 41 | sq <- sqrt[Double <|- C](x + y) 42 | } yield div[C](xq + yq, sq) 43 | 44 | def main(args: Array[String]): Unit = { 45 | implicit val choiceString: Choice[String, Double, String] = 46 | new Choice[String, Double, String] { 47 | override def left(a: String): String = s"left{$a}" 48 | override def right(b: Double): String = s"right{$b}" 49 | } 50 | 51 | println(calc(1, 2)) 52 | println(calc(1, -2)) 53 | println(calc(0, 0)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/Parser.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.tagged 2 | import scala.annotation.tailrec 3 | import scala.util.Try 4 | 5 | import Operation.{Change => change, Create => create, Transact => transact} 6 | 7 | object Parser { 8 | val Create = "create (\\w+)".r 9 | val Change = "change (\\w+) ([-+]?[\\d\\.]+)".r 10 | val Begin = "begin" 11 | val End = "end" 12 | val Empty = "\\s*".r 13 | 14 | final case class ParseSub(parsed: Vector[Operation], rest: Option[List[String]]) 15 | 16 | def parse(strings: List[String]): Either[String, Vector[Operation]] = 17 | parseIter(strings, Vector()) flatMap { 18 | case ParseSub(_, Some(rest)) => Left("unexpected end of transaction") 19 | case ParseSub(parsed, None) => Right(parsed) 20 | } 21 | 22 | @tailrec 23 | def parseIter(strings: List[String], parsed: Vector[Operation]): Either[String, ParseSub] = 24 | strings match { 25 | case Nil => Right(ParseSub(parsed, None)) 26 | case line :: rest => 27 | line match { 28 | case End => Right(ParseSub(parsed.reverse, Some(rest))) 29 | case Create(id) => parseIter(rest, parsed :+ create(id)) 30 | case Change(id, count) => 31 | Try(BigDecimal(count)) match { 32 | case util.Failure(err) => Left(err.toString) 33 | case util.Success(diff) => parseIter(rest, parsed :+ change(id, diff)) 34 | } 35 | case Begin => 36 | parseSub(rest, Vector()) match { 37 | case l @ Left(_) => l 38 | case Right(ParseSub(_, None)) => Left("unexpected end of input inside transaction") 39 | case Right(ParseSub(sub, Some(remains))) => 40 | parseIter(remains, parsed :+ transact(sub.toList)) 41 | } 42 | case Empty() => parseIter(rest, parsed) 43 | case _ => Left(s"could not parse: $line") 44 | } 45 | } 46 | 47 | def parseSub(strings: List[String], parsed: Vector[Operation]): Either[String, ParseSub] = parseIter(strings, parsed) 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/console/DiApp.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.console 2 | 3 | import cats.data.EitherT 4 | import cats.effect._ 5 | import cats.instances.either._ 6 | import cats.instances.list._ 7 | import cats.instances.option._ 8 | import cats.syntax.applicativeError._ 9 | import cats.syntax.either._ 10 | import cats.syntax.flatMap._ 11 | import cats.syntax.functor._ 12 | import cats.syntax.monad._ 13 | import cats.{MonadError, ~>} 14 | import mainecoon.syntax.functorK._ 15 | 16 | final case class DiApp[F[_]: MonadError[?[_], Throwable]]( 17 | input: Input[F], 18 | parser: Parser[F], 19 | output: Output[F], 20 | ) { 21 | def run: F[Unit] = 22 | output.putLine("DI app started") >> 23 | (input.readLine >>= parser.parseAndRun) 24 | .handleErrorWith(ex => output.putLine(s"error: $ex") as true) 25 | .iterateWhile(identity) >> 26 | output.putLine("DI app exiting") 27 | } 28 | 29 | object DiApp extends IOApp { 30 | def unliftErr[F[_], E](f: E => Throwable)(implicit err: MonadError[F, Throwable]): EitherT[F, E, ?] ~> F = 31 | functionK[EitherT[F, E, ?]](_.leftMap(f).value.flatMap(_.raiseOrPure[F])) 32 | 33 | def makeApp[F[_]: Concurrent]: F[DiApp[F]] = 34 | for { 35 | storage <- Storage[F, String, BigDecimal] 36 | account = AccountService(storage.mapK(EitherT.liftK[F, AccountError]), 100) 37 | .mapK(unliftErr(AppAccountError)) 38 | output = Output.console[F] 39 | input = Input.console[F] 40 | exec = ExecuteCommand.account[F](account, output) 41 | parser = Parser(exec.mapK(EitherT.liftK[F, ParseError])) 42 | .mapK(unliftErr(AppParseError)) 43 | } yield DiApp(input, parser, output) 44 | 45 | override def run(args: List[String]): IO[ExitCode] = 46 | makeApp[IO] flatMap (_.run) as ExitCode.Success 47 | } 48 | 49 | sealed abstract class AppError(message: String) extends Exception(message, null, false, true) 50 | 51 | final case class AppParseError(err: ParseError) extends AppError(s"Parse Error: $err") 52 | final case class AppAccountError(err: AccountError) extends AppError(s"Account Error: $err") 53 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/ParserTF.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless 2 | package tagged 3 | 4 | import cats.instances.list._ 5 | import cats.kernel.Monoid 6 | import Monoid.empty 7 | import Operate.{change, create, transact} 8 | import Raise.raise 9 | import cats.syntax.monoid._ 10 | 11 | import scala.annotation.tailrec 12 | import scala.util.Try 13 | 14 | object ParserTF { 15 | val Create = "create (\\w+)".r 16 | val Change = "change (\\w+) ([-+]?[\\d\\.]+)".r 17 | val Begin = "begin" 18 | val End = "end" 19 | val Empty = "\\s*".r 20 | 21 | final case class ParseSub[A](parsed: A, rest: Option[List[String]]) 22 | 23 | def parse[A: Operate: Monoid: Raise[?, String]](strings: List[String]): A = 24 | parseIter[A](strings, empty[A]) match { 25 | case ParseSub(_, Some(rest)) => raise("unexpected end of transaction") 26 | case ParseSub(parsed, None) => parsed 27 | } 28 | 29 | @tailrec 30 | def parseIter[A: Operate: Monoid: Raise[?, String]](strings: List[String], parsed: A): ParseSub[A] = 31 | strings match { 32 | case Nil => ParseSub(parsed, None) 33 | case line :: rest => 34 | line match { 35 | case End => ParseSub(parsed, Some(rest)) 36 | case Create(id) => parseIter(rest, parsed |+| create[A](id)) 37 | case Change(id, count) => 38 | val res = Try(BigDecimal(count)) match { 39 | case util.Failure(err) => raise(err.toString) 40 | case util.Success(diff) => change[A](id, diff) 41 | } 42 | parseIter(rest, parsed |+| res) 43 | case Begin => 44 | parseSub[A](rest, empty[A]) match { 45 | case ParseSub(_, None) => ParseSub(raise("unexpected end of input inside transaction"), Some(rest)) 46 | case ParseSub(sub, Some(remains)) => parseIter(remains, parsed |+| transact(sub)) 47 | } 48 | case Empty() => parseIter(rest, parsed) 49 | case _ => ParseSub(parsed |+| raise(s"could not parse: $line"), Some(rest)) 50 | } 51 | } 52 | 53 | def parseSub[A: Operate: Monoid: Raise[?, String]](strings: List[String], parsed: A): ParseSub[A] = 54 | parseIter(strings, parsed) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/functor/Accounts.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.functor 2 | import cats.effect.Sync 3 | import cats.effect.concurrent.Ref 4 | import cats.syntax.functor._ 5 | import cats.syntax.flatMap._ 6 | import cats.syntax.applicative._ 7 | import cats.syntax.apply._ 8 | import Raise.syntax._ 9 | import ru.tinkoff.tagless.functor.AccountError.{AlreadyCreated, DoesNotExist, InsufficientFunds} 10 | 11 | trait Accounts[F[_]] extends Transactional[Accounts[F], F] { 12 | def get(name: String): F[BigDecimal] 13 | def modify(name: String, state: BigDecimal): F[BigDecimal] 14 | def create(name: String): F[Unit] 15 | } 16 | 17 | object Accounts { 18 | def apply[F[_]: Sync: Raise[?[_], AccountError]]: F[Accounts[F]] = 19 | for (ref <- Ref[F].of(Map[String, BigDecimal]())) 20 | yield AccountsImpl[F](ref) 21 | 22 | private case class AccountsImpl[F[_]: Sync](ref: Ref[F, Map[String, BigDecimal]]) extends Accounts[F] { 23 | override def get(name: String): F[BigDecimal] = 24 | for (map <- ref.get; res <- map.get(name).liftTo(DoesNotExist(name))) yield res 25 | override def modify(name: String, diff: BigDecimal): F[BigDecimal] = 26 | for { 27 | (m, upd) <- ref.access 28 | cur <- m.get(name).liftTo(DoesNotExist(name)) 29 | next = cur + diff 30 | _ <- InsufficientFunds(name, cur, diff).raise.whenA(next < 0) 31 | _ <- upd(m + (name -> next)) 32 | } yield next 33 | override def create(name: String): F[Unit] = 34 | for { 35 | (m, upd) <- ref.access 36 | _ <- AlreadyCreated(name).raise.whenA(m.contains(name)) 37 | _ <- upd(m + (name -> BigDecimal(0))) 38 | } yield () 39 | 40 | override def begin: F[(Accounts[F], F[Boolean])] = 41 | for ((m, upd) <- ref.access) yield (AccountsImpl(ref), ref.get >>= upd) 42 | } 43 | } 44 | 45 | trait AccountError extends Throwable 46 | 47 | object AccountError { 48 | final case class DoesNotExist(id: String) extends Exception(s"account $id does not exist") with AccountError 49 | final case class InsufficientFunds(id: String, has: BigDecimal, diff: BigDecimal) 50 | extends Exception(s"insufficient funds for account $id to change by $diff: has $has ") with AccountError 51 | final case class AlreadyCreated(id: String) extends Exception(s"account $id already exists") with AccountError 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/console/AccountService.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.console 2 | 3 | import cats.MonadError 4 | import cats.instances.either._ 5 | import cats.instances.list._ 6 | import cats.instances.option._ 7 | import cats.syntax.applicativeError._ 8 | import cats.syntax.either._ 9 | import cats.syntax.flatMap._ 10 | import cats.syntax.foldable._ 11 | import cats.syntax.functor._ 12 | import cats.syntax.monadError._ 13 | import cats.syntax.option._ 14 | import mainecoon.autoFunctorK 15 | 16 | @autoFunctorK 17 | trait AccountService[F[_]] { 18 | def read(acc: Account): F[Amount] 19 | def create(acc: Account): F[Unit] 20 | def transfer(from: Account, to: Account, amount: Amount): F[Unit] 21 | } 22 | 23 | object AccountService { 24 | 25 | def apply[F[_]: MonadError[?[_], AccountError]](storage: Storage[String, BigDecimal, F], 26 | initialAmount: BigDecimal = 0): AccountService[F] = 27 | Impl(storage, initialAmount) 28 | 29 | private case class Impl[F[_]]( 30 | storage: Storage[String, BigDecimal, F], 31 | initialAmount: BigDecimal 32 | )(implicit F: MonadError[F, AccountError]) 33 | extends AccountService[F] { 34 | import AccountError._ 35 | 36 | override def read(acc: Account): F[Amount] = 37 | for { 38 | res <- storage.get(acc.id) 39 | amt <- res.liftTo[F](UnknownAccount(acc): AccountError) 40 | } yield Amount(amt) 41 | 42 | override def create(acc: Account): F[Unit] = 43 | storage 44 | .transact(_(acc.id).fold(true -> List(acc.id -> initialAmount))(_ => false -> List.empty)) 45 | .ensure(AlreadyExists)(identity) 46 | .void 47 | 48 | override def transfer(from: Account, to: Account, amount: Amount): F[Unit] = 49 | storage.transact { get => 50 | val results = for { 51 | f <- get(from.id).toRight(UnknownAccount(from)).ensure(Deficit)(_ >= amount.value) 52 | t <- get(to.id).toRight(UnknownAccount(to)) 53 | } yield List(from.id -> (f - amount.value), to.id -> (t + amount.value)) 54 | 55 | results.left.toOption -> results.foldK 56 | }.flatMap(_.traverse_(_.raiseError[F, Unit])) 57 | } 58 | } 59 | 60 | sealed trait AccountError extends Product with Serializable 61 | 62 | object AccountError { 63 | final case class UnknownAccount(acc: Account) extends AccountError 64 | case object Deficit extends AccountError 65 | case object AlreadyExists extends AccountError 66 | } 67 | 68 | final case class Account(id: String) 69 | final case class Amount(value: BigDecimal) 70 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/ParseLF.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.tagged 2 | 3 | import cats.kernel.Monoid 4 | 5 | import scala.annotation.tailrec 6 | import scala.util.Try 7 | import cats.syntax.monoid._ 8 | import cats.instances.list._ 9 | import Monoid.empty 10 | import Operational.{Operational, change, create, transact} 11 | 12 | object ParseLF { 13 | val Create = "create (\\w+)".r 14 | val Change = "change (\\w+) ([-+]?[\\d\\.]+)".r 15 | val Begin = "begin" 16 | val End = "end" 17 | val Empty = "\\s*".r 18 | 19 | final case class ParseSub[A](parsed: A, rest: Option[List[String]]) 20 | 21 | def parse[A: Operational: Monoid](strings: List[String]): Either[String, A] = 22 | parseIter[A](strings, empty[A]) flatMap { 23 | case ParseSub(_, Some(rest)) => Left("unexpected end of transaction") 24 | case ParseSub(parsed, None) => Right(parsed) 25 | } 26 | 27 | @tailrec 28 | def parseIter[A: Operational: Monoid](strings: List[String], parsed: A): Either[String, ParseSub[A]] = 29 | strings match { 30 | case Nil => Right(ParseSub(parsed, None)) 31 | case line :: rest => 32 | line match { 33 | case End => Right(ParseSub(parsed, Some(rest))) 34 | case Create(id) => parseIter(rest, parsed |+| create[A](id)) 35 | case Change(id, count) => 36 | Try(BigDecimal(count)) match { 37 | case util.Failure(err) => Left(err.toString) 38 | case util.Success(diff) => parseIter(rest, parsed |+| change[A](id, diff)) 39 | } 40 | case Begin => 41 | parseSub[List[A]](rest, Nil) match { 42 | case Left(err) => Left(err) 43 | case Right(ParseSub(_, None)) => Left("unexpected end of input inside transaction") 44 | case Right(ParseSub(sub, Some(remains))) => 45 | parseIter(remains, parsed |+| transact(sub)) 46 | } 47 | case Empty() => parseIter(rest, parsed) 48 | case _ => Left(s"could not parse: $line") 49 | } 50 | } 51 | 52 | def parseSub[A: Operational: Monoid](strings: List[String], parsed: A): Either[String, ParseSub[A]] = 53 | parseIter(strings, parsed) 54 | 55 | implicit def liftOperationalToList[A](implicit op: Operational[A]): Operational[List[A]] = 56 | new Operational[List[A]] { 57 | override def create(id: String): List[A] = List(op.create(id)) 58 | override def change(id: String, diff: BigDecimal): List[A] = List(op.change(id, diff)) 59 | override def transact(xs: List[List[A]]): List[A] = xs.flatten 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/Interpreter.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless.tagged 2 | import cats.data.{State, StateT} 3 | import cats.implicits._ 4 | import cats.kernel.Monoid 5 | import ru.tinkoff.tagless.tagged.Interpreter.implicits._ 6 | import ru.tinkoff.tagless.tagged.Operation.{Change, Create, Transact} 7 | 8 | object Interpreter { 9 | import State.{inspect, modify} 10 | type Accounts = Map[String, BigDecimal] 11 | 12 | 13 | final case class Transaction(accounts: Accounts = Map.empty, errors: Vector[String] = Vector()) { 14 | def raise(s: String) = copy(errors = errors :+ s) 15 | } 16 | type Result[A] = State[Transaction, A] 17 | 18 | def setAccounts(acc: Accounts): Result[Unit] = modify(_.copy(accounts = acc)) 19 | def getAccounts: Result[Accounts] = inspect(_.accounts) 20 | 21 | def raise(s: String): Result[Unit] = State.modify(_.raise(s)) 22 | 23 | def interpreter: Operation => Result[Unit] = { 24 | case Create(id) => operate.create(id) 25 | case Change(id, diff) => operate.change(id, diff) 26 | case Transact(ops) => operate.transact(ops.traverse_(interpreter)) 27 | } 28 | 29 | object implicits { 30 | implicit val raiseString: Raise[Result[Unit], String] = raise 31 | 32 | implicit def resultMonoid[A: Monoid]: Monoid[Result[A]] = new Monoid[Result[A]] { 33 | override def empty: Result[A] = Monoid.empty[A].pure[Result] 34 | override def combine(x: Result[A], y: Result[A]): Result[A] = 35 | for (xx <- x; yy <- y) yield xx |+| yy 36 | } 37 | 38 | implicit val operate: Operate[Result[Unit]] = new Operate[Result[Unit]] { 39 | override def create(id: String): Result[Unit] = 40 | for { 41 | acc <- getAccounts 42 | _ <- if (acc contains id) raise(s"Account $id already exists") 43 | else setAccounts(acc + (id -> BigDecimal(0))) 44 | } yield () 45 | 46 | override def change(id: String, diff: BigDecimal): Result[Unit] = 47 | for { 48 | acc <- getAccounts 49 | _ <- acc.get(id).fold(raise(s"Account $id doesn't exists")) { cur => 50 | val next = cur + diff 51 | if (next < 0) raise(s"Insufficient funds at $id: $cur to change by $diff") 52 | else setAccounts(acc + (id -> next)) 53 | } 54 | } yield () 55 | 56 | override def transact(xs: Result[Unit]): Result[Unit] = StateT.modifyF { 57 | case Transaction(acc, errs) => 58 | xs.runS(Transaction(acc, Vector())).map { 59 | case Transaction(newAcc, Vector()) => Transaction(newAcc, errs) 60 | case Transaction(_, errs2) => Transaction(acc, errs ++ errs2) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | trait Raise[A, E] { 68 | def raise(err: E): A 69 | } 70 | 71 | object Raise { 72 | def raise[A, E](e: E)(implicit r: Raise[A, E]): A = r.raise(e) 73 | } 74 | -------------------------------------------------------------------------------- /src/main/scala/ru/tinkoff/tagless/tagged/Operation.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tagless 2 | package tagged 3 | import cats.Functor 4 | import cats.syntax.functor._ 5 | import mainecoon.autoInvariant 6 | import simulacrum.typeclass 7 | 8 | sealed trait Operation 9 | 10 | object Operation { 11 | final case class Create(id: String) extends Operation 12 | final case class Change(id: String, diff: BigDecimal) extends Operation 13 | final case class Transact(ops: List[Operation]) extends Operation 14 | } 15 | 16 | sealed trait OperationF[A] 17 | 18 | object OperationF { 19 | final case class Create[A](id: String) extends OperationF[A] 20 | final case class Change[A](id: String, diff: BigDecimal) extends OperationF[A] 21 | final case class Transact[A](ops: List[A]) extends OperationF[A] 22 | 23 | implicit val functor: Functor[OperationF] = new Functor[OperationF] { 24 | override def map[A, B](fa: OperationF[A])(f: A => B): OperationF[B] = 25 | fa match { 26 | case Create(id) => Create(id) 27 | case Change(id, diff) => Change(id, diff) 28 | case Transact(lst) => Transact(lst.map(f)) 29 | } 30 | } 31 | 32 | def fromRec(op: Operation): Fix[OperationF] = 33 | Fix(op match { 34 | case Operation.Create(id) => Create(id) 35 | case Operation.Change(id, diff) => Change(id, diff) 36 | case Operation.Transact(ops) => Transact(ops.map(fromRec)) 37 | }) 38 | 39 | def toRec(fix: Fix[OperationF]): Operation = 40 | fix.value match { 41 | case Create(id) => Operation.Create(id) 42 | case Change(id, diff) => Operation.Change(id, diff) 43 | case Transact(ops) => Operation.Transact(ops.map(toRec)) 44 | } 45 | } 46 | 47 | final case class Fix[F[_]](value: F[Fix[F]]) { 48 | def fold[A](algebra: Algebra[F, A])(implicit F: Functor[F]): A = 49 | algebra(value.map(_.fold(algebra))) 50 | } 51 | 52 | object Fix { 53 | def mu[F[_]: Functor, A](algebra: Algebra[F, A]): AlgebraMorphism[F, Fix[F], A] = AlgebraMorphism( 54 | from = (ff: F[Fix[F]]) => Fix(ff), 55 | to = algebra, 56 | f = _.fold(algebra) 57 | ) 58 | } 59 | 60 | final case class AlgebraMorphism[F[_], A, B](from: Algebra[F, A], to: Algebra[F, B], f: A => B) { 61 | def guaranteed(fa: F[A])(implicit F: Functor[F]): Boolean = 62 | f(from(fa)) == to(fa.map(f)) 63 | } 64 | 65 | final case class CoalgebraMorphism[F[_], A, B](from: Coalgebra[F, A], to: Coalgebra[F, B], f: A => B) { 66 | def guaranteed(a: A)(implicit F: Functor[F]): Boolean = 67 | from(a).map(f) == to(f(a)) 68 | } 69 | 70 | trait PreOperational[A, B] { 71 | def create(id: String): B 72 | def change(id: String, diff: BigDecimal): B 73 | def transact(xs: List[A]): B 74 | } 75 | 76 | object Operational { 77 | type Operational[A] = PreOperational[A, A] 78 | def create[A](id: String)(implicit op: Operational[A]): A = op.create(id) 79 | def change[A](id: String, diff: BigDecimal)(implicit op: Operational[A]): A = op.change(id, diff) 80 | def transact[A](xs: List[A])(implicit op: Operational[A]): A = op.transact(xs) 81 | 82 | import OperationF._ 83 | 84 | def fromAlgebra[A](alg: Algebra[OperationF, A]): Operational[A] = 85 | new Operational[A] { 86 | override def create(id: String): A = alg(Create(id)) 87 | override def change(id: String, diff: BigDecimal): A = alg(Change(id, diff)) 88 | override def transact(xs: List[A]): A = alg(Transact(xs)) 89 | } 90 | 91 | def toAgebra[A](op: Operational[A]): Algebra[OperationF, A] = { 92 | case Create(id) => op.create(id) 93 | case Change(id, diff) => op.change(id, diff) 94 | case Transact(ops) => op.transact(ops) 95 | } 96 | 97 | def fromCoalgebra[A](coalg: Coalgebra[OperationF, A]): CoalgT[PreOperational, A] = 98 | new CoalgT[PreOperational, A] { 99 | override def run[B](a: A, op: PreOperational[A, B]): B = coalg(a) match { 100 | case Create(id) => op.create(id) 101 | case Change(id, diff) => op.change(id, diff) 102 | case Transact(ops) => op.transact(ops) 103 | } 104 | } 105 | 106 | def toCoalgebra[A](coalg: CoalgT[PreOperational, A]): Coalgebra[OperationF, A] = { a => 107 | val prealg = new PreOperational[A, OperationF[A]] { 108 | override def create(id: String): OperationF[A] = OperationF.Create(id) 109 | override def change(id: String, diff: BigDecimal): OperationF[A] = OperationF.Change(id, diff) 110 | override def transact(xs: List[A]): OperationF[A] = OperationF.Transact(xs) 111 | } 112 | coalg.run(a, prealg) 113 | } 114 | } 115 | @autoInvariant 116 | trait Operate[A] { 117 | def create(id: String): A 118 | def change(id: String, diff: BigDecimal): A 119 | def transact(xs: A): A 120 | } 121 | 122 | object Operate { 123 | def create[A](id: String)(implicit op: Operate[A]): A = op.create(id) 124 | def change[A](id: String, diff: BigDecimal)(implicit op: Operate[A]): A = op.change(id, diff) 125 | def transact[A](xs: A)(implicit op: Operate[A]): A = op.transact(xs) 126 | } 127 | --------------------------------------------------------------------------------