Search code examples
scalascalazshapeless

Use case class copy method and abstract over named parameters


I am using scalaz state monad, and I have the following problem:
My state is a case class containing vectors of different types:

case class Repository(instances: Vector[Instance], vpcs: Vector[Vpc], subnets: Vector[Subnet]….)

I would like to make changes to the Repository via state actions. For example I would like to update some of the elements in a vector based on an update function as follows:

  val applicative = Applicative[({type f[a] = State[Repository, a]})#f]
  def createTags[T <: {def id : String}](memberSelector: Repository => Vector[T])(updateTags: T => T) =
    for {
      matchingResources <- State.gets((repo: Repository)=> memberSelector(repo).filter(t => resourcesIds.contains(t.id)))
      _ <- applicative.traverse(matchingResources)(matchingResource => State.modify((repo: Repository) => repo.copy(*** = memberSelector(repo).replaceFirst(matchingResource, updateTags(matchingResource)))))
    } yield ()

replaceFirst simply updates an element in a vector in a pimped implicit class:

def replaceFirst(oldElem: A, newElem: A): Vector[A] = {
  val i = v.indexOf(oldElem)
  if (i == -1) v else v.updated(i, newElem)
}

The problem is that I can’t abstract over the named parameter to use when invoking the copy method of the Repository class (see the *** in the code above).
I thought maybe Shapeless could help me do that: I could use the case class to HList isomorphism to treat the Repository case class as an HList and then use an HList operation to update the relevant Vector. I haven’t been able to get it to work though. Is it possible to do via Shapeless? Any other ideas?


Solution

  • You should be able to use a lens library for this instead of case class copy function directly. A lens is a mechanism to both get and set some A in a B. You're already taking the get in the form of memberSelector.

    e.g using monocale which provides nice macros for creating lens (I didn't actually typecheck this)

    object Repository {
      import monocle.Lens
      import monocle.macros.GenLens
    
      val _instances: Lens[Repository, Vector[Instance]] = GenLens[Repository](_.instances)
      val _vpcs: Lens[Repository, Vector[Vpc]] = GenLens[Repository](_.vpcs)
      ///...
    
      val applicative = Applicative[({type f[a] = State[Repository, a]})#f]
      def createTags[T <: {def id : String}](lens: Lens[Repository, Vector[T]])(updateTags: T => T) =
        for {
          matchingResources <- State.gets((repo: Repository)=> lens.get(repo).filter(t => resourcesIds.contains(t.id)))
          _ <- applicative.traverse(matchingResources)(matchingResource => State.modify((repo: Repository) => lens.modify(_.replaceFirst(matchingResource, updateTags(matchingResource)))))
        } yield ()
    }
    

    The next version of monacle should also include nice ways to use Lens actions to get scalaz.State. This should allow for very similar syntax to scalaz.Lens version below.

    You could also use scalaz.Lens also

    object Repository {
      import scalaz.Lens
    
      val _instances: Lens[Repository, Vector[Instance]] = Lens.lensu((r, i) => r.copy(instances = i), _.instances)
      ///...
    
      import scalaz.std.vector._
      import scalaz.syntax.traverse._
      def createTags[T <: {def id : String}](lens: Lens[Repository, Vector[T]])(updateTags: T => T) =
        for {
          matchingResources <- lens.map(_.filter(t => resourcesIds.contains(t.id)))
          _ <- matchingResources.traverseS_(matchingResource => lens.mods_(_.replaceFirst(matchingResource, updateTags(matchingResource))))
        } yield ()
    }