Categories
Uncategorized

Scala Data Validation Series Part 1- Why You Should Become Familiar With Monads (For Comprehensions) And Either[E, B] For Validating Data

My goal in this first part is very humble, to convince you to stop, for God’s sake, stop throwing exceptions during your data validation in Scala.

I will provide you with friendly incremental examples to make a case that…

Throwing exceptions in Scala is self limiting:

throw new Exception(“kaboom”) # Please never do this

And instead you should embrace the following:

  1. Either[Error, A] as your main data transport type for objects in your code
  2. And for comprehensions where you can compose a result.

This blog posting style, again, will be incremental. We will start by presenting a java like example, meaning code will respond to corrupt data by throwing exceptions. Then we will gradually move forward as we add scala features until we reach our goal, to take advantage of the capabilities of scala to produce cleaner scala code with powerful validation techniques.

There will be peaks and valleys of outcomes during this process so get ready for the ride!

I will be presenting scala 3.X code instead of scala 2.X code because I would love this blog to be relevant for at least a couple of years.

We will not be using external libraries. The reason I do this is because the reader should be aware that most scala shops are very opinionated on the types of libraries they use. Don’t be surprised if you land a job where you are not allowed to use cats, or ZIO or any library that uses macros.

There are multiple files that support this posting available in repo https://github.com/scala-blog/validation-part1/tree/v1.

Step 1: Throwing Exceptions All Over Town

File Example1JavaLikeValidation.scala summarizes how I often validated data with Java. We had validate() procedures that would either throw exceptions or just move on. If you look at the actual file, you will see all validate(*) methods throw exceptions when something is wrong.

Here is a snippet from where we do validations very much like we do in java from file Example1JavaLikeValidation.scala:

val goodEmail: String = "mrme@xmail.com"
val goodSSN: String = "111-11-2345"
val goodAge: String = "49"

validateEmail(goodEmail)
validateSSN(goodSSN)
validateAge(goodAge)

val goodResult: String =  f"email: ${goodEmail}, ssn: ${goodSSN}, age $goodAge}"

val badAge = "old"
val badSSN = "abc-22-2212"

validateAge(badAge)
validateSSN(badSSN)

val badResult: String =  f"email: ${goodEmail}, ssn: ${badSSN}, age $badAge}"

println(badResult)
println(goodResult)

Here are some issues I see with the code above:

  1. We will not be able to see the output of command println(goodResult) due to an exception thrown lines above. We can salvage this situation by adding a few try-catch, thus making the business logic much harder to understand.
  2. We will not know if badSSN was actually a bad SSN since we already threw an exception where we have validateAge(badAge) . What if we want to find a way to track all the times we had bad SSNs? We again, will have to rely on a series of try-catches and again, that would make the business logic harder to understand.
  3. Even when we validated all the fields, we are still dealing with strings. How do we know for a fact that string goodSSN is actually a social security number, or goodAge is actually a person’s age, or goodEmail is actually an email? This issue can become conflictive if the developer validating the fields is not the same as the developer putting together the business logic.
  4. We don’t know if an exception will be thrown after all validate(*) methods are completed. Sure in, in my simple example, it can be easy to tell. But complex code could fail anywhere. Can we handle unexpected exceptions in a better more “expected” way?
  5. Much of the validation process involves parsing data. Why not reuse much of this code to build actual data types for age, ssn and email to make our code safer?

Step 2: Adding Case Classes

In this step, we add a little flavor of scala by adding case classes. Hopefully we can address issue #3 from step 1 above. Here is a snippet from file Example2JavaLikeValidationWithCaseClasses.scala where we create objects representing real types:

case class SSN(area: Int, group: Int, serial: Int)
case class Email(user: String, domain: String)
case class Age(age: Int)

val goodEmail: String = "mrme@xmail.com"
val goodSSN: String = "111-11-2345"
val goodAge: String = "49"

validateEmail(goodEmail)
validateSSN(goodSSN)
validateAge(goodAge)

val splitEmail: Array[String] = goodEmail.split("@")
val emailUser = splitEmail(0)
val emailDomain = splitEmail(1)
val email: Email = Email(emailUser, emailDomain)

val ageInt = goodAge.toInt
val age: Age = Age(ageInt)

