рдпрджрд┐ рдЖрдк рд▓реЗрди-рджреЗрди рдХрд╛ рд╕рдорд░реНрдерди рдХрд░рдиреЗ рд╡рд╛рд▓реЗ рдПрдХ рдбреЗрдЯрд╛рдмреЗрд╕ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рддреЗ рд╣реИрдВ, рддреЛ рдЖрдк рд╕рдВрдЧрддрддрд╛ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рднреА рдирд╣реАрдВ рд╕реЛрдЪрддреЗ рд╣реИрдВ - рдбреЗрдЯрд╛рдмреЗрд╕ рдЖрдкрдХреЗ рд▓рд┐рдП рд╕рдм рдХреБрдЫ рдХрд░рддрд╛ рд╣реИред рдпрджрд┐ рдЖрдкрдХреЗ рдкрд╛рд╕ рдХрдИ рдбреЗрдЯрд╛рдмреЗрд╕, рдПрдХ рд╡рд┐рддрд░рд┐рдд рдкреНрд░рдгрд╛рд▓реА, рдпрд╛ рдпрд╣рд╛рдВ рддрдХ тАЛтАЛрдХрд┐, рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рд╕рдВрд╕реНрдХрд░рдг 4 рддрдХ MongoDB, рд╕рдм рдХреБрдЫ рдЗрддрдирд╛ рд░рд╕реАрд▓рд╛ рдирд╣реАрдВ рд╣реИред
рдПрдХ рдЙрджрд╛рд╣рд░рдг рдкрд░ рд╡рд┐рдЪрд╛рд░ рдХрд░реЗрдВ - рд╣рдо рдлрд╛рдЗрд▓ рдХреЛ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдореЗрдВ рд╕рд╣реЗрдЬрдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ рдФрд░ рдЗрд╕реЗ рджреЛ рджрд╕реНрддрд╛рд╡реЗрдЬреЛрдВ рдореЗрдВ рд▓рд┐рдВрдХ рдЬреЛрдбрд╝ рд╕рдХрддреЗ рд╣реИрдВред рдмреЗрд╢рдХ рд╣рдо рдкрд░рдорд╛рдгреБрддрд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ - рдпрд╛ рддреЛ рдлрд╝рд╛рдЗрд▓ рдХреЛ рд╕рд╣реЗрдЬрд╛ рдЬрд╛рддрд╛ рд╣реИ рдФрд░ рджрд╕реНрддрд╛рд╡реЗрдЬреЛрдВ рдореЗрдВ рдЬреЛрдбрд╝рд╛ рдЬрд╛рддрд╛ рд╣реИ, рдпрд╛ рди рд╣реА (рдХреИрдЯ-рдЗрдлреЗрдХреНрдЯреНрд╕ IO рдХрд╛ рдЙрдкрдпреЛрдЧ рдЗрд╕рдХреЗ рдмрд╛рдж рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ):
saveDataToFile(data) 
  .flatMap { file =>
    addFileRef(documentId, file) 
      .flatMap { result =>
        addFileRef(fileRegistry, file) 
          .flatMap { result =>
            ??? 
          }
          .handleErrorWith { error =>
            
            removeFileRef(documentId, file).attempt >> IO.raiseError(error)
          }
      }
      .handleErrorWith { error =>
        
        removeFile(file).attempt >> IO.raiseError(error)
      }
  }
? Pyramid of doom.
! , .
, , . ( ) "" .
- Saga, . / , . , . тАФ Scala .
тАФ :
final case class Action(
  perform:    ???,
  commit:     ???,
  compensate: ???
)
? - () => Try[T], IO тАФ , cats.
final case class Action(
  perform:    IO[Unit],
  commit:     IO[Unit],
  compensate: IO[Unit]
)
commit perform, compensate perform ?
:
final case class Action[T](
  perform:    IO[T],
  commit:     T => IO[Unit],
  compensate: T => IO[Unit]
)
, T тАФ .. perform.
:
def saveDataToFile(data: Data): IO[File] = ???
def removeFile(file: File): IO[Unit] = ???
def saveDataAction(data: Data): Action[File] = Action(
  perform = saveDataToFile(data),
  compensate = removeFile
)
тАФ
, , ? Seq[Action[_]] тАФ . тАФ , .
тАФ - ? .
:
final case class ActionChain[A, B](
  first: Action[A],
  next:  A => Action[B]
)
, тАФ . :
sealed trait Transaction[T]
final case class Action[T](...) extends Transaction[T]
final case class ActionChain[A, B](
  first: Transaction[A],
  next:  A => Transaction[B]
) extends Transaction[B]
! , :
ActionChain(
  saveDataAction(data),
  { file => 
    ActionChain(
      addFileRefAction(documentId, file),
      { _ => 
        addFileRefAction(fileRegistry, file)
      }
    )
  }
)
, .
, тАФ , ?
, , IO .
, . "" тАФ .
Transaction case' . , :
- тАФ
 
