How to modify this nested case classes with "Seq" fields? - scala

Some nested case classes and the field addresses is a Seq[Address]:
// ... means other fields
case class Street(name: String, ...)
case class Address(street: Street, ...)
case class Company(addresses: Seq[Address], ...)
case class Employee(company: Company, ...)
I have an employee:
val employee = Employee(Company(Seq(
Address(Street("aaa street")),
Address(Street("bbb street")),
Address(Street("bpp street")))))
It has 3 addresses.
And I want to capitalize the streets start with "b" only. My code is mess like following:
val modified = employee.copy(company = employee.company.copy(addresses =
employee.company.addresses.map { address =>
address.copy(street = address.street.copy(name = {
if (address.street.name.startsWith("b")) {
address.street.name.capitalize
} else {
address.street.name
}
}))
}))
The modified employee is then:
Employee(Company(List(
Address(Street(aaa street)),
Address(Street(Bbb street)),
Address(Street(Bpp street)))))
I'm looking for a way to improve it, and can't find one. Even tried Monocle, but can't apply it to this problem.
Is there any way to make it better?
PS: there are two key requirements:
use only immutable data
don't lose other existing fields

As Peter Neyens points out, Shapeless's SYB works really nicely here, but it will modify all Street values in the tree, which may not always be what you want. If you need more control over the path, Monocle can help:
import monocle.Traversal
import monocle.function.all._, monocle.macros._, monocle.std.list._
val employeeStreetNameLens: Traversal[Employee, String] =
GenLens[Employee](_.company).composeTraversal(
GenLens[Company](_.addresses)
.composeTraversal(each)
.composeLens(GenLens[Address](_.street))
.composeLens(GenLens[Street](_.name))
)
val capitalizer = employeeStreeNameLens.modify {
case s if s.startsWith("b") => s.capitalize
case s => s
}
As Julien Truffaut points out in an edit, you can make this even more concise (but less general) by creating a lens all the way to the first character of the street name:
import monocle.std.string._
val employeeStreetNameFirstLens: Traversal[Employee, Char] =
GenLens[Employee](_.company.addresses)
.composeTraversal(each)
.composeLens(GenLens[Address](_.street.name))
.composeOptional(headOption)
val capitalizer = employeeStreetNameFirstLens.modify {
case 'b' => 'B'
case s => s
}
There are symbolic operators that would make the definitions above a little more concise, but I prefer the non-symbolic versions.
And then (with the result reformatted for clarity):
scala> capitalizer(employee)
res3: Employee = Employee(
Company(
List(
Address(Street(aaa street)),
Address(Street(Bbb street)),
Address(Street(Bpp street))
)
)
)
Note that as in the Shapeless answer, you'll need to change your Employee definition to use List instead of Seq, or if you don't want to change your model, you could build that transformation into the Lens with an Iso[Seq[A], List[A]].

If you are open to replacing the addresses in Company from Seq to List, you can use "Scrap Your Boilerplate" from shapeless (example).
import shapeless._, poly._
case class Street(name: String)
case class Address(street: Street)
case class Company(addresses: List[Address])
case class Employee(company: Company)
val employee = Employee(Company(List(
Address(Street("aaa street")),
Address(Street("bbb street")),
Address(Street("bpp street")))))
You can create a polymorphic function which capitalizes the name of a Street if the name starts with a "b".
object capitalizeStreet extends ->(
(s: Street) => {
val name = if (s.name.startsWith("b")) s.name.capitalize else s.name
Street(name)
}
)
Which you can use as :
val afterCapitalize = everywhere(capitalizeStreet)(employee)
// Employee(Company(List(
// Address(Street(aaa street)),
// Address(Street(Bbb street)),
// Address(Street(Bpp street)))))

Take a look at quicklens
You could do it like this
import com.softwaremill.quicklens._
case class Street(name: String)
case class Address(street: Street)
case class Company(address: Seq[Address])
case class Employee(company: Company)
object Foo {
def foo(e: Employee) = {
modify(e)(_.company.address.each.street.name).using {
case name if name.startsWith("b") => name.capitalize
case name => name
}
}
}

Related

How do I use ScalaTest "inside" with an Array?