val ssnSplit = goodSSN.split("-")
val ssnArea = ssnSplit(0).toInt
val sshGroup = ssnSplit(1).toInt
val sshSerial = ssnSplit(2).toInt
val ssn: SSN = SSN(ssnArea, sshGroup, sshSerial)

val goodResult: String = f"email: ${email.user}@${email.domain}, ssn: ${ssn.area}-${ssn.group}-${ssn.serial}, age ${age.age}"
println(goodResult)

Case classes allows us to formalize the types of data we are dealing with. However, we have also added more issues by trying to introduce scala features.

Here is what we gained at this point:

  • We addressed issue #3 from step 1 above. We don’t deal with strings anymore, but with real emails, SSNs and ages.

But we also added more issues while trying to write actual scala code!

  1. For example, we are re executing the split method for validation and for data parsing. This type of duplicate execution is unavoidable when validation is separated from parsing.
  2. Code is becoming harder to read. All those split methods, array access methods are making the business logic unreadable.

Please, don’t ask yourself “what is the point of using scala” yet. Yes, this is a valley in the learning process. But keep following, as this is a necessary learning step.

Step 3: Using Factory Methods From Companion Objects

In this step we will clean up the mess we did in step #2 by adding factory methods inside companion objects. We delegate the responsibility of parsing and validating the string to companion objects.

Now the code looks a bit cleaner (Thank God!). Here is a snippet from file Example3JavaLikeValidationWithCaseClassesAndFactoryMethods.scala:

..
...
  case class Age private (age: Int)
  object Age:
    def fromString(string: String):Age =
      if(string == null)
        throw new Error("Email is null")
      else if(string.size == 0)
        throw new Error("Email is empty")
      else if(string.size != string.filter(_.isDigit).size)
        throw new Error(s"Email $string is malformed")
      else
        Age(string.toInt)
  
  
  val goodEmail: String = "mrme@xmail.com"
  val goodSSN: String = "111-11-2345"
  val goodAge: String = "49"
  
  
  val email: Email = Email.fromString(goodEmail)
  val age: Age = Age.fromString(goodAge)
  val ssn: SSN = SSN.fromString(goodSSN)
  
  val goodResult: String =  f"email: ${email.user}@${email.domain}, ssn: ${ssn.area}-${ssn.group}-${ssn.serial}, age ${age.age}"
  
  val badAge: String = "old"
  val badSSN: String = "abc-22-2212"

  val ageBroken: Age = Age.fromString(badAge)
  val ssnBroken: SSN = SSN.fromString(badSSN) // We will never make it to here and we will need spaghetti code to handle this case

  // never happens due to previous exceptions
  val badResult: String =  f"email: ${email.user}@${email.domain}, ssn: ${ssnBroken.area}-${ssnBroken.group}-${ssnBroken.serial}, age ${ageBroken.age}"
  

  println(badResult)
  println(goodResult)

In this step, we gained a little, namely..

  • Validation and building of data types for email, ssn, and age are delegated to methods within companion objects. This is a more logical placement for parsing and validating of data from strings instead of doing it a level above, as we have been doing it in step #2. Plus, we can avoid potentially re executing the same code, example, the split methods for Email and SSN parsing.

At this point, you may think we barely progressed from step #1. Don’t get too frustrated. It’s part of the learning process!

Step 4: Introducing Option[A] And For Comprehension

You are entering the no java zone. At this point we eliminate all exceptions and give you a sneak peek at the shape of how scala code should look like.

Here is a snippet from file Example4ValidationWithCaseClassesAndFactoryMethodsPlusOption.scala:

.
..
  case class Age private(age: Int)
  object Age:
    def fromString(string: String): Option[Age] =
      if (string == null)
        None
      else if (string.size == 0)
        None
      else if (string.size != string.filter(_.isDigit).size)
        None
      else
        Some(Age(string.toInt))

  val goodEmail: String = "mrme@xmail.com"
  val goodSSN: String = "111-11-2345"
  val goodAge: String = "49"
  
  
  val goodResult: Option[String] = for 
    email <- Email.fromString(goodEmail)
    age <- Age.fromString(goodAge)
    ssn <- SSN.fromString(goodSSN)
  yield f"email: ${email.user}@${email.domain}, ssn: ${ssn.area}-${ssn.group}-${ssn.serial}, age ${age.age}"
  
  val badAge: String = "old"
  val badSSN: String = "abc-22-2212"

  val badResult: Option[String] = for 
    email <- Email.fromString(goodEmail)
    age <- Age.fromString(badAge)
    ssn <- SSN.fromString(badSSN)
  yield f"email: ${email.user}@${email.domain}, ssn: ${ssn.area}-${ssn.group}-${ssn.serial}, age ${age.age}"  

  badResult match 
    case Some(good: String) => println(good)
    case None => println("I know something bad happened. Nothing else.") // this will print out

  goodResult match
    case Some(good: String) => println(good) // this will print out
    case None => println("I know something bad happened. Nothing else.")