private def compile[R](restOfTransaction: T => IO[R]): IO[R] = this match {
  case Action(perform, commit, compensate) => perform.flatMap { t =>
    restOfTransaction(t).redeemWith(
      bind = commit(t).attempt >> IO.pure(_),
      recover = compensate(t).attempt >> IO.raiseError(_)
    )
  }
  case ActionChain(first, next) => ???
}
, CatsredeemWith , attempt / ( , ), >> " ", IO.pure IO.raiseError continue(t) тАФ .
 тАФ , :
private def compile[R](restOfTransaction: T => IO[R]): IO[R] = this match {
  case Action(perform, commit, compensate) => ...
  case ActionChain(first, next) =>
    first.compile { a =>
      next(a).compile { t =>
        restOfTransaction(t)
      }
    }
}
, :
sealed trait Transaction[T] {
  def compile: IO[T] = compile(IO.pure) 
}
IO , commit/compensate , ( / IO, ).
, . , ? , .
, . :
-, Action.compile(restOfTransaction):
perform , restOfTransaction ( T, perform )compile: perform, restOfTransaction ( T)perform , commit compensate restOfTransaction ( redeemWith)
ActionChain.compile(restOfTransaction) , Action' , compile':
transaction.compile(restOfTransaction) 
=== 
action1.compile(t1 => 
  action2.compile(t2 =>
    action3.compile(t3 => ...
      restOfTransaction(tn))))
, ActionChain.first ActionChain:
ActionChain(ActionChain(action1, t1 => action2), t2 => action3).compile(restOfTransaction) >>
ActionChain(action1, t1 => action2).compile(t2 => action3.compile(restOfTransaction)) >>
action1.compile(t1 => action2.compile(t2 => action3.compile(restOfTransaction))) []
Action.compile :
commit compensate
?
:
- ( )
 - ,
 - ???
 - PROFIT!
 
, :
ActionChain(
  saveDataAction(data),
  { file => 
    ActionChain(
      addFileRefAction(documentId, file),
      { _ => 
        addFileRefAction(fileRegistry, file)
      }
    )
  }
).compile
chain ActionChain, :
sealed trait Transaction[T] {
  def chain[R](f: T => Transaction[R]): Transaction[R] = ActionChain(this, f)
}
saveDataAction(data).chain { file =>
  addFileRefAction(documentId, file).chain { _ =>
    addFileRefAction(fileRegistry, file)
  }
}.compile
chain flatMap , , () !
, Scala, for cats, ( ).
, тАФ !
sealed trait Transaction[T] {
  def flatMap[R](f: T => Transaction[R]): Transaction[R] = ActionChain(this, f)
  def map[R](f: T => R): Transaction[R] = flatMap { t => Action(IO(f(t))) }
}
Scala, :
def saveDataAndAddFileRefs(data: Data, documentId: ID): Transaction[Unit] = for {
  file <- saveDataAction(data)
  _    <- addFileRefAction(documentId, file)
  _    <- addFileRefAction(fileRegistry, file)
} yield ()
saveDataAndAddFileRefs(data, documentId).compile
, тАФ . тАФ , .
рдХреЗ рд▓рд┐рдП catsрдЖрдк рд╕реНрдкрд╖реНрдЯ рд░реВрдк рд╕реЗ рд╕рдмреВрдд рд╣реИ рдХрд┐ рдШреЛрд╖рдгрд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХреА рдЬрд░реВрд░рдд рд╣реИ Transactionрдпрд╣ рдПрдХ рдЗрдХрд╛рдИ рд╣реИ - рдПрдХ typclass рдХрд╛ рдПрдХ рдЙрджрд╛рд╣рд░рдг:
object Transaction {
  implicit object MonadInstance extends Monad[Transaction] {
    override def pure[A](x: A): Transaction[A] = Action(IO.pure(x))
    override def flatMap[A, B](fa: Transaction[A])(f: A => Transaction[B]): Transaction[B] = fa.flatMap(f)
    
    override def tailRecM[A, B](a: A)(f: A => Transaction[Either[A, B]]): Transaction[B] = f(a).flatMap {
      case Left(a) => tailRecM(a)(f)
      case Right(b) => pure(b)
    }
  }
}
рдпрд╣ рд╣рдореЗрдВ рдХреНрдпрд╛ рджреЗрддрд╛ рд╣реИ? catsрдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рд╕реЗ рддреИрдпрд╛рд░ рддрд░реАрдХреЛрдВ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ :
val transaction = dataChunks.traverse_ { data =>
  saveDataAndAddFileRefs(data, documentId) 
}
transaction.compile 
рд╕рднреА рдХреЛрдб рдпрд╣рд╛рдВ рдЙрдкрд▓рдмреНрдз рд╣реИ: https://github.com/atamurius/scala-transactions