scp2: report
This commit is contained in:
200
scala_project_2025/bank_system/report.md
Normal file
200
scala_project_2025/bank_system/report.md
Normal file
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: "scala project part 2"
|
||||
date: 2025-11-07
|
||||
author: fredrik robertsen
|
||||
margin:
|
||||
x: 3.5cm
|
||||
y: 4cm
|
||||
fontsize: 14pt
|
||||
mainfont: libertinus serif
|
||||
lineheight: 1.5
|
||||
pagesize: a4
|
||||
---
|
||||
|
||||
# task 3
|
||||
|
||||
## how does it all work?
|
||||
|
||||
we essentially implement a queue that we can put our transactions in. these
|
||||
transactions are then processed in turn by the bank using threads. to ensure
|
||||
thread safety we are writing the core functionality purely, i.e. returning
|
||||
new instances of accounts rather than modifying them, which could incur race
|
||||
conditions. immutable state makes things easy to work with.
|
||||
|
||||
all we need are some accounts with a balance and a unique id, some registry to
|
||||
track these accounts and some checks to make sure that a transaction is
|
||||
processed correctly, i.e. that there are enough available funds.
|
||||
|
||||
then we can model the transaction as a finite state automaton using a state enum,
|
||||
making it succeed if enough money is available, otherwise fail.
|
||||
|
||||
## what was easy
|
||||
|
||||
implementing the `Account`s, `TransactionPool`s, `Transaction`s and the simpler
|
||||
parts of `Bank`, such as `createAccount` and `getAccount` were all easy tasks,
|
||||
only requiring some lookups for what is available in scala/java.
|
||||
|
||||
these didn't require a lot of work or many lines of code per implementation,
|
||||
since most of it is just setters and getters.
|
||||
|
||||
## what was hard
|
||||
|
||||
transaction processing in `Bank`.
|
||||
|
||||
proper case handling to ensure correct state often becomes unwieldy, and this
|
||||
was no exception. it was a case of running the tests, seeing what failed, then
|
||||
reasoning about why it failed, then attempting to patch it by inserting some
|
||||
if/else statements that hopefully fail certain transactions that should have
|
||||
been failed.
|
||||
|
||||
even then, it wasn't very hard, as this is only a toy program with very few
|
||||
moving parts. we are doing ourselves a great service by using immtutable state,
|
||||
making it much easier to figure out what part of the code must be wrong when
|
||||
tests fail.
|
||||
|
||||
## implementation
|
||||
|
||||
i will be going three the three main pain-points that had the most impact on
|
||||
making the tests pass.
|
||||
|
||||
the first is enqueuing a `Transaction`:
|
||||
|
||||
```scala
|
||||
def transfer(from: String, to: String, amount: Double): Unit = {
|
||||
if (amount <= 0) {
|
||||
val t = new Transaction(from, to, amount)
|
||||
t.fail()
|
||||
completedTransactions.add(t)
|
||||
return
|
||||
}
|
||||
|
||||
val f = getAccount(from)
|
||||
val t = getAccount(to)
|
||||
|
||||
if (f.isEmpty || t.isEmpty) {
|
||||
val t = new Transaction(from, to, amount)
|
||||
t.fail()
|
||||
completedTransactions.add(t)
|
||||
return
|
||||
}
|
||||
|
||||
if (f.get.balance < amount) {
|
||||
val t = new Transaction(from, to, amount)
|
||||
t.fail()
|
||||
completedTransactions.add(t)
|
||||
return
|
||||
}
|
||||
|
||||
transactionsPool.add(new Transaction(from, to, amount))
|
||||
}
|
||||
```
|
||||
|
||||
the initial TODO-comment only mentioned to ad the transaction to the pool. but
|
||||
upon running the tests it quickly became apparent that we need some case
|
||||
handling for when transactions are illegal. though i'm sure i could've put this
|
||||
in `Transaction` and that would've probably been better, i just did it here. in
|
||||
short: i'm ensuring that amount is positive, accounts exist in our registry and
|
||||
there is enough money in the account who is transfered from.
|
||||
|
||||
then processesing the transactions wasn't so bad using the comments that were
|
||||
already in place. we also get to use some neat scala syntax, like
|
||||
`filter(_.isPending)`:
|
||||
|
||||
```scala
|
||||
def processTransactions: Unit = {
|
||||
|
||||
val workers: List[Thread] = transactionsPool.iterator.toList
|
||||
.filter(_.isPending)
|
||||
.map(processSingleTransaction)
|
||||
|
||||
workers.map(element => element.start())
|
||||
workers.map(element => element.join())
|
||||
|
||||
val succeded: List[Transaction] =
|
||||
transactionsPool.iterator.toList.filter(_.succeeded)
|
||||
|
||||
val failed: List[Transaction] =
|
||||
transactionsPool.iterator.toList.filter(_.failed)
|
||||
|
||||
succeded.map(transactionsPool.remove(_))
|
||||
succeded.map(completedTransactions.add(_))
|
||||
|
||||
failed.map(t => {
|
||||
t.setPending()
|
||||
t.incrementAttempt()
|
||||
if (t.exceeded) {
|
||||
transactionsPool.remove(t)
|
||||
completedTransactions.add(t)
|
||||
}
|
||||
})
|
||||
|
||||
if (!transactionsPool.isEmpty) {
|
||||
processTransactions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
then lastly we have to actually process the transactions, one at a time. i first
|
||||
did this:
|
||||
|
||||
```scala
|
||||
private def processSingleTransaction(t: Transaction): Thread =
|
||||
new Thread(() => {
|
||||
accountsRegistry.synchronized {
|
||||
val fromOpt = getAccount(t.from)
|
||||
val toOpt = getAccount(t.to)
|
||||
|
||||
(fromOpt, toOpt) match {
|
||||
case (Some(from), Some(to)) =>
|
||||
from.withdraw(t.amount) match {
|
||||
case Right(updatedFrom) =>
|
||||
to.deposit(t.amount) match {
|
||||
case Right(updatedTo) =>
|
||||
accountsRegistry(t.from) = updatedFrom
|
||||
accountsRegistry(t.to) = updatedTo
|
||||
t.succeed()
|
||||
case Left(_) => t.fail()
|
||||
}
|
||||
case Left(_) => t.fail()
|
||||
}
|
||||
case _ => t.fail()
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
here we are getting the accounts as Options, then pattern matching to obtain the
|
||||
accounts, otherwise failing the transaction. then performing a withdrawal of the
|
||||
amount, followed by a deposit, then updating the registry with new accounts with
|
||||
updated balances. only then will the transaction succeed.
|
||||
|
||||
but my time with haskell has taught me that this exact pattern is a monadic
|
||||
pattern. in haskell, we could use `do`-notation to remove all this fluff where
|
||||
we are failing, and instead focus on the parts that don't lead to a failure.
|
||||
i already know `Option` and `Either` are monads, so this pattern is fair to
|
||||
expect. so i looked at how to emulate this syntax in scala, and lo and behold;
|
||||
we have `do`-notation in scala in the form of `for`/`yield`:
|
||||
|
||||
```scala
|
||||
private def processSingleTransaction(t: Transaction): Thread =
|
||||
new Thread(() => {
|
||||
accountsRegistry.synchronized {
|
||||
for {
|
||||
from <- getAccount(t.from)
|
||||
to <- getAccount(t.to)
|
||||
updatedFrom <- from.withdraw(t.amount).toOption
|
||||
updatedTo <- to.deposit(t.amount).toOption
|
||||
} yield {
|
||||
accountsRegistry(t.from) = updatedFrom
|
||||
accountsRegistry(t.to) = updatedTo
|
||||
t.succeed()
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
with this refactor, we still pass the tests and obtain much more readable code.
|
||||
it's easy to read that we are getting the accounts, getting new accounts from
|
||||
some updates (wrapped as options, failing if the we obtain a `Left`), then we do
|
||||
some work with these values if we succeed with all steps, i.e. updating the
|
||||
registry. very beautiful!
|
||||
2693
scala_project_2025/bank_system/report.pdf
Normal file
2693
scala_project_2025/bank_system/report.pdf
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user