There are two key incremental differences in this step:

  1. The factory methods on the companion objects now return Option[A] instead of the actual object of type , allowing a potential None object to be returned.
  2. We are using for comprehensions to glue the building of a result, Namely.
val goodResult: Option[String] = for 
email <- Email.fromString(goodEmail)
age <- Age.fromString(goodAge)
ssn <- SSN.fromString(goodSSN)
yield f"email: ${email.user}@${email.domain}, ssn: ${ssn.area}-${ssn.group}-${ssn.serial}, age ${age.age}"

At this point, I invite you to take a pause, meditate and fully digest this step if feel you are in unfamiliar territory.

In code above, if any of the fromString(*) methods return a None, the whole goodResult value becomes None. That is what the for comprehension does for you, for free, otherwise you get a burrito (container) of type Some[String] containing the entire correctly formatted string result.

And as usual, during this learning process, we gained some points but we lost some as well.

Here is what we gained at this step:

  1. This pattern allows you to have a clear view of the business logic, while the validation is done in the background. If any of the fromString(*) methods fail, goodResult becomes None.
  2. We can finally see the printed results of goodResult and badResult in the code, handled nicely addressing issue #1 in step #1.

However, unfortunately we also went backwards. In fact, we maybe worse off than step #1!

  • There is no error generation in this code! When a factory method fails, it just returns None, and we don’t know what None means.

We are basically wiping out any information about errors when we return None when a fromString(*) method cannot build a data object.

I intentionally added this step, not to frustrate you further, but as a convenient learning transition to Either[E, A]

Step 5: Replacing Option[A] With Either[E, A]

This is when stuff gets real. With Either[E, A] we can finally carry error information in a consistent manner when we return from fromString(*) methods.

Since Either[E, A] is a cousin of Option[A] (they both implement map and flatMap) we can still reuse much of the code from step above, including the for comprehensions we added in step #4 above.

Here is a snippet from file Example5ValidationWithEither.scala:

.
..
case class Age private(age: Int)
object Age:
  def fromString(string: String): Either[String, Age] =
    if (string == null)
      Left("Age is null")
    else if (string.size == 0)
      Left("Age is empty")
    else if (string.size != string.filter(_.isDigit).size)
      Left(s"Age is malfored : $string")
    else
      Right(Age(string.toInt))

  val goodEmail: String = "mrme@xmail.com"
  val goodSSN: String = "111-11-2345"
  val goodAge: String = "49"

  val goodResult = for
  email <- Email.fromString(goodEmail)
  age <- Age.fromString(goodAge)
  ssn <- SSN.fromString(goodSSN)
    yield f"email: ${email.user}@${email.domain}, ssn: ${ssn.area}-${ssn.group}-${ssn.serial}, age ${age.age}"
  
  val badAge = "old"
  val badSSN = "abc-22-2212"
  
  val badSsnEither: Either[String, SSN] = SSN.fromString(badSSN)
  
  val badResult = for
  email <- Email.fromString(goodEmail)
  age <- Age.fromString(badAge)
  ssn <- badSsnEither
    yield f"email: ${email.user}@${email.domain}, ssn: ${ssn.area}-${ssn.group}-${ssn.serial}, age ${age.age}"

  badResult match
    case Right(good) => println(good)
    case Left(e) => println(s"I know at least one bad thing bad happened. And I know what it is: ${e}") // this will print out

  goodResult match
    case Right(good) => println(good) // this will print out
    case Left(e) => println(s"I know at least one bad thing bad happened. And I know what it is: ${e}")

  badSsnEither match
    case Left(f) => println(s"BAD SSN TRACKER: ERROR: ${f}")

