Introduction
In the previous post, we set up a new Scala project and made sure everything was working. Now, let’s start building our game by defining the model.
We’ll start with the most basic Hero
model and then explore how we can leverage Scala’s powerful type system and libraries to make our code more type-safe.
Case class
We can start with a simple case class to represent our hero. A case class is a special type of class in Scala that is used to represent immutable data.
Let’s create a Hero
class:
case class Hero(
id: String,
name: String,
level: Int
)
And let’s try to create a hero:
def main(): Unit =
val hero = Hero("1", "Hero", 0)
println(hero) // Hero(1,Hero,0)
It looks good, but nothing prevents us from creating the following hero:
def main(): Unit =
val hero = Hero("Hero", "1", -1)
println(hero) // Hero(Hero,1,-1)
We ended up with ID Hero, the name 1, and negative level! We don’t want to make that possible. We need to make invalid states unrepresentable.
Dependencies
We’ll add our first library - iron . It’ll allow us to define types with very strict constraints.
To keep your IntelliJ project in sync, enable Auto reload or manually sync sbt changes .
To add the first dependency, create a new file project/Dependencies.scala
with the following code:
import sbt.*
object Dependencies {
private object Version {
val iron = "3.0.0"
}
lazy val iron: ModuleID = "io.github.iltotore" %% "iron" % Version.iron
}
Then, update build.sbt
:
lazy val dependencies = {
Seq(Dependencies.iron)
}
lazy val root = (project in file("."))
.settings(
name := "rpg",
idePackagePrefix := Some("dev.kamgy"),
libraryDependencies ++= dependencies
)
Iron
Let’s say that we want to make sure that:
- the Hero ID is a UUID
- the Hero name must be alphanumeric, 5 to 32 characters long
- the Hero level is a positive integer
Let’s make our Hero
model more strict. This is a big step. If you’re new to refined types
, it might take a moment to wrap your head around them.
Create a new file scala/hero/model/HeroModel.scala
and add the following code:
package dev.kamgy
package hero.model
import java.util.UUID
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.collection.MinLength
import io.github.iltotore.iron.constraint.numeric.Positive
import io.github.iltotore.iron.constraint.string.Alphanumeric
object HeroModel {
type HeroId = HeroId.T
object HeroId extends RefinedType[UUID, Pure] {
def generate(): HeroId = HeroId.applyUnsafe(UUID.randomUUID())
}
type HeroName = HeroName.T
object HeroName extends RefinedType[String, Alphanumeric & MinLength[5] & MaxLength[32]]
type HeroLevel = HeroLevel.T
object HeroLevel extends RefinedType[Int, Positive]
}
Then, create a new file scala/hero/model/Hero.scala
and add the following code:
package dev.kamgy
package hero.model
import hero.model.HeroModel.*
case class Hero(
id: HeroId,
name: HeroName,
level: HeroLevel
)
Let’s do a quick experiment in main.scala
:
package dev.kamgy
import hero.model.Hero
import hero.model.HeroModel.*
@main
def main(): Unit =
val hero = Hero(HeroId.generate(), HeroName("SuperHero"), HeroLevel(21))
println(hero) // Hero(1440c791-d92a-4596-8b73-2e5579fea403,SuperHero,21)
This should work fine.
Now, let’s try to create a hero with an invalid name:
def main(): Unit =
Hero(HeroId.generate(), HeroName("Bad"), HeroLevel(21))
// Constraint Error: Should be alphanumeric & Should have a minimum length of 5 & Should have a maximum length of 32
The name is invalid, and the compiler is complaining!
Let’s try to create a hero with a negative level:
def main(): Unit =
Hero(HeroId.generate(), HeroName("SuperHero"), HeroLevel(-1))
// Constraint Error: Should be strictly positive
Great job, we made our Hero
model more strict and type-safe!
Summary
In the next post, we’ll build the testing setup and write our first model tests.
You can find the complete code for this post here .