Reputation: 55569
Consider these case classes where Blog
contains Post
s and a Post
contains Comment
s, and then a Relation
to relate a single parent to a single child:
case class Comment(id: Int)
case class Post(id: Int, comments: List[Comment])
case class Blog(id: Int, posts: List[Post])
case class Relation[A, B](parent: A, child: B)
What I'm looking to do is recursively collapse a list of these Relations
into the top level parent
, starting with something like this (the reason for this un-nested structure in the first place is that this is how an SQL result with joined columns would look when parsed):
val relations = List(
Relation(Post(1, Nil), Comment(1)),
Relation(Post(1, Nil), Comment(2)),
Relation(Post(2, Nil), Comment(3)),
Relation(Post(3, Nil), Comment(4)),
Relation(Post(4, Nil), Comment(5))
)
And the output should be:
List(
Post(1,List(Comment(1), Comment(2))),
Post(2, List(Comment(3), Comment(4), Comment(5)))
)
With one level of nesting, this is easy:
def collapse[A, B](relations: List[Relation[A, B]])(implicit f: (A, List[B]) => A): List[A] = {
relations.groupBy(_.parent)
.mapValues(_.map(_.child))
.toList
.map(f.tupled)
}
Where the implicit
function require looks like this:
implicit def collapsePost(post: Post, comments: List[Comment]): Post = post.copy(comments = comments)
The trouble begins when I want to add another layer of nesting, and B
could instead be another Relation
. So I've tried many variations of this structure:
sealed abstract class Node
case class Relation[A, B <: Node](parent: A, child: B) extends Node
case class Child[B](value: B) extends Node
So now the example I seek to collapse looks more like this:
val relations = List(
Relation(Blog(1, Nil), Relation(Post(1, Nil), Child(Comment(1)))),
Relation(Blog(1, Nil), Relation(Post(2, Nil), Child(Comment(2)))),
Relation(Blog(1, Nil), Relation(Post(2, Nil), Child(Comment(3))))
)
And the desired output:
List(
Blog(1, List(
Post(1, List(Comment(1))),
Post(2, List(Comment(2), Comment(3)))
)
)
)
The signature of my collapse
function remains the same (for now). I can groupBy on the parent
of the relations
parameter, but then I get stuck at what would be the recursive step. At some point in the chain of functions within collapse
, I'll have a Map[A, List[Node]]
, where List[Node]
is either a List[Relation[B, ?]]
or List[Child[B]]
. The question mark notes my failure to move on. I've tried introducing TypeTag
s to pattern match the lists, since the types are erased, but the compiler will still complain about the second type parameter of Relation
.
i.e. if list
is a List[Node]
, then
list match {
case rel @ List[Relation[B, _]] => collapse(rel) // Complains about missing TypeTag in recursive call to `collapse`
case children @ List[Child[B]] => children
}
Is there a way around the missing type parameter in a recursively defined class like this (without the need for reflection, if possible)? Or perhaps I've just gone down the wrong path entirely.
Upvotes: 1
Views: 177
Reputation: 13667
Here is a solution that doesn't use the Child
class or require changes to collapse
:
implicit def collapseBlog(blog: Blog, relations: List[Relation[Post, Comment]])(implicit f: (Post, List[Comment]) => Post) = {
blog.copy(posts = collapse(relations))
}
We are taking advantage of recursive implicit resolution - collapseBlog
will use collapsePost
as its implicit and together they can collapse the two-level deep structure. Essentially, collapse(relations)
becomes collapse(relations)((x, y) => collapseBlog(x, y)(collapsePost))
after implicit resolution.
Upvotes: 1