There are a lot of similarities from the previous step. However, we also added new and better stuff, namely:

  1. The fromString(*) methods now return Either[E, A] instead of Option[A]
  2. We brought back the error messages that we wiped out in step 4 (yey!). The errors are basically wrapped in a Left(errorString) expression.
  3. The good results are returned wrapped in Right(data). The convention is that what comes from Right(goodData) is the expected data object and what comes from Left(error) is bad because data was not parsed successfully.
  4. We track error details about failures in a safe manner using pattern matching (Example, check the last lines of the snippet code for this step)

Great! Finally we have achieved one significant gain…

  • We can easily track any bad data we wish without juggling with nested try-catch that makes business logic hard to understand. In this snippet I showed how we can track a bad SSN despite being parsed a few lines above without worrying about the order of failures.

If you reached this point with a good level of understanding, you are in the place where I am hoping you would be.

Step 6: Handling The Unexpected

You may have noticed I have not dealt with perhaps, the one elephant in the room, that even when we avoid throwing exceptions, it wont mean exceptions wont be thrown during execution.. duh. Perhaps, exceptions are thrown because we did something dumb like an out of index access. Or perhaps we are using libraries that throw exceptions under specific scenarios.

Despite the unexpected exceptions, we can “normalize” these cases and bring them into the scala realm. In this part, we convert exceptions to the Left part of Either[E, A]. Let me explain, consider this code, that doesn’t check for string being null:

object Email:
  def fromString(string: String): Either[String, Email] =
      val split = string.split("@")
      if (split.size != 2)
        Left(s"Email '${string}' is malformed")
      else
        Right(Email(user = split(0), domain = split(1)))

A null pointer exception will be thrown if string is null.

But wait.. does this mean we wasted all this time for nothing? What good is this blog if at the end of the day we can’t handle unexpected exceptions? Well, here is how we can handle these cases, including the cases where an external library may throw an unexpected exception:

object Email:
  def fromString(string: String): Either[String, Email] =
    Try {
      fromStringUnsafe(string)
    } match
      case Success(good) => good
      case Failure(exception) => Left(s"ERROR: Unexpected: parsing email '${string}': " + exception.getMessage)

  private def fromStringUnsafe(string: String): Either[String, Email] =
      val split = string.split("@")
      if (split.size != 2)
        Left(s"Email is malformed. Trying to parse '$string''")
      else
        Right(Email(user = split(0), domain = split(1)))

Of course, you may want to check for a null string, but keep in mind I am doing this to make a point. What we did above is to simply carry the information of an unexpected exception into the left side of the Either[E,A] which by default represents an error container. Perhaps, at this point you can perceive the type we use on the left side of the Either[E, A] in this blog (String) may be insufficient to carry the necessary error information. But that maybe a topic for another blog.

What did we achieve here? Simple..

  • We have handled unexpected exceptions using proper scala

File Example6HandlingTheUnexpected.scala shows how we change all factory methods to handle unexpected errors.

Step 7: Some Refactoring To Clean Up

We will do two refactorings in this step. We will refactor the Either[E, A] and we will also refactor the factory methods.

Using Either[String, A] all the time is cumbersome. If the left side is always a String, why do we need to repeat it all the time? We address this by using a type alias:

type EitherWithErrorString[A] = Either[String, A]

Now our factory method can look like the following instead:

object Email:
  def fromString(string: String): EitherWithErrorString[Email] =
    Try {
      fromStringUnsafe(string)
    } match
      case Success(good) => good
      case Failure(exception) => Left(s"ERROR: Unexpected: parsing email '${string}': " + exception.getMessage)
...
..
.

The next refactoring is to put the fromString method in a trait so it can be reused in Email, SSN, and Age companion objects.

The resulting modifications in this step can be seen in file Example7TypeAndFactoryMethodRefactoring.scala

Summary: What Did We Gain In This Blog?

Some readers maybe panicking after seeing our last scala example quite a few lines longer than the first example in step 1. But we are doing a lot more, as we are addressing all the issues that were explained. I put together a java example that attempts to do the same as step 6, with the goal to explain what it may look like if we didn’t use scala features.

Advertisement
Categories
scala.js series

Part 2: The SMAkkaR.js Stack- Using monocle and akka to facilitate model and component reusability in a react scala.js application

In our our previous blog we used akka to manage the state of our application. Here is a list of benefits we get so far:

  • Leveraging akka as the delegated entity to modify the state of a single page application.
  • Unlike redux, we can use type pattern matching to identify a message type, making the application far more robust.
  • We can also leverage the akka messaging pattern for all sorts of other cool applications such as authentication, tracking events, or state history.