I have a case class and am trying to test similar to this. The change would be something like this...
case class Record(names: Array[Name] ...)
I am new to Scala and not sure how this would work syntactically
Please consider the following code:
case class Name(first: String, middle: String, last: String)
case class Record(names: Array[Name])
val rec = Record(
Array(Name("Sally", "Anna", "Jones"), Name("Sally1", "Anna1", "Jones1"))
)
inside (rec) { case Record(nameArray) =>
inside (nameArray) { case Array(name, name1) =>
inside(name) {
case Name(first, middle, last) =>
first should be("Sally")
middle should be("Anna")
last should be("Jones")
}
inside(name1) {
case Name(first, middle, last) =>
first should be("Sally1")
middle should be("Anna1")
last should be("Jones1")
}
}
}
Note that if the number of names at case Array(name, name1) is different then the actual, the test will fail.
As Luis mentioned in the comment, it is not recommended to use Arrays in case classes. This code will work the same if you change Array into List, Vector or ArraySeq.

combine two lists of different case class types

Combine two case class lists in to merged case class list
case class emp(emp_id:Integer,emp_name:String)
case class manager(manger_id:Integer,manager_name :String,emp_id:Integer)
case class combined(emp_id:Integer,emp_name :String,
manager_id:Integer,manager_name :String)
val list1:List[emp]= List((1,"emp1"),(2,"emp2")
val list2:List[manager]= List((101,"mgr1",1)(103,"mgr3",1))
expected output
val list3 = List(
(1,"emp1",101,"mgr1"),
(1,"emp1",103,"mgr3"),
(2,"emp2",null,null))
Depends on. If your data is already sorted by `emp_id and you have the same amount of managers as employees you can go with:
list1.zip(list2).map { case (e, m) =>
combined(e.emp_id, e.emp_name, m.manager_id, m.manager_name)
}
However, I suppose is not the case in a real-life scenario, where you need to match. Since the managers have an emp_id you can first run a groupBy on managers and then iterate over the employees to enrich them with manager input.
val grouped: Map[Int, manager] = list2.groupBy(_.emp_id)
list1.map { e =>
val manager_id = grouped.get(e.emp_id).flatMap (l =>
Try{l(0)}.toOption.map(_.manager_id)).getOrElse("null")
val manager_name = grouped.get(e.emp_id).flatMap (l =>
Try{l(0)}.toOption.map(_.manager_name)).getOrElse("null")
combined(e.emp_id, e.emp_name, manager_id, manager_name)
}
Did not checked the syntax, but you should get the point here.
P.S
Please use CamelCase and capital letters for classes in Scala.
Here's how I'd be tempted to tackle it.
// types
case class Emp(emp_id:Int, emp_name:String)
case class Manager(manager_id:Int, manager_name:String, emp_id:Int)
case class Combined(emp_id :Int
,emp_name :String
,manager_id :Option[Int]
,manager_name :String)
// input data
val emps :List[Emp] = List(Emp(1,"emp1"),Emp(2,"emp2"))
val mgrs :List[Manager] = List(Manager(101,"mgr1",1),Manager(103,"mgr3",1))
// lookup Emp name by ID
val empName = emps.groupMapReduce(_.emp_id)(_.emp_name)(_+_)
mgrs.map(mgr => Combined(mgr.emp_id
,empName(mgr.emp_id)
,Some(mgr.manager_id)
,mgr.manager_name)
) ++ empName.keySet
.diff(mgrs.map(_.emp_id).toSet)
.map(id => Combined(id, empName(id), None, ""))
//res0: List[Combined] = List(Combined(1, "emp1", Some(101), "mgr1")
// ,Combined(1, "emp1", Some(103), "mgr3")
// ,Combined(2, "emp2", None, ""))
I used Option[Int] and empty string "" to replace null, which Scala style tries to avoid.

Pattern matching in Scala with generic return type

There are four different types: Location, Language, Technology and Industry. There is a repository for each type that can return a collection of these types. For example a list of Locations. Each type has a name property with type String. There is a list of Strings. which can contain names of Locations, Languages, etc. I would like to write a function to find those typed entities (Location, Language, ...) that match the names of the String list. I was thinking something about like this:
def find[T](names: String): Set[T] = {
val collection = T match {
case Language => LanguageRepository.getAll
case Location => LocationRepository.getAll
case Teehnology => TechnologyRepository.getAll
case Industry => IndustryRepository.getAll
}
// find the elements in the collection
}
This is not correct, so how can the querying of the collection be done, and after that how can I be sure that the name property is there?
Why not use algebraic data types for this? They can serve as enums. The example:
sealed trait Property
case object LanguageProperty extends Property
case object LocationProperty extends Property
case object TechnologyProperty extends Property
case object IndustryProperty extends Property
def find(names: String, property: Property): Set[ParentClass] = {
val collection = property match {
case LanguageProperty => LanguageRepository.getAll
case LocationProperty => LocationRepository.getAll
case TechnologyProperty => TechnologyRepository.getAll
case IndustryProperty => IndustryRepository.getAll
}
// find the elements in the collection
}
Implying that ParentClass is a parent class of your Language, Location, Technology and Industry classes.
You can pass an implicit ClassTag value to determine the runtime class you passed
case class Language()
case class Location()
case class Teehnology()
case class Industry()
val LANG = classOf[Language]
val LOC = classOf[Location]
val TEC = classOf[Teehnology]
val IND = classOf[Industry]
def find[Z](names: String)(implicit ct: ClassTag[Z]): Set[Z] = {
val collection = ct.runtimeClass match {
case LANG => Set(Language())
case LOC => Set(Location())
case TEC => Set(Teehnology())
case IND => Set(Industry())
}
collection.asInstanceOf[Set[Z]]
}
and then
find[Language]("")
find[Industry]("")
produces
res0: Set[Language] = Set(Language())
res1: Set[Industry] = Set(Industry())

How can i convert this nonfunctional scala code with immutable members to an elegant solution?

How to avoid mutable index and make this more elegant?I know Null has to be changed with Option , i am just curious about the answers.
class Person(val name: String, val department: String)
var people = Array(new Person(“Jones”, “Marketing”), new Person(“Smith”, “Engineering”))
var engineer: Person = null
var index = 0
while (index < people.length) {
if (people(index).department == “Engineering”)
engineer = people(index)
index = index + 1
}
println(engineer.name + “ is an engineer”)
class Person(val name: String, val department: String)
val people = Array(new Person(“Jones”, “Marketing”), new Person(“Smith”, “Engineering”))
// Option[ Person ]... None if no Person satisfy this condition... Some( p ), if Person p is the first Person to satisfy the condition.
val personOption = people.find( p => p.department == "Engineering" )
personOption match {
case Some( p ) => println( " Found one engineer - " + p )
case None => println( "No engineer" )
}
If you want to find the last engineer in the array, you would probably use:
case class Person(val name: String, val department: String)
val people = Array(Person(“Jones”, “Marketing”), Person(“Smith”, “Engineering”))
def findLastEngineer(l: Seq[Person]) : Option[Person] =
people.foldLeft(None) {
case (previousOpt, eng) => if (eng.department == "Engineering") Some(eng) else previousOpt
}
}
println(findLastEngineer(people).map(_.name).getOrElse("Not found"))
I would do something like this:
case class Person(name: String, department: String)
val people = List(Person("Jones", "Marketing"), Person("Smith", "Engineering"))
val engineers = people.filter { person:Person => person.department == "Engineering" }
engineers.map { engineer: Person => println(engineer.name + " is an engineer") }
Try to use functions to transform your types in others. Usually we use map/reduce/filter functions to do this.
Here's how I would refactor:
// Refactored into a case class, since it's a simple data container
case class Person(name: String, department: String)
// Using the case class convenience apply method to drop `new`
val people = Array(Person(“Jones”, “Marketing”), Person(“Smith”, “Engineering”))
// Selects all the engineers. You could add `.headOption` to get the first.
val engineers = people.filter(_.department == "Engineering")
// Functional way of iterating the collection of engineers
// Also, using string interpolation to print
for (engineer <- engineers) println(s"${engineer.name} is an engineer.")
Alternatively, you could use collect to filter and pick the name:
// Collect is kind of a like a handy filter + map
val engineerNames = people.collect {
case Person(name, "Engineering") => name
}
for (name <- engineerNames) println(s"$name is an engineer.")
One last tip, if your departments are some finite set of fixed options, you should probably also consider making it a type:
sealed trait Department
case object Engineering extends Department
case object Marketing extends Department
// ... for each valid department
And then you can match on identity, rather than value. This lets you rely on the type system instead of constantly having to validate strings (known to some as stringly-typed programming). Best practice is to validate your data as early as possible into types, deal with it as typed data, and then only convert back to string for exporting data back out of your system (e.g. printing to screen, logging, serving via API).
You can use find to find first:
people
.find { _.department == "Engineering" }
.foreach { engineer => println(engineer.name + " is an engineer") }
or filter to find all:
people
.filter { _.department == "Engineering" }
.foreach { engineer => println(engineer.name + " is an engineer") }
By the way you can fix your code just by moving increment operation outside the if block:
if (people(index).department == "Engineering") {
engineer = people(index)
// index = index + 1
}
index = index + 1
After that you should check engineer for null, because your array may not contain a Person for your condition.
So it looks like you want to find last Person, thus you can use:
people
.foldLeft(None: Option[Person])((r, p) =>
if (p.department == "Engineering") Some(p) else r)
.foreach { engineer => println(engineer.name + " is an engineer") }
Also after avoiding all vars you can also change your Array (which is mutable structure) to List (by default scala.collection.immutable.List)

How do I parse DBObject to case class object using subset2?

Does anyone know how to parse DBObject to case class object using subset2 ? Super concise documentation doesn't help me :(
Consider following case class
case class MenuItem(id : Int, name: String, desc: Option[String], prices: Option[Array[String]], subitems: Option[Array[MenuItem]])
object MenuItem {
implicit val asBson = BsonWritable[MenuItem](item =>
{
val buf: DBObjectBuffer = DBO("id" -> item.id, "name" -> item.name)
item.desc match { case Some(value) => buf.append("desc" -> value) case None => }
item.prices match { case Some(value) => buf.append("prices" -> value) case None => }
item.subitems match { case Some(value) => buf.append("subitems" -> value) case None => }
buf()
}
)
}
and I wrote this parser
val menuItemParser: DocParser[MenuItem] = int("id") ~ str("name") ~ str("desc").opt ~ get[Array[String]]("prices").opt ~ get[Array[MenuItem]]("subitems").opt map {
case id ~ name ~ desc_opt ~ prices_opt ~ subitems => {
MenuItem(id, name, desc_opt, prices_opt, subitems)
}
}
It works if I remove last field subitems. But version shown above doesn't compile because MenuItem has field that references itself. It gives me following error
Cannot find Field for Array[com.borsch.model.MenuItem]
val menuItemParser: DocParser[MenuItem] = int("id") ~ str("name") ~ str("desc").opt ~ get[Array[String]]("prices").opt ~ get[Array[MenuItem]]("subitems").opt map {
^
It obviously doesn't compile because last get wants Field[MenuItem] implicit. But if I define it for MenuItem wouldn't it be pretty much copy-paste of DocParser[MenuItem] ?
How would you do it elegantly ?
I am an author of Subset (both 1.x and 2).
The README states that you need to have Field[T] per every T you would like to read (it's under "deserialization" section)
Just a side note. Frankly I don't find very logical to name a deserializer for MenuItem to be jodaDateTime.
Anyway Field[T] must translate from vanilla BSON types to your T. BSON cannot store MenuItem natively, see native BSON types here
But certainly the main problem is that you have a recursive data structure, so your "serializer" (BsonWritable) and "deserializer" (Field) must be recursive as well. Subset has implicit serializer/deserializer for List[T], but they require you to provide those for MenuItem : recursion.
To keep things short, I shall demonstrate you how you would write something like that for simpler "case class".
Suppose we have
case class Rec(id: Int, children: Option[List[Rec]])
Then the writer may look like
object Rec {
implicit object asBson extends BsonWritable[Rec] {
override def apply(rec: Rec) =
Some( DBO("id" -> rec.id, "children" -> rec.children)() )
}
Here, when you are writing rec.children into "DBObject", BsonWriteable[Rec] is being used and it requires "implicit" Field[Rec] in turn. So, this serializer is recursive.
As of the deserializer, the following will do
import DocParser._
implicit lazy val recField = Field({ case Doc(rec) => rec })
lazy val Doc: DocParser[Rec] =
get[Int]("id") ~ get[List[Rec]]("children").opt map {
case id ~ children => new Rec(id, children)
}
}
These are mutually recursive (remember to use lazy val!)
You would use them like so:
val dbo = DBO("y" -> Rec(123, Some(Rec(234, None) :: Nil))) ()
val Y = DocParser.get[Rec]("y")
dbo match {
case Y(doc) => doc
}