Compare commits

...

6 commits

Author SHA1 Message Date
Timofey Khoruzhii bed837db76 solve 2023-10-28 21:41:10 +03:00
Карпышев Сергей Вадимович c1099c97f9 Format 2023-10-26 13:44:21 +03:00
Карпышев Сергей Вадимович 310b3961ad Add scalafmt and git flow 2023-10-26 11:30:08 +03:00
Карпышев Сергей Вадимович a478d5dccf Update sbt version 2023-10-26 11:24:53 +03:00
Карпышев Сергей Вадимович 32ce6be392 Add homework 2023-10-13 21:11:44 +03:00
Карпышев Сергей Вадимович 0d872fc382 Add lecture materials 2023-05-19 16:39:37 +03:00
15 changed files with 429 additions and 0 deletions

23
.github/workflows/scala.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: Scala CI
on:
push:
branches:
- main
- 'homework/*'
pull_request:
branches:
- main
- 'homework/*'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests
run: sbt test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Formatting
run: sbt scalafmtCheck test:scalafmtCheck

8
.scalafmt.conf Normal file
View file

@ -0,0 +1,8 @@
maxColumn = 120
runner.dialect = scala3
includeCurlyBraceInSelectChains = false
align.preset = more
version = "3.7.7"
rewrite.trailingCommas.style = keep
newlines.penalizeSingleSelectMultiArgList = false
newlines.alwaysBeforeMultilineDef = false

16
build.sbt Normal file
View file

@ -0,0 +1,16 @@
ThisBuild / version := "0.1.0"
ThisBuild / scalaVersion := "3.2.1"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-stream" % "2.8.2",
"co.fs2" %% "fs2-core" % "3.7.0",
"org.typelevel" %% "cats-effect" % "3.5.2",
"org.typelevel" %% "cats-mtl" % "1.3.1",
"org.scalatest" %% "scalatest" % "3.2.15" % "test"
)
scalacOptions := List("-Ykind-projector")
lazy val lang = (project in file("."))
.settings(name := "bachelor-homeworks")

1
project/build.properties Normal file
View file

@ -0,0 +1 @@
sbt.version=1.6.2

1
project/plugins.sbt Normal file
View file

@ -0,0 +1 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")

View file

@ -0,0 +1,42 @@
//> using dep org.typelevel::cats-core:2.10.0
package hw.logger
import cats.{Monad, Monoid}
import scala.util.Try
import cats.data.WriterT
import cats.syntax.applicative.*
import cats.implicits.catsSyntaxApplicativeId
case class Debug(debug: String)
case class Info(info: String)
case class Error(error: String)
type WriterTF[F[_], L] = [A] =>> WriterT[F, L, A]
case class LogEmbed[F[_], A](value: WriterT[WriterTF[WriterTF[F, Vector[Debug]], Vector[Info]], Vector[Error], A])
final case class Logger[F[_]: Monad]():
def debug(debugMsg: String): LogEmbed[F, Unit] = {
val in = WriterT[F, Vector[Debug], (Vector[Info], (Vector[Error], Unit))](
Monad[F].pure((Vector(Debug(debugMsg)), (Vector.empty[Info], (Vector.empty[Error], ()))))
)
val iin = WriterT[WriterTF[F, Vector[Debug]], Vector[Info], (Vector[Error], Unit)](in)
LogEmbed(WriterT[WriterTF[WriterTF[F, Vector[Debug]], Vector[Info]], Vector[Error], Unit](iin))
}
def info(infoMsg: String): LogEmbed[F, Unit] = {
val in = WriterT[F, Vector[Debug], (Vector[Info], (Vector[Error], Unit))](
Monad[F].pure((Vector.empty[Debug], (Vector(Info(infoMsg)), (Vector.empty[Error], ()))))
)
val iin = WriterT[WriterTF[F, Vector[Debug]], Vector[Info], (Vector[Error], Unit)](in)
LogEmbed(WriterT[WriterTF[WriterTF[F, Vector[Debug]], Vector[Info]], Vector[Error], Unit](iin))
}
def error(errorMsg: String): LogEmbed[F, Unit] = {
val in = WriterT[F, Vector[Debug], (Vector[Info], (Vector[Error], Unit))](
Monad[F].pure((Vector.empty[Debug], (Vector.empty[Info], (Vector(Error(errorMsg)), ()))))
)
val iin = WriterT[WriterTF[F, Vector[Debug]], Vector[Info], (Vector[Error], Unit)](in)
LogEmbed(WriterT[WriterTF[WriterTF[F, Vector[Debug]], Vector[Info]], Vector[Error], Unit](iin))
}