In this blog, we will use the capabilities of monocle. Through monocle and akka, we will be able to easily design react components that can update themselves without the react component having to know where in the application model the data resides.

Let me explain with the example I am about to show you. If our application, has many counters in different hierarchy levels and in many different types of react components, we can update any of these counters using the same code! When a react component has a counter, neither component nor the update method need to know where the counter came from for the application to increment it. I hope the message sort of makes sense at this point. So let’s start!

We will start with the code from our previous blog or we can also use the repo for this blog and track the changes through commit tags.

Our first step is to run the application from previous blog repo or use this blog’s repo and checkout tag “initial-with-no-monocle”:

~/projects/my-app % sbt dev 

As discussed in previous blog, we should see two counters, and each of them should increment it’s value by clicking on it.

Setting up monocle

Let’s add monocle to our build.sbt file first:

libraryDependencies ++= Seq(
  "com.github.julien-truffaut" %%%  "monocle-core"  % "2.0.4",
  "com.github.julien-truffaut" %%%  "monocle-macro" % "2.0.4",
  "com.github.julien-truffaut" %%%  "monocle-law"   % "2.0.4" % "test"
)

Reload build.sbt file and make sure it compiles and runs the same way.

Adding our first lenses

We start by replacing the older case class update with update through lenses. For now, we will add the lenses into file hello.world.messaging.MessageHandler.scala

// File MessageHandler.scala
package hello.world.messaging

...
...

// we use these two lens objects to update each counter
  val counter1Lens   : Lens[Application, Counter] = GenLens[Application](_.counter1)
  val counter2Lens   : Lens[Application, Counter] = GenLens[Application](_.counter2)

  val system = ActorSystem("ApplicationStoreActorSystem")
...
...
    private def messageUpdate: PartialFunction[Any, Application] = {
      case IncrementCounter1 =>
        log.info(s"Increment  counter1")
        //models.topModel.app.copy(counter1 = Counter(models.topModel.app.counter1.value + 1))
        counter1Lens.modify(c => c.copy(c.value+1))(models.topModel.app)

      case IncrementCounter2 =>
        log.info(s"Increment counter2")
        //models.topModel.app.copy(counter2 = Counter(models.topModel.app.counter2.value + 1))
        counter2Lens.modify(c => c.copy(c.value+1))(models.topModel.app)
    }
  }

}

Moving our lenses to a react component as a methodology

Application should still run exactly the same way as before. But at this point, we will perform a series of steps so we can be more DRY, these two lenses look awfully similar don’t they?

In our quest for a more DRY solution, we will start by taking the lenses out of file MessageHandler.scala and move it to react component App.scala.

// File App.scala
package hello.world
import hello.world.messaging.ApplicationMessageListContainer.IncrementCounter
import hello.world.messaging.MessageHandler
import hello.world.models.{Application, Counter}
import slinky.core._
import slinky.core.annotations.react
import slinky.web.html._

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import monocle.{Lens, Optional}
import monocle.macros.GenLens
@JSImport("resources/App.css", JSImport.Default)
@js.native
object AppCSS extends js.Object

@JSImport("resources/logo.svg", JSImport.Default)
@js.native
object ReactLogo extends js.Object



object ApplicationProxy {
  var update:() => Unit = () => ()
}

@react class App extends StatelessComponent {
  type Props = Unit
  private val css = AppCSS

// We move our lenses to here
  val counter1Lens = GenLens[Application](_.counter1)
  val counter2Lens = GenLens[Application](_.counter2)


  override def componentWillMount() = {
    ApplicationProxy.update = () => {
      this.forceUpdate()
    }
  }
  def render() = {

    div(className := "App")(
      header(className := "App-header")(
        img(src := ReactLogo.asInstanceOf[String], className := "App-logo", alt := "logo"),
        h1(className := "App-title")("Welcome to React (with Scala.js!)")
      ),
      p(className := "App-intro")(
        "To get started, edit ", code("App.scala"), " and save to reload."
      ),
      button(
        s"Click to increment counter1 ${models.topModel.app.counter1.value}",
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter(counter1Lens)
        })
      ),
      br(),br(),
      button(
        s"Click to increment counter2 ${models.topModel.app.counter2.value}",
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter(counter2Lens)
        })
      )
    )
  }
}

