I have the following generic case class to model a resources in an HTTP API (we use Akka HTTP):
case class Job[Result](
id: String,
result: Option[Result] = None,
userId: String,
)
and want to specify multiple, named, variations of a Job, were some of them provide a result, while others don't:
case class FooResult(data: String)
type FooJob = Job[FooResult]
// BarJob does not have any result, thus result should be None
case class BarJob = Job[/*what do to?*/]
my question is, is there any way to define Job as a generic case class where the type parameter only needs to be specified when the field result is Some ? What I would like to do is something like:
// result is by default None in Job, so I don't want to specify any type here
case class BarJob = Job
Or perhaps there's better ways to do this, rather than using type aliases?
One option is to use base traits for Jobs with and without results:
trait Job {
def id: String
def userId: String
}
trait JobWithResult[T] extends Job {
def result: T
}
case class FooResult(data: String)
case class FooJob(id: String, userId: String, result: FooResult) extends JobWithResult[FooResult]
case class BarJob(id: String, userId: String) extends Job
You can then use match on a Job to see whether it has a result or not.
job match {
case FooJob(id, userId, foo) => println(s"FooJob with result $foo")
case j: JobWithResult[_] => println(s"Other job with id ${j.id} with result")
case j: Job => println(s"Job with id {$j.id} without result")
}
This assumes that the result is not actually optional.
As pointed out in the comments, the Option is "unnecessary".
I submit, it's not so much "unnecessary" as indicative ... of a way for you to implement what you want without repetitive declarations.
case class Job[Result](id: String, userId: String, result: Option[Result] = None)
object Job {
def void(id: String, userId: String) = Job[Nothing](id, userId)
def apply[R](id: String, userId: String, result: R) = Job(id, userId, Option(result))
}
type FooJob = Job[FooResult]
type VoidJob = Job[Nothing]
Related
I'm implementing a client for the Keystone API of Openstack. For the Users I have the following classes:
import java.time.OffsetDateTime
import io.circe.derivation.{deriveDecoder, deriveEncoder, renaming}
import io.circe.{Decoder, Encoder}
object User {
object Update {
implicit val encoder: Encoder[Update] = deriveEncoder(renaming.snakeCase)
}
case class Update(
name: Option[String] = None,
password: Option[String] = None,
defaultProjectId: Option[String] = None,
enabled: Option[Boolean] = None,
)
implicit val decoder: Decoder[User] = deriveDecoder(renaming.snakeCase)
}
final case class User(
id: String,
name: String,
domainId: String,
defaultProjectId: Option[String] = None,
passwordExpiresAt: Option[OffsetDateTime] = None,
enabled: Boolean = true,
)
Where User.Update contains the possible fields to update a user. Updates are done using PATCH, or in other words they are partial. The encoders are being used in a class which has the methods to create, update, delete, get, and list the users. This service class uses the encoders in a http4s EntityEncoder with:
import io.circe.{Encoder, Printer}
import org.http4s.{EntityDecoder, circe}
val jsonPrinter: Printer = Printer.noSpaces.copy(dropNullValues = true)
implicit def jsonEncoder[A: Encoder]: EntityEncoder[F, A] = circe.jsonEncoderWithPrinterOf(jsonPrinter)
My problem is how to implement the update for defaultProjectId. In the final json sent to the server the following cases are possible:
Keep the current value of defaultProjectId (the json object does not contain the field default_project_id:
{
(...)
}
Change the defaultProjectId to an-id:
{
(...),
"default_project_id: "an-id",
(...)
}
Unset the defaultProjectId:
{
(...),
"default_project_id: null,
(...)
}
The current implementation: defaultProjectId: Option[String] = None + dropNullValues in the printer, models correctly the cases 1 and 2, but prevents case 3.
Ideally I would have an ADT like:
sealed trait Updatable[+T]
case object KeepExistingValue extends Updatable[Nothing]
case object Unset extends Updatable[Nothing]
case class ChangeTo[T](value: T) extends Updatable[T]
Usage example (probably in the future all fields would be Updatables):
case class Update(
name: Option[String] = None,
password: Option[String] = None,
defaultProjectId: Updatable[String] = KeepExistingValue,
enabled: Option[Boolean] = None,
)
But I can't find a clean way to encode this ADT. Attempted solutions and their problems (they all require not using the printer with dropNullValues in the update method):
Unset is special:
// The generic implementation of Updatable
implicit def updatableEncoder[T](implicit valueEncoder: Encoder[T]): Encoder[Updatable[T]] = {
case KeepExistingValue => Json.Null
case Unset => Json.fromString(Unset.getClass.getName) // Or another arbitrary value
case ChangeTo(value) => valueEncoder(value)
}
// In the service class
def nullifyUnsets(obj: JsonObject): JsonObject = obj.mapValues {
case json if json.asString.contains(Unset.getClass.getName) => Json.Null
case json => json
}
def update(id: String, update: Update): F[Model] = {
// updateEncoder is of type Encoder[Update]
updateEncoder(update).dropNullValues.mapObject(nullifyUnsets)
(...)
}
Pros:
Using the dropNullValues nicely handles the KeepExistingValue case.
If the user invokes dropNullValues to derive the encoder the code still works.
Cons:
Because of dropNullValues the Unset case is meh.
We iterate twice on the Json Object field/values, once for dropNullValues another for mapObject.
Json.fromString(Unset.getClass.getName) is arbitrary and can collide with a legit value for T, although very unlikely.
KeepExistingValue is special:
// The generic implementation of Updatable
implicit def updatableEncoder[T](implicit valueEncoder: Encoder[T]): Encoder[Updatable[T]] = {
case KeepExistingValue => Json.fromString(Unset.getClass.getName) // Or another arbitrary value
case Unset => Json.Null
case ChangeTo(value) => valueEncoder(value)
}
// In the service class
def dropKeepExistingValues(obj: JsonObject): JsonObject = obj.filter{
case (_, json) => !json.asString.contains(Unset.getClass.getName)
}
def update(id: String, update: Update): F[Model] = {
// updateEncoder is of type Encoder[Update]
updateEncoder(update).mapObject(dropKeepExistingValues)
(...)
}
Pros:
Simpler implementation, updatableEncoder implementation maps more directly to the needed Json.
Just one pass over the Json Object field/values.
Cons:
If the programmer invokes dropNullValues to derive the encoder then the code stops working.
Json.fromString(Unset.getClass.getName) is arbitrary and can collide with a legit value for T, although very unlikely.
I'm sure I'm not the first one to hit this problem, but I can't search for it, the best I got is this comment.
I am currently trying to create case classes that are constructed differently based on the inputs of their parameters. As a real world use-case, let's say we are constructing a case class that contains sensitive information. So everytime the case class contains data from a user, we need to hash their phone number, otherwise construct the case class as normal. If that did not make much sense, I have created a lighter example to illustrate what I am trying to do.
Let's say we are creating a case class that does not accept a sad cow like:
case class HappyCow(name: String, feeling: String)
I tried making the case class construction conditional by defining an apply method:
case class HappyCow(name: String, feeling: String) {
def apply(name: String, feeling: String): HappyCow =
if (feeling == "sad") HappyCow(name, "Happy")
else HappyCow(name, feeling)
}
However testing if my solution works results in:
val cow1 = HappyCow("Moowy", "excited")
val cow2 = HappyCow("MooMoo", "sad")
println(cow1) // HappyCow(Moowy,excited)
println(cow2) // HappyCow(MooMoo,sad)
println(cow2.feeling) // sad
I expected cow2.feeling to be "Happy"
apply should be a method of companion object, not case class.
Also inside definition of apply replace HappyCow(name, "Happy")... with new HappyCow(name, "Happy")..., otherwise it's infinite recursion.
case class HappyCow(name: String, feeling: String)
object HappyCow {
def apply(name: String, feeling: String): HappyCow =
if (feeling == "sad") new HappyCow(name, "Happy")
else new HappyCow(name, feeling)
}
val cow1 = HappyCow("Moowy", "excited")
val cow2 = HappyCow("MooMoo", "sad")
println(cow1) // HappyCow(Moowy,excited)
println(cow2) // HappyCow(MooMoo,Happy)
println(cow2.feeling) // Happy
I am starting a new project using Scala and Akka and am having trouble writing tests. In my tests I am checking the equality of two List objects using should equal:
actualBook should equal (expectedBook)
Everything in my test suite compiles and runs, but the tests fail with the following message:
org.scalatest.exceptions.TestFailedException: List(BookRow(A,100.0,10.6)) did not equal List(BookRow(A,100.0,10.6))
Clearly the tests are passing (i.e., both List objects contain the same contents). Not sure if this is relevant or not, but actualBook and expectedBook have the same hash code (and actualBook(0) and expectedBook(0) also have the same hash code).
I am wondering if the problem is due to...
my using the incorrect comparison operator
the fact that I have not explicitly defined a way to compare BookRow objects.
For reference here is the code for my tests:
package lob
import cucumber.api.DataTable
import org.scalatest.Matchers._
import scala.collection.JavaConversions._
import cucumber.api.java.en.{When, Then}
class OrderBookSteps {
val orderTypes = OrderType.all()
val buyBook: OrderBook = new OrderBook(Bid, orderTypes)
val sellBook: OrderBook = new OrderBook(Ask, orderTypes)
#When("""^the following orders are added to the "(.*?)" book:$""")
def ordersAddedToBook(sideString: String, orderTable: DataTable) {
val (side, book) = getBook(sideString)
val orders = orderTable.asList[OrderRow](classOf[OrderRow]).toList.map(
r => LimitOrder(r.broker, side, r.volume, r.price.toDouble))
orders.foreach(book.add)
}
#Then("""^the "(.*?)" order book looks like:$""")
def orderBookLooksLike(sideString: String, bookTable: DataTable) {
val (_, book) = getBook(sideString)
val expectedBook = bookTable.asList[BookRow](classOf[BookRow]).toList
val actualBook = book.orders().map(o => BookRow(o.broker, o.volume, orderTypes(o).bookDisplay))
actualBook should equal (expectedBook)
}
def getBook(side: String) = side match {
case "Bid" => (Bid, buyBook)
case "Ask" => (Ask, sellBook)
}
case class OrderRow(broker: String, volume: Double, price: String)
case class BookRow(broker: String, volume: Double, price: String)
}
You can try:
List(BookRow(A,100.0,10.6)).toSeq should equal (List(BookRow(A,100.0,10.6)).toSeq)
Or:
List(BookRow(A,100.0,10.6) should contain theSameElementsAs List(BookRow(A,100.0,10.6))
Assuming you have BookRow (normal class) equals overridden.
I have found a solution, although I don't understand why it works! I just needed to replace:
case class OrderRow(broker: String, volume: Double, price: String)
case class BookRow(broker: String, volume: Double, price: String)
with
private case class OrderRow(broker: String, volume: Double, price: String)
private case class BookRow(broker: String, volume: Double, price: String)
I would like to know why this works.
Suppose, I have my domain object named "Office":
case class Office(
id: Long,
name: String,
phone: String,
address: String
) {
def this(name: String, phone: String, address: String) = this(
null.asInstanceOf[Long], name, phone, address
)
}
When I create new Office:
new Office("officeName","00000000000", "officeAddress")
I don't specify id field becouse I don't know it. When I save office (by Anorm) I now id and do that:
office.id = officeId
So. I know that using null is non-Scala way. How to avoid using null in my case?
UPDATE #1
Using Option.
Suppose, something like this:
case class Office(
id: Option[Long],
name: String,
phone: String,
address: String
) {
def this(name: String, phone: String, address: String) = this(
None, name, phone, address
)
}
And, after saving:
office.id = Option(officeId)
But what if I need to find something by office id?
SomeService.findSomethingByOfficeId(office.id.get)
Does it clear? office.id.get looks not so good)
UPDATE #2
Everyone thanks! I've got new ideas from your answers! Greate thanks!
Why not declare the id field as a Option? You should really avoid using null in Scala. Option is preferable since it is type-safe and plays nice with other constructs in the functional paradigm.
Something like (I haven't tested this code):
case class Office(
id: Option[Long],
name: String,
phone: String,
address: String
) {
def this(name: String, phone: String, address: String) = this(
None, name, phone, address
)
}
Just make the id field an Option[Long]; once you have that, you can use it like this:
office.id.map(SomeService.findSomethingByOfficeId)
This will do what you want and return Option[Something]. If office.id is None, map() won't even invoke the finder method and will immediately return None, which is what you want typically.
And if findSomethingByOfficeId returns Option[Something] (which it should) instead of just Something or null/exception, use:
office.id.flatMap(SomeService.findSomethingByOfficeId)
This way, if office.id is None, it will, again, immediately return None; however, if it's Some(123), it will pass that 123 into findSomethingByOfficeId; now if the finder returns a Some(something) it will return Some(something), if however the finder returns None, it will again return None.
if findSomethingByOfficeId can return null and you can't change its source code, wrap any calls to it with Option(...)—that will convert nulls to None and wrap any other values in Some(...); if it can throw an exception when it can't find the something, wrap calls to it with Try(...).toOption to get the same effect (although this will also convert any unrelated exceptions to None, which is probably undesirable, but which you can fix with recoverWith).
The general guideline is always avoid null and exceptions in Scala code (as you stated); always prefer Option[T] with either map or flatMap chaining, or using the monadic for syntactic sugar hiding the use of map and flatMap.
Runnable example:
object OptionDemo extends App {
case class Something(name: String)
case class Office(id: Option[Long])
def findSomethingByOfficeId(officeId: Long) = {
if (officeId == 123) Some(Something("London")) else None
}
val office1 = Office(id = None)
val office2 = Office(id = Some(456))
val office3 = Office(id = Some(123))
println(office1.id.flatMap(findSomethingByOfficeId))
println(office2.id.flatMap(findSomethingByOfficeId))
println(office3.id.flatMap(findSomethingByOfficeId))
}
Output:
None
None
Some(Something(London))
For a great introduction to Scala's rather useful Option[T] type, see http://danielwestheide.com/blog/2012/12/19/the-neophytes-guide-to-scala-part-5-the-option-type.html.
When using id: Option[Long] , extract the option value for instance with
if (office.id.isDefined) {
val Some(id) = office.id
SomeService.findSomethingByOfficeId(id)
}
or perhaps for instance
office.id match {
case None => Array()
case Some(id) => SomeService.findSomethingByOfficeId(id)
}
Also you can define case classes and objects as follows,
trait OId
case object NoId extends OId
case class Id(value: Long) extends OId
case class Office (
id: OId = NoId,
name: String,
phone: String,
address: String
)
Note that by defaulting id for example to NoId , there is no need to declare a call to this. Then
val office = Office (Id(123), "name","phone","addr")
val officeNoId = Office (name = "name",phone="phone",address="addr")
If the id member is defined last, then there is no need to name the member names,
val office = Office ("name","phone","addr")
office: Office = Office(name,phone,addr,NoId)
As of invoking (neatly) a method,
office.id match {
case NoId => Array()
case Id(value) => SomeService.findSomethingByOfficeId(value)
}
I prefer more strong restriction for object Id property:
trait Id[+T] {
class ObjectHasNoIdExcepthion extends Throwable
def id : T = throw new ObjectHasNoIdExcepthion
}
case class Office(
name: String,
phone: String,
address: String
) extends Id[Long]
object Office {
def apply(_id : Long, name: String, phone: String, address: String) =
new Office(name, phone, address) {
override def id : Long = _id
}
}
And if I try to get Id for object what is not stored in DB, I get exception and this mean that something wrong in program behavior.
val officeWithoutId =
Office("officeName","00000000000", "officeAddress")
officeWithoutId.id // Throw exception
// for-comprehension and Try monad can be used
// update office:
for {
id <- Try { officeWithoutId.id }
newOffice = officeWithoutId.copy(name = "newOffice")
} yield OfficeDAL.update(id, newOffice)
// find office by id:
for {
id <- Try { officeWithoutId.id }
} yield OfficeDAL.findById(id)
val officeWithId =
Office(1000L, "officeName","00000000000", "officeAddress")
officeWithId.id // Ok
Pros:
1) method apply with id parameter can be incapsulated in DAL logic
private[dal] def apply (_id : Long, name: String, ...
2) copy method always create new object with empty id (safty if you change data)
3) update method is safety (object not be overridden by default, id always need to be specified)
Cons:
1) Special serealization/deserealization logic needed for store id property (json for webservices, etc)
P.S.
this approach is good if you have immutable object (ADT) and store it to DB with id + object version instead object replace.
I'm trying to improve a CSV parsing routine and feel that extractors could be useful here but can't figure them out. Suppose there's a file with user ids and emails:
1,alice#alice.com
2,bob#bob.com
3,carol#carol.com
If the User class is defined as case class User(id: Int, email: String) everything is pretty easy with something like
lines map { line =>
line split "," match {
case Array(id, email) => User(id.toInt, email)
}
}
What I don't understand is how to deal with the case where User class can have complex properties e.g
case class Email(username: String, host: string)
case class User(id: Int, email: Email)
You probably want to use a regular expression to extract the contents of the email address. Maybe something like this:
val lines = Vector(
"1,alice#alice.com",
"2,bob#bob.com",
"3,carol#carol.com")
case class Email(username: String, host: String)
case class User(id: Int, email: Email)
val EmailRe = """(.*)#(.*\.com)""".r // substitute a real email address regex here
lines.map { line =>
line.split(",") match {
case Array(id, EmailRe(uname, host)) => User(id.toInt, Email(uname, host))
}
}
Here you go, an example using a custom Extractor.
// 1,Alice,21212,Baltimore,MD" -> User(1, Alice, Address(21212, Baltimore, MD))
Define a custom Extractor that creates the objects out of given String:
object UserExtractor {
def unapply(s: String) : Option[User] = try {
Some( User(s) )
}
catch {
// bettor handling of bad cases
case e: Throwable => None
}
}
Case classes to hold the data with a custom apply on Comapnion object on User:
case class Address(code: String, cit: String, county: String)
case class User(id: Int, name: String, address: Address)
object User {
def apply(s: String) : User = s.split(",") match {
case Array(id, name, code, city, county) => User(id.toInt, name, Address(code, city, county) )
}
}
Unapplying on a valid string (in the example valid means the correct number of fields).
"1,Alice,21212,Baltimore,MD" match { case UserExtractor(u) => u }
res0: User = User(1,Alice,Address(21212,Baltimore,MD))
More tests could be added with more custom apply methods.
I'd use a single RegexExtractor :
val lines = List(
"1,alice#alice.com",
"2,bob#bob.com",
"3,carol#carol.com"
)
case class Email(username: String, host: String)
case class User(id: Int, email: Email)
val idAndEMail = """^([^,]+),([^#]+)#(.+)$""".r
and define a function that transforms a line to the an User :
def lineToUserWithMail(line: String) : Option[User] =
idAndEMail.findFirstMatchIn(line) map {
case userWithEmail(id,user,host) => User(id.toInt, Email(user,host) )
}
Applying the function to all lines
lines flatMap lineToUserWithMail
//res29: List[User] = List(User(1,Email(alice,alice.com)), User(2,Email(bob,bob.com)), User(3,Email(carol,carol.com)))
Alternatively you could implement custom Extractors on the case classe by adding an unnapply Method. But for that case it wouldn't be worth the pain.
Here is an example for unapply
class Email(user:String, host:String)
object Email {
def unapply(s: String) : Option[(String,String)] = s.split("#") match {
case Array(user, host) => Some( (user,host) )
case _ => None
}
}
"bob#bob.com" match {
case Email(u,h) => println( s"$u , $h" )
}
// prints bob , bob.com
A word of warning on using Regex to parse CSV-data. It's not as easy as you might think, i would recommend to use a CSV-Reader as http://supercsv.sourceforge.net/ which handles some nasty edge cases out of the box.