View file

@ -0,0 +1,19 @@
package hw.traits
import cats.Monad
import cats.data.{Reader, ReaderT}
import cats.syntax.applicative.*
trait Ask[F[_], R]:
def ask: F[R]
object Ask:
def apply[F[_], R](using ask: Ask[F, R]): Ask[F, R] = ask
given [R]: Ask[Reader[R, *], R] =
new Ask[Reader[R, *], R]:
def ask: Reader[R, R] = Reader(identity)
given [F[_]: Monad, R]: Ask[ReaderT[F, R, *], R] =
new Ask[ReaderT[F, R, *], R]:
def ask: ReaderT[F, R, R] = ReaderT(r => r.pure[F])

View file

@ -0,0 +1,21 @@
package hw.traits
import cats.{Monad, Monoid}
import cats.data.{Writer, WriterT}
import cats.syntax.applicative.*
import cats.syntax.writer._
import cats.syntax.flatMap._
trait Tell[F[_], W]:
def tell(log: W): F[Unit]
object Tell:
def apply[F[_], W](using tell: Tell[F, W]): Tell[F, W] = tell
given [W: Monoid]: Tell[Writer[W, *], W] with
def tell(log: W): Writer[W, Unit] =
Writer(log, ())
given [F[_]: Monad, W: Monoid]: Tell[WriterT[F, W, *], W] with
def tell(log: W): WriterT[F, W, Unit] =
WriterT.tell[F, W](log)

View file

@ -0,0 +1,20 @@
package hw.user
type UserId = UserId.T
object UserId:
opaque type T <: Int = Int
def apply(i: Int): UserId = i
type UserName = UserName.T
object UserName:
opaque type T <: String = String
def apply(s: String): UserName = s
type Age = Age.T
object Age:
opaque type T <: Byte = Byte
val Adult: Age = 18.toByte
def apply(v: Byte): Age = v
final case class User(id: UserId, name: UserName, age: Age, friends: Set[UserId]):
def isAdult: Boolean = age >= Age.Adult

View file

@ -0,0 +1,5 @@
package hw.user
object UserErrors:
case class UserAlreadyExists(name: UserName) extends Throwable
case class UserDoesNotExists(id: UserId) extends Throwable

View file

@ -0,0 +1,57 @@
package hw.user
import cats.MonadThrow
import cats.mtl.Ask
import cats.syntax.applicativeError.*
import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import hw.user.UserErrors.*
case class Config(chunkSize: Int)
trait UserRepositoryDao:
def findAll(config: Config): List[User]
def create(name: UserName, age: Age, friends: Set[UserId] = Set.empty)(
config: Config
): Either[UserAlreadyExists, User]
def delete(userId: UserId)(config: Config): Either[UserDoesNotExists, Unit]
def update(user: User)(config: Config): Either[UserDoesNotExists, Unit]
trait UserRepository[F[_]]:
def findAll: F[List[User]]
def create(name: UserName, age: Age, friends: Set[UserId] = Set.empty): F[User]
def delete(userId: UserId): F[Unit]
def update(user: User): F[Unit]
object UserRepositoryDao:
def apply[F[_]: MonadThrow](dao: UserRepositoryDao)(using Ask[F, Config]): UserRepository[F] = new UserRepository[F]:
override def findAll: F[List[User]] =
Ask[F, Config].ask.flatMap { config =>
dao.findAll(config).pure[F]
}
override def create(name: UserName, age: Age, friends: Set[UserId] = Set.empty): F[User] =
Ask[F, Config].ask.flatMap { config =>
dao.create(name, age, friends)(config) match {
case Right(user) => user.pure[F]
case Left(error) => MonadThrow[F].raiseError[User](error)
}
}
override def delete(userId: UserId): F[Unit] =
Ask[F, Config].ask.flatMap { config =>
dao.delete(userId)(config) match {
case Right(_) => ().pure[F]
case Left(error) => MonadThrow[F].raiseError[Unit](error)
}
}
override def update(user: User): F[Unit] =
Ask[F, Config].ask.flatMap { config =>
dao.update(user)(config) match {
case Right(_) => ().pure[F]
case Left(error) => MonadThrow[F].raiseError[Unit](error)
}
}