Don’t try running anything as you will see compile errors at this point. Now we update our message types so they can contain a lens by updating file ApplicationMessageListContainer.scala:

// File ApplicationMessageListContainer.scala
package hello.world.messaging

import hello.world.models.{Application, Counter}
import monocle.Optional

trait ApplicationFrontEndMessage

object ApplicationMessageListContainer {
    case class IncrementCounter1(lens: Lens[Application, Counter]) extends ApplicationFrontEndMessage
    case class IncrementCounter2(lens: Lens[Application, Counter]) extends ApplicationFrontEndMessage

}

And finally, we remove the lenses from MessageHandler.scala and modify the message handling:

// File MessageHandler.scala
...
...
// remove lenses from here!
//  val counter1Lens   : Lens[Application, Counter] = GenLens[Application](_.counter1)
//  val counter2Lens   : Lens[Application, Counter] = GenLens[Application](_.counter2)
...
..
    private def messageUpdate: PartialFunction[Any, Application] = {
      case m:IncrementCounter1 =>
        log.info(s"Increment  counter1")
        // now the lens is in the message!
        m.lens.modify(c => c.copy(c.value+1))(models.topModel.app)

      case m:IncrementCounter2 =>
        log.info(s"Increment counter2")
        // now the lens is in the message!
        m.lens.modify(c => c.copy(c.value+1))(models.topModel.app)
    }
  }

}

Now stuff should work exactly as before. Make sure you have all the imports available.

Generalizing our message handler so we can be fully DRY

It looks a bit cleaner, but we are not DRY yet. We notice Increment* messages are identical at this point. Let’s make a single IncrementCounter message!

We start by modifying our message type in file ApplicationMessageListContainer.scala:

// File ApplicationMessageListContainer.scala
package hello.world.messaging

import hello.world.models.{Application, Counter}
import monocle.Optional

trait ApplicationFrontEndMessage

object ApplicationMessageListContainer {
  //object IncrementCounter1 extends ApplicationFrontEndMessage
  //object IncrementCounter2 extends ApplicationFrontEndMessage

  case class IncrementCounter(lens:Optional[Application, Counter]) extends ApplicationFrontEndMessage
}

Notice that we are no longer using Lens[_,_] types, we are using Optional which is a more flexible lens that allows us to deal with Map, List, Vector etc.

In our App.scala we perform the following changes:

// File App.scala
...
import monocle.{Lens, Optional}
import monocle.macros.GenLens
...
// Notice we are now using Optional
  val counter1Lens   : Optional[Application, Counter] = GenLens[Application](_.counter1).asOptional
  val counter2Lens   : Optional[Application, Counter] = GenLens[Application](_.counter2).asOptional
  val counterGroupLens   : Optional[Application, Vector[Counter]] = GenLens[Application](_.counterList).asOptional
..
..
  def render() = {
...
...
      button(
        s"Click to increment counter1 ${models.topModel.app.counter1.value}",
// One message used IncrementCounter
        onClick := (_ => {
// One message used IncrementCounter
          MessageHandler.actor ! IncrementCounter(counter1Lens)
        })
      ),
      br(),br(),
      button(
        s"Click to increment counter2 ${models.topModel.app.counter2.value}",
// One message used IncrementCounter
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter(counter2Lens)
        })
      ),
    )
  }
}

At this point the app should be up and running, and again with exactly the same behaviour as usual.

At this point, we were able to generalize the update counter model. However, there is no evidence how reusable this approach is through different react components in different hierarchies. This next session addresses this very matter.

Passing monocle lenses through react properties and composing them to enable component and model reusability

We will add a new react component to this application, a group of counters in it’s own visual react component. These counters will use the same message handler to update the models. We will compose lenses to achieve this.

Let’s create the new component first.

// New file CounterListComponent
package hello.world

import hello.world.messaging.MessageHandler
import hello.world.messaging.ApplicationMessageListContainer._
import hello.world.models.{Application, Counter}
import slinky.core._
import slinky.core.annotations.react
import slinky.web.html._
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import monocle.{Iso, Lens, Optiona}
import monocle.function.At
import monocle.macros.GenLens
import monocle.function.all._
import monocle.function.At.at
import monocle.std.list._



