Human always makes mistake. Also, software-engineer makes mistake. Guess there are some codes like this.
val userId: Long = 12 | |
val deviceUuid: Long = 1234 | |
val carSerialId: Long = 12345 | |
def getHashCode( | |
userId: Long, | |
deviceUuid: Long, | |
carSerialId: Long | |
): String = { | |
val userIdPlus: Long = userId + 1 | |
val deviceUuidPlus: Long = deviceUuid + 2 | |
val carSerialIdPlus: Long = carSerialId + 3 | |
s"$userIdPlus-$deviceUuidPlus-$carSerialIdPlus" | |
} | |
getHashCode(userId, deviceUuid, carSerialId) // Right Answer = res0: String = 13-1236-12348 | |
getHashCode(deviceUuid, userId, carSerialId) // Wrong Answer = res1: String = 1235-14-12348 |
Two function calls
getHashCode(userId, deviceUuid, carSerialId) // Right Answer = res0: String = 13-1236-12348 getHashCode(deviceUuid, userId, carSerialId) // Wrong Answer = res1: String = 1235-14-12348
both compile. But the first one is a right answer, but secondly is wrong. Then What is a good way to help myself not make a mistake? At first thinking, It is very good way to make case class.
case class UserIdCaseClass(userId: Long) | |
case class DeviceUuidCaseClass(deviceUuid: Long) | |
case class CarSerialIdCaseClass(carSerialId: Long) | |
def getHashCodeCaseClass( | |
userIdCaseClass: UserIdCaseClass, | |
deviceUuidCaseClass: DeviceUuidCaseClass, | |
carSerialIdCaseClass: CarSerialIdCaseClass | |
): String = { | |
val userIdPlus: Long = userIdCaseClass.userId + 1 | |
val deviceUuidPlus: Long = deviceUuidCaseClass.deviceUuid + 2 | |
val carSerialIdPlus: Long = carSerialIdCaseClass.carSerialId + 3 | |
s"$userIdPlus-$deviceUuidPlus-$carSerialIdPlus" | |
} | |
getHashCodeCaseClass(UserIdCaseClass(userId), DeviceUuidCaseClass(deviceUuid), CarSerialIdCaseClass(carSerialId)) // Right Answer = res2: String = 13-1236-12348 | |
getHashCodeCaseClass(DeviceUuidCaseClass(deviceUuid), UserIdCaseClass(userId), CarSerialIdCaseClass(carSerialId)) // Not Even Compiled! |
This way, we can reduce mistakes. But we lost many things. First, we have to write many boilerplate. We have to make case class every Id and other Long types. And we lost that Id is the Long type. So, if we want to use userId or carSerialId or DeviceUuid, we have to call member. And cannot assign to Long type variable! To solve these problems, we can use Tagged Type.
Tagged Type
Tagged Type is tagging to some type A and Define as a subtype of A. For example, if we want to define userId as a subtype of Long, we can declare UserId as a Tagged Type of Long. Here is following an example.
import shapeless.tag | |
import shapeless.tag.@@ | |
trait UserIdTag | |
trait DeviceUuidTag | |
trait CarSerialIdTag | |
type UserId = Long @@ UserIdTag | |
type DeviceUuid = Long @@ DeviceUuidTag | |
type CarSerialId = Long @@ CarSerialIdTag | |
def getHashCodeTagged(userId: UserId, deviceUuid: DeviceUuid, carSerialId: CarSerialId): String = { | |
val userIdPlus: Long = userId + 1 | |
val deviceUuidPlus: Long = deviceUuid + 2 | |
val carSerialIdPlus: Long = carSerialId + 3 | |
s"$userIdPlus-$deviceUuidPlus-$carSerialIdPlus" | |
} | |
val taggedUserId: UserId = tag[UserIdTag][Long](userId) | |
val taggedDeviceUuid: DeviceUuid = tag[DeviceUuidTag][Long](deviceUuid) | |
val taggedCarSerialId: CarSerialId = tag[CarSerialIdTag][Long](carSerialId) | |
getHashCodeTagged(taggedUserId, taggedDeviceUuid, taggedCarSerialId) // Right Answer = res2: String = 13-1236-12348 | |
getHashCodeTagged(taggedDeviceUuid, taggedCarSerialId, taggedUserId) // Not Even Compiled |
Now, UserId, DeviceUuid, CarSerialId is Subtype of Long. We can use this type as Long type. For example, add Number to UserId by just using ‘+’ operator. or can assign to Long. But, It is UserId Type, DeviceUuid cannot be passed as a parameter in UserId Position.
Tagged Type Eraser
Sometimes you want to override function by tagged type like this.
def getId(userId: UserId): Long = userId + 1 | |
def getId(deviceUuid: DeviceUuid): Long = deviceUuid + 2 // Not Compiled because tagged type erased after compile | |
def getId(carSerialId: CarSerialId): Long = carSerialId + 3 // Not Compiled because tagged type erased after compile |
But if you override like this, Compiler says that ‘Not Compiled because tagged type erased after compile’. Yes, Tagged Type is erased after compile time. Then how can we solve the problem? You can solve this by using Either.
def getUserIdOrDeviceIdOrCarSerialId( | |
id: Either[UserId, Either[DeviceUuid, CarSerialId]] | |
): Long = id match { | |
case Left(userId) => userId + 1 | |
case Right(Left(deviceUuid)) => deviceUuid + 2 | |
case Right(Right(carSerialId)) => carSerialId + 3 | |
} | |
getUserIdOrDeviceIdOrCarSerialId(Left(taggedUserId)) | |
getUserIdOrDeviceIdOrCarSerialId(Right(Left(taggedDeviceUuid))) | |
getUserIdOrDeviceIdOrCarSerialId(Right(Right(taggedCarSerialId))) |
Of course, you can use Coproduct in shapeless
import shapeless.{Coproduct, CNil, :+:, Inl, Inr} | |
type Id = UserId :+: DeviceUuid :+: CarSerialId :+: CNil | |
def getId(id: Id): Long = id match { | |
case Inl(userId) => userId + 1 | |
case Inr(Inl(deviceUuid)) => deviceUuid + 2 | |
case Inr(Inr(Inl(carSerialId))) => carSerialId + 3 | |
case Inr(Inr(Inr(cNil))) => 0 | |
} | |
getId(Coproduct[Id](taggedUserId)) | |
getId(Coproduct[Id](taggedDeviceUuid)) | |
getId(Coproduct[Id](taggedCarSerialId)) |
By this way, You can keep self from make mistake.
val userId: Long = 12 | |
val deviceUuid: Long = 1234 | |
val carSerialId: Long = 12345 | |
def getHashCode( | |
userId: Long, | |
deviceUuid: Long, | |
carSerialId: Long | |
): String = { | |
val userIdPlus: Long = userId + 1 | |
val deviceUuidPlus: Long = deviceUuid + 2 | |
val carSerialIdPlus: Long = carSerialId + 3 | |
s"$userIdPlus-$deviceUuidPlus-$carSerialIdPlus" | |
} | |
getHashCode(userId, deviceUuid, carSerialId) // Right Answer = res0: String = 13-1236-12348 | |
getHashCode(deviceUuid, userId, carSerialId) // Wrong Answer = res1: String = 1235-14-12348 | |
// Lets do it by case class | |
case class UserIdCaseClass(userId: Long) | |
case class DeviceUuidCaseClass(deviceUuid: Long) | |
case class CarSerialIdCaseClass(carSerialId: Long) | |
def getHashCodeCaseClass( | |
userIdCaseClass: UserIdCaseClass, | |
deviceUuidCaseClass: DeviceUuidCaseClass, | |
carSerialIdCaseClass: CarSerialIdCaseClass | |
): String = { | |
val userIdPlus: Long = userIdCaseClass.userId + 1 | |
val deviceUuidPlus: Long = deviceUuidCaseClass.deviceUuid + 2 | |
val carSerialIdPlus: Long = carSerialIdCaseClass.carSerialId + 3 | |
s"$userIdPlus-$deviceUuidPlus-$carSerialIdPlus" | |
} | |
getHashCodeCaseClass(UserIdCaseClass(userId), DeviceUuidCaseClass(deviceUuid), CarSerialIdCaseClass(carSerialId)) // Right Answer = res2: String = 13-1236-12348 | |
//getHashCodeCaseClass(DeviceUuidCaseClass(deviceUuid), UserIdCaseClass(userId), CarSerialIdCaseClass(carSerialId)) // Not Even Compiled! | |
import shapeless.tag | |
import shapeless.tag.@@ | |
trait UserIdTag | |
trait DeviceUuidTag | |
trait CarSerialIdTag | |
type UserId = Long @@ UserIdTag | |
type DeviceUuid = Long @@ DeviceUuidTag | |
type CarSerialId = Long @@ CarSerialIdTag | |
def getHashCodeTagged( | |
userId: UserId, | |
deviceUuid: DeviceUuid, | |
carSerialId: CarSerialId | |
): String = { | |
val userIdPlus: Long = userId + 1 | |
val deviceUuidPlus: Long = deviceUuid + 2 | |
val carSerialIdPlus: Long = carSerialId + 3 | |
s"$userIdPlus-$deviceUuidPlus-$carSerialIdPlus" | |
} | |
val taggedUserId: UserId = tag[UserIdTag][Long](userId) | |
val taggedDeviceUuid: DeviceUuid = tag[DeviceUuidTag][Long](deviceUuid) | |
val taggedCarSerialId: CarSerialId = tag[CarSerialIdTag][Long](carSerialId) | |
getHashCodeTagged(taggedUserId, taggedDeviceUuid, taggedCarSerialId) // Right Answer = res2: String = 13-1236-12348 | |
//getHashCodeTagged(taggedDeviceUuid, taggedCarSerialId, taggedUserId) // Not Even Compiled | |
// Can't Override | |
def getId(userId: UserId): Long = userId + 1 | |
//def getId(deviceUuid: DeviceUuid): Long = deviceUuid + 2 // Not Compiled because tagged type erased after compile | |
//def getId(carSerialId: CarSerialId): Long = carSerialId + 3 // Not Compiled because tagged type erased after compile | |
// Let's do by either | |
def getUserIdOrDeviceIdOrCarSerialId( | |
id: Either[UserId, Either[DeviceUuid, CarSerialId]] | |
): Long = id match { | |
case Left(userId) => userId + 1 | |
case Right(Left(deviceUuid)) => deviceUuid + 2 | |
case Right(Right(carSerialId)) => carSerialId + 3 | |
} | |
getUserIdOrDeviceIdOrCarSerialId(Left(taggedUserId)) | |
getUserIdOrDeviceIdOrCarSerialId(Right(Left(taggedDeviceUuid))) | |
getUserIdOrDeviceIdOrCarSerialId(Right(Right(taggedCarSerialId))) | |
// You can do by Coproduct by shapeless | |
import shapeless.{Coproduct, CNil, :+:, Inl, Inr} | |
type Id = UserId :+: DeviceUuid :+: CarSerialId :+: CNil | |
def getId(id: Id): Long = id match { | |
case Inl(userId) => userId + 1 | |
case Inr(Inl(deviceUuid)) => deviceUuid + 2 | |
case Inr(Inr(Inl(carSerialId))) => carSerialId + 3 | |
case Inr(Inr(Inr(cNil))) => 0 | |
} | |
getId(Coproduct[Id](taggedUserId)) | |
getId(Coproduct[Id](taggedDeviceUuid)) | |
getId(Coproduct[Id](taggedCarSerialId)) |