View file

@ -0,0 +1,37 @@
package hw.logger
import cats.data.WriterT
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import scala.util.{Success, Try}
class Tests extends AnyFlatSpec with Matchers:
def logEmbed(debug: Vector[String], info: Vector[String], error: Vector[String]): LogEmbed[Try, Unit] =
LogEmbed(
WriterT(WriterT(WriterT(Success((debug.map(Debug.apply), (info.map(Info.apply), (error.map(Error.apply), ())))))))
)
it should "info log" in {
val logger = Logger[Try]()
logger.info("info") shouldBe logEmbed(Vector.empty, Vector("info"), Vector.empty)
}
it should "debug log" in {
val logger = Logger[Try]()
logger.debug("debug") shouldBe logEmbed(Vector("debug"), Vector.empty, Vector.empty)
}
it should "error log" in {
val logger = Logger[Try]()
logger.error("error") shouldBe logEmbed(Vector.empty, Vector.empty, Vector("error"))
}
it should "log everything" in {
val logger = Logger[Try]()
(for {
_ <- logger.info("info").value
_ <- logger.debug("debug").value
_ <- logger.error("error").value
} yield ()) shouldBe logEmbed(Vector("debug"), Vector("info"), Vector("error")).value
}

View file

@ -0,0 +1,14 @@
package hw.traits
import cats.data.{Reader, ReaderT}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class AskTests extends AnyFlatSpec with Matchers:
it should "create Ask[Reader] instance" in {
Ask[Reader[Int, *], Int].ask.run(42) shouldBe 42
}
it should "create Ask[ReaderT] instance" in {
Ask[ReaderT[Option, Int, *], Int].ask.run(42) shouldBe Some(42)
}

View file

@ -0,0 +1,14 @@
package hw.traits
import cats.data.{Writer, WriterT}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class TellTests extends AnyFlatSpec with Matchers:
it should "create Tell[Writer] instance" in {
Tell[Writer[Int, *], Int].tell(42).run shouldBe (42, ())
}
it should "create Tell[WriterT] instance" in {
Tell[WriterT[Option, Int, *], Int].tell(42).run shouldBe Some((42, ()))
}

View file