@react class CounterListComponent extends StatelessComponent {
  type Props = Optional[Application, Vector[Counter]]
  private val css = AppCSS


  def render() = {

    val counterListLens = props  // for doc purposes

    // our lens gives two way access to Vector[Counter], get and update
    val counterListOption = counterListLens.getOption(models.topModel.app)

    val counterDomElementList = counterListOption match {
      case Some(list) => list.zipWithIndex.map(c => {
// We compose a lens per counter
        val counterLens: Optional[Application, Counter] = props composeOptional index(c._2)
        div(
          style := js.Dynamic.literal(
            padding = "10px",
          ),
          button(
            s"click here to increment: ${
              c._1.value
            }",
            onClick := (_ => {
              MessageHandler.actor ! IncrementCounter(counterLens)
            })
          )
        )
      }
      )
    }


    div(
      className := "CounterList",
      style := js.Dynamic.literal(
        padding = "0px 150px 0px 150px",
      )
    )(
      div(
        style := js.Dynamic.literal(
          background = "deeppink",
          padding = "10px",
        ),
        counterDomElementList
      )
    )
  }
}

The code above is creating a lens for each counter inside the map. In the onClick we send the lens to the actor. We expect this message to end in the same place in MessageHandler.scala as the other messages passed in App.scala.

Now we include component CounterListComponent in App.scala:

// File App.scala
package hello.world

...
...
        s"Click to increment counter2 ${models.topModel.app.counter2.value}",
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter(counter2Lens)
        })
      ),
      br(),br(),
// new component added
      CounterListComponent(counterGroupLens)
    )
  }
}

At this point the application should look like the following. And you should be able to increment any of the counter buttons independently:

Cool isn’t it? Now lets summarize on what we achieved in this blog:

  • Passing lenses as react properties
    • Allows for the react component to be oblivious of where the data is coming from. This is not a big deal if the component is read only, but it’s a great deal if the react component needs to modify itself.
  • Composing lenses as they get passed through component hierarchies enables separation of concern
    • Stateful react components don’t need to care where the data is placed in the data model. The same goes for it’s child components.

And here is how we do it

  • Lenses are created within react components
  • Pass lenses as react properties, not so much data
  • Compose lenses as you pass them as properties to your child components
  • Generalize model updates through akka messages and pattern matching

Categories
scala.js series

Part 1: Using akka and react to organize your single page scala.js application

This blog should resonate specially to those users who are looking for something like Redux to organize a single page React+Scala.js application.

We will try to manage a single page application from a single nested case class. All updates to this model will be delegated to an AKKA actor. Later in this blog, we will be using Monocle to simplify the update of the application model.

The way we are going to do this is by having two separate buttons. Each button will show an integer that will be incremented with a click.

We will start very simple, and as we progress, we will enhance our code.

Project setup

We will start simple from the react slinky template.

% sbt new shadaj/create-react-scala-app.g8

For the sake of consistency, please keep the default values. After running command dev under sbt you should see the following browser content if everything goes well:

~/projects/my-app % sbt dev 

Now we add the akka dependencies to your build.sbt:

// build.sbt
libraryDependencies += "org.akka-js" %%% "akkajsactor" % "2.2.6.5"
libraryDependencies += "org.akka-js" %%% "akkajsactortyped" % "2.2.6.5"

At this point, we are ready for coding!

We then setup the application model

We will be creating a package called “models” under our default package “hello.world”. We then create file Application.scala with the following code:

// File Application.scala
package hello.world.models

case class Counter(value: Integer = 1)

case class ApplicationContainer(var app: Application = Application())

case class Application
(
  counter1: Counter = Counter(),
  counter2: Counter = Counter()
)

That is it for our application model. We keep it simple.

Next we create a package file under “models” . We use this package to retrieve and initialize our application model:

//package.scala
package hello.world

package object models {
  val topModel = ApplicationContainer()
}

We then render our buttons showing the counter values

Now we add our buttons. At this point, they won’t do anything at all. We edit file App.scala and add 2 html buttons inside method render():

