Scala.js and Algebraic Data Types

Last time I started the discourse about the place of Scala in front-end development. My point, roughly , is that it should take business logic and leave view rendering to JavaScript. But some difficulties can appear while someone tries to bind both languages together. In my previous post I talked about Scala – JavaScript classes two-way conversion. Today I’m going to make one step further and take an example with Algebraic Data Types.

Say, we have to develop an application that helps to deal with stock trading. We will buy and sell assets and our program will keep all records of these operations. Moreover, we’ll be able to undo last operations, get error messages if we haven’t got enough cash or asset.

As in the previous example we will keep all information in the application State. We can transform this state by applying the appropriate method called from JavaScript. The main difference from previous example – the method can take an object of two different types as an argument. If we want to buy we will put an object of the Buy type. Vice versa, if we need to sell we will put an object of the Sell type. It’s very easy to do it in Scala:

sealed trait Message

case class Sell(
name: String,
amount: Int,
price: Int
) extends Message


case class Buy(
name: String,
amount: Int,
price: Int
) extends Message

Unfortunately, we can’t use traits or interfaces in JavaScript. But we can use abstract classes instead:

@ScalaJSDefined
abstract class MessageJs extends js.Object {
val tipe: String
}

@ScalaJSDefined
class SellJs extends MessageJs {
val tipe = "sell"
var name: String = _
var amount: Int = _
var price: Int = _

}

@ScalaJSDefined
class BuyJs extends MessageJs {
val tipe = "buy"
var name: String = _
var amount: Int = _
var price: Int = _
}

As you can see, I need an additional field ‘tipe’ to store information about a type. I use a companion object to create constructor methods:

object SellJs {
def apply(_name: String, _amount: Int, _price: Int): SellJs =
new SellJs {
amount = _amount
price = _price
name = _name
}
}

object BuyJs {
def apply(_name: String, _amount: Int, _price: Int): BuyJs =
new BuyJs {
amount = _amount
price = _price
name = _name
}
}

I use “monkey patching” to add convert methods to instances of types Message and MessageJs:

object MessageImplicits {

implicit class MessageToBack(msg: MessageJs) {
def convert(): Message =
msg match {
case x if x.tipe == "sell" =>
val y = x.asInstanceOf[SellJs]
Sell(y.name, y.amount, y.price)
case x if x.tipe == "buy" =>
val y = x.asInstanceOf[BuyJs]
Buy(y.name, y.amount, y.price)
}
}

implicit class MessageToFront(msg: Message) {
def convert(): MessageJs =
msg match {
case Sell(x, y, z) => SellJs(x, y, z)
case Buy(x, y, z) => BuyJs(x, y, z)
}
}

implicit def messageFrontToBack(msgJs: MessageJs): Message =
msgJs.convert()


implicit def messageBAckToFront(msg: Message): MessageJs =
msg.convert()

}

In the State I ‘ll store information about my cash, assets, and transactions:

case class Asset(
name: String,
amount: Int
)

case class AppState(
cash: Int = 10000,
assets: Map[String, Asset] = Map.empty,
transactions: List[Message] = List.empty
)

The companion object of a case class AppState will store the State itself. Moreover it will include methods to work with the State. The major one of them is the process method that takes a JavaScript object of the MessageJs type and returns a String. Actually it calls the processMessage private method, which will do all the work: it takes a Message instance as a parameter, changes the State in accordance with the class of this message (Buy or Sell) and returns a response of the Response type.

The other interesting method is undo. It cancels the last operation produced by the process method.