@ -0,0 +1,151 @@
package hw.user
import cats.mtl.Ask
import cats.{Applicative, MonadError, MonadThrow}
import hw.user.UserErrors.{UserAlreadyExists, UserDoesNotExists}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import scala.util.Random
object TestsData:
var lastConfig: Either[Unit, Config] = Left(())
val sampleUsers: List[User] = List(
User(UserId(0), UserName("Subject #0"), Age(42), Set.empty),
User(UserId(1), UserName("Subject #1"), Age(41), Set.empty),
User(UserId(2), UserName("Subject #2"), Age(40), Set.empty),
User(UserId(3), UserName("Subject #3"), Age(39), Set.empty),
User(UserId(4), UserName("Subject #4"), Age(38), Set.empty)
)
def dao(users: List[User]) = new UserRepositoryDao:
var usersList = users.toSet
override def findAll(config: Config): List[User] =
lastConfig = Right(config)
usersList.toList
override def create(name: UserName, age: Age, friends: Set[UserId])(
config: Config
): Either[UserAlreadyExists, User] =
lastConfig = Right(config)
if (usersList.exists(_.name == name)) Left(UserAlreadyExists(name))
else
val user = User(UserId(usersList.size), name, age, friends)
usersList += user
Right(user)
override def delete(userId: UserId)(config: Config): Either[UserDoesNotExists, Unit] =
lastConfig = Right(config)
if (usersList.exists(_.id == userId))
usersList -= usersList.find(_.id == userId).get
Right(())
else Left(UserDoesNotExists(userId))
override def update(user: User)(config: Config): Either[UserDoesNotExists, Unit] =
lastConfig = Right(config)
if (usersList.exists(_.id == user.id))
usersList -= usersList.find(_.id == user.id).get
usersList += user
Right(())
else Left(UserDoesNotExists(user.id))
type M = [A] =>> Config => Either[Throwable, A]
object M:
given MonadThrow[M] = new MonadError[M, Throwable]:
override def pure[A](x: A): M[A] = _ => Right(x)
override def flatMap[A, B](fa: M[A])(f: A => M[B]): M[B] = c => fa(c).flatMap(a => f(a)(c))
override def tailRecM[A, B](a: A)(f: A => M[Either[A, B]]): M[B] =
c =>
f(a)(c) match
case Left(e) => Left(e)
case Right(Left(a)) => tailRecM(a)(f)(c)
case Right(Right(b)) => Right(b)
override def raiseError[A](e: Throwable): M[A] = _ => Left(e)
override def handleErrorWith[A](fa: M[A])(f: Throwable => M[A]): M[A] = c =>
fa(c) match
case Left(e) => f(e)(c)
case Right(a) => Right(a)
given Ask[M, Config] = new Ask[M, Config]:
override def applicative: Applicative[M] = MonadThrow[M]
override def ask[E2 >: Config]: M[E2] = c => Right(c)
class Tests extends AnyFlatSpec with Matchers:
import TestsData.*
import M.given
it should "correct findAll config propagation" in {
val repository = UserRepositoryDao[M](dao(List.empty))
val config = Config(42)
repository.findAll(config)
lastConfig shouldBe Right(Config(42))
}
it should "correct create config propagation" in {
val repository = UserRepositoryDao[M](dao(List.empty))
val config = Config(43)
repository.create(UserName("Subject #0"), Age(0), Set.empty)(config)
lastConfig shouldBe Right(Config(43))
}
it should "correct delete config propagation" in {
val repository = UserRepositoryDao[M](dao(List.empty))
val config = Config(44)
repository.delete(UserId(0))(config)
lastConfig shouldBe Right(Config(44))
}
it should "correct update config propagation" in {
val repository = UserRepositoryDao[M](dao(List.empty))
val config = Config(45)
repository.update(User(UserId(0), UserName("Subject #0"), Age(0), Set.empty))(config)
lastConfig shouldBe Right(Config(45))
}
it should "correct return all users" in {
val repository = UserRepositoryDao[M](dao(sampleUsers))
val config = Config(46)
repository.findAll(config) shouldBe Right(sampleUsers.toSet.toList)
}
it should "correct create new user" in {
val repository = UserRepositoryDao[M](dao(sampleUsers))
val config = Config(46)
repository.create(UserName("New subject"), Age(0), Set.empty)(config) shouldBe Right(
User(UserId(5), UserName("New subject"), Age(0), Set.empty)
)
}
it should "not create user more than once" in {
val repository = UserRepositoryDao[M](dao(sampleUsers))
val config = Config(46)
repository.create(UserName("Subject #2"), Age(0), Set.empty)(config) shouldBe a[Left[Throwable, User]]
}
it should "delete existing user" in {
val repository = UserRepositoryDao[M](dao(sampleUsers))
val config = Config(46)
repository.delete(UserId(2))(config) shouldBe Right(())
}
it should "not delete nonexistent user" in {
val repository = UserRepositoryDao[M](dao(sampleUsers))
val config = Config(46)
repository.delete(UserId(42))(config) shouldBe a[Left[Throwable, Unit]]
}
it should "update existing user" in {
val repository = UserRepositoryDao[M](dao(sampleUsers))
val config = Config(46)
repository.update(User(UserId(2), UserName("Subject #2"), Age(42), Set.empty))(config) shouldBe Right(())
}
it should "not update nonexistent user" in {
val repository = UserRepositoryDao[M](dao(sampleUsers))
val config = Config(46)
repository
.update(User(UserId(22), UserName("Subject #22"), Age(42), Set.empty))(config) shouldBe a[Left[Throwable, Unit]]
}