// File App.scala
....
..
def render() = {

    div(className := "App")(
      header(className := "App-header")(
        img(src := ReactLogo.asInstanceOf[String], className := "App-logo", alt := "logo"),
        h1(className := "App-title")("Welcome to React (with Scala.js!)")
      ),
      p(className := "App-intro")(
        "To get started, edit ", code("App.scala"), " and save to reload."
      ),
// add the following: start
      button(
        s"Click to increment counter1 ${models.topModel.app.counter1.value}"
      ),
      br(),br(),
      button(
        s"Click to increment counter2 ${models.topModel.app.counter2.value}"
      )
// end of additions
    )
  }

At this point, you should see two additional, non functional buttons, available in the browser:

One ugly hack is required to make this work, just one

In order for the counters to increment, we need to tell the react application to update. For this to work, we need to access react method forceUpdate() externally from an akka actor. This is done by modifying the following in App.scala:

// File App.scala
....
// Added ApplicationProxy for external access of forceUpdate()
object ApplicationProxy {
  var update:() => Unit = () => ()
}

@react class App extends StatelessComponent {
  type Props = Unit
  private val css = AppCSS

// Added componentWillMount() to "steal" forceUpdate() out of react
  override def componentWillMount() = {
    ApplicationProxy.update = () => {
      this.forceUpdate()
    }
  }

  def render() = {
.....

Enter the awesomeness of akka

Akka deals with messages. This means we first need to build our message types before we do anything. Since we only increment two counters, we are going to have two message names, IncrementCounter1 and IncrementCounter2. We create package “messaging” under root package “hello.world” and then create file ApplicationMessageListContainer.scala:

// File ApplicationMessageListContainer.scala
package hello.world.messaging

trait ApplicationFrontEndMessage

object ApplicationMessageListContainer {
  object IncrementCounter1 extends ApplicationFrontEndMessage
  object IncrementCounter2 extends ApplicationFrontEndMessage
}

Now we are ready to create the messaging code. Under same package “hello.world.messaging” we create file MessageHandler.scala:

// File MessageHandler.scala
package hello.world.messaging

import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
import hello.world.messaging.ApplicationMessageListContainer.{IncrementCounter1, IncrementCounter2}
import hello.world.{ApplicationProxy, models}
import hello.world.models.{Application, Counter}

object MessageHandler {

  val system = ActorSystem("ApplicationStoreActorSystem")

  val actor = system.actorOf(Props[ApplicationActor], name = "ApplicationInfoActor")

  class ApplicationActor extends Actor with ActorLogging {
    def receive = {
      case m: ApplicationFrontEndMessage => {
        models.topModel.app = messageUpdate(m)
        ApplicationProxy.update()
      }
    }

    private def messageUpdate: PartialFunction[Any, Application] = {
      case IncrementCounter1 =>
        log.info(s"Increment  counter1")
//monocle will improve this ugly update
        models.topModel.app.copy(counter1 = Counter(models.topModel.app.counter1.value + 1))


      case IncrementCounter2 =>
        log.info(s"Increment counter2")
//monocle will improve this ugly update
        models.topModel.app.copy(counter2 = Counter(models.topModel.app.counter2.value + 1))
    }
  }

}

The messaging system is basically copy-modifying the current model. With a 2 level case class, it already looks pretty ugly. We will be using monocle to address this ugliness in my next blog.

Of course, we need to add a listener to our buttons (onClick) in file App.scala for the buttons to trigger changes:

// File App.scala
package hello.world

import hello.world.messaging.MessageHandler
import hello.world.messaging.ApplicationMessageListContainer._
import slinky.core._
import slinky.core.annotations.react
import slinky.web.html._
.......
....
  def render() = {
    div(className := "App")(
      header(className := "App-header")(
        img(src := ReactLogo.asInstanceOf[String], className := "App-logo", alt := "logo"),
        h1(className := "App-title")("Welcome to React (with Scala.js!)")
      ),
      p(className := "App-intro")(
        "To get started, edit ", code("App.scala"), " and save to reload."
      ),
      button(
        s"Click to increment counter1 ${models.topModel.app.counter1.value}",
//onClick added
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter1
        })
      ),
      br(),br(),
      button(
        s"Click to increment counter2 ${models.topModel.app.counter2.value}",
//onClick added
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter2
        })
      )
    )
  }

Now take the app for a ride and click on those buttons, they should increment as you click on them:

Not too bad isn’t it? Now you have a single page app with a model organized by akka messaging. In my next blog, we will add monocle to manage our application model.

Code for this blog can be accessed through the following repo:

https://github.com/scala-blog/akka-react-no-monocle/tree/master