@JSExportTopLevel("AppState")
object AppState {

private var state = AppState()

@JSExport
def process(msg: MessageJs): String = processMessage(msg)

private def processMessage(msg: Message): Response = msg match {

case Buy (x, y, z) if y * z <= state.cash =>
state = state.copy(
cash = state.cash - y * z,
assets = state.assets + (x -> state.assets.get(x)
.map(a => a.copy(amount = a.amount + y))
.getOrElse(Asset(x, y))),
transactions = Buy (x, y, z) :: state.transactions
)
OK
case Buy (_, _, _) => NotEnoughMoney
case Sell (x, y, z) if y <= state.assets.get(x)
.map(_.amount).getOrElse(0) =>

val asset: (String, Option[Asset]) = x -> state.assets.get(x)
.map(a => a.copy(amount = a.amount - y))
state = state.copy(
cash = state.cash + y * z,
assets = asset match {
case (_, None )=> state.assets
case (_, Some(v)) => v match {
case Asset(n, 0) => state.assets - n
case Asset(n, a) => state.assets + (n -> Asset(n, a))
}
},
transactions = Sell(x, y, z) :: state.transactions
)
OK
case Sell (_, _, _) => NotEnoughAsset

}

@JSExport
def countTransactions(): Int = state.transactions.size

@JSExport
def countAssets(): Int = state.assets.size

@JSExport
def assetAmount(name: String): Int = state.assets.get(name)
.fold(0)(_.amount)

@JSExport
def cash(): Int = state.cash

@JSExport
def undo(): String = {
val transaction: Option[Message] = state.transactions.headOption

val result: Response = transaction
.fold ( NoTransactions.asInstanceOf[Response]) {

case Buy(x, y, z) => processMessage(Sell(x, y, z))
case Sell(x, y, z) => processMessage(Buy(x, y, z))
}

if(result == OK) state = state.
copy(transactions = state.transactions.tail.tail)

result
}

}

Here is another ADT. But with two differences from the first case:

  • I have a String type on the JavaScript side
  • I need only one-way conversion: from Scala to JavaScript

sealed trait Response {
def convert(): String
}

case object OK extends Response {
def convert(): String = "OK"
}

case object NotEnoughMoney extends Response {
def convert(): String = "Not enough money"
}

case object NotEnoughAsset extends Response {
def convert(): String = "Not enough asset"
}

case object NoTransactions extends Response {
def convert(): String = "No transactions"

}

object Response {
implicit def ResponseToString[T <: Response](resp: T): String =
resp.convert()
}

To make sure that it all works I wrote some tests:

" all methods on the State must work" in {

val sell = SellJs("A", 10, 10)

assert(AppState.process(sell) == "Not enough asset")

assert(AppState.cash() == 10000)

assert(AppState.assetAmount("A") == 0)

assert(AppState.countAssets() == 0)

assert(AppState.countTransactions() == 0)

assert(AppState.undo() == "No transactions")

val buy = BuyJs("A", 1000, 100)

assert(AppState.process(buy) == "Not enough money")

assert(AppState.undo() == "No transactions")

val buy1 = BuyJs("A", 100, 10)
val buy2 = BuyJs("B", 100, 5)
val sell1 = SellJs("A", 10, 10)

assert(AppState.process(buy1) == "OK")
assert(AppState.process(buy2) == "OK")
assert(AppState.process(sell1) == "OK")

assert(AppState.assetAmount("A") == 90)
assert(AppState.cash() == 8600)

assert(AppState.countTransactions() == 3)


assert(AppState.undo() == "OK")

assert(AppState.countTransactions() == 2)

assert(AppState.assetAmount("A") == 100)
assert(AppState.cash() == 8500)


assert(AppState.undo() == "OK")

assert(AppState.assetAmount("A") == 100)
assert(AppState.assetAmount("B") == 0)
assert(AppState.cash() == 9000)

assert(AppState.countTransactions() == 1)


}

To make sure that it all really work on the browser side I wrote some JavaScript:

var sell = {tipe: 'sell', name: 'A', amount: 10, price: 10};

console.log(AppState.process(sell));
console.log(AppState.cash());
console.log(AppState.assetAmount('A'));
console.log(AppState.countAssets());
console.log(AppState.countTransactions());
console.log(AppState.undo());

var buy = {tipe: 'buy', name: 'A', amount: 1000, price: 100};
console.log(AppState.process(buy));
console.log(AppState.undo());

var buy1 = {tipe: 'buy', name: 'A', amount: 100, price: 10};
var buy2 = {tipe: 'buy', name: 'B', amount: 100, price: 5};
var sell1 = {tipe: 'sell', name: 'A', amount: 10, price: 10};

console.log(AppState.process(buy1));
console.log(AppState.process(buy2));
console.log(AppState.process(sell1));

console.log(AppState.assetAmount('A'));
console.log(AppState.cash());
console.log(AppState.countTransactions());
console.log(AppState.undo());
console.log(AppState.countTransactions());
console.log(AppState.assetAmount('A'));
console.log(AppState.cash());
console.log(AppState.undo());
console.log(AppState.assetAmount('A'));
console.log(AppState.assetAmount('B'));
console.log(AppState.cash());
console.log(AppState.countTransactions());



To be continued…


See the code on github




About Alexandre Kremlianski

Scala / Scala.js / JavaScript programmer

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.