Scala lens for collection parameter
Asked Answered
R

2

6

What is the best way to update an element in a collection using lenses? For example:

case class Ingredient(name: String, quantity: Int)
case class Recipe(val ingredients: List[Ingredient])

If I want to use lenses to create a new recipe with the quantity of a single ingredient changed, what's the best way of doing it?

The approach I've tried is to create a lens on the fly: Lens[List[Ingredient], Ingredient]. This feels a little clunky though:

case class Recipe(val ingredients: List[Ingredient]) {
  import Recipe._
  def changeIngredientQuantity(ingredientName: String, newQuantity: Int) = {
    val lens = ingredientsLens >=> ingredientLens(ingredientName) >=> Ingredient.quantityLens
    lens.set(this, newQuantity)
  }
}

object Recipe {
  val ingredientsLens = Lens.lensu[Recipe, List[Ingredient]](
    (r, i) => r.copy(ingredients = i),
    r => r.ingredients
  )
  def ingredientLens(name: String) = Lens.lensu[List[Ingredient], Ingredient](
    (is, i) => is.map (x => if (x.name == name) i else x),
    is => is.find(i => i.name == name).get
  )
}

case class Ingredient(name: String, quantity: Int)

object Ingredient {
  val quantityLens = Lens.lensu[Ingredient, Int](
    (i, q) => i.copy(quantity = q),
    i => i.quantity
  )
}
Regurgitation answered 18/10, 2013 at 8:14 Comment(1)
you can omit val in case class parameters, they are vals by defaultFeces
R
6

You cannot create a Lens between a List[T] and T at a given index because a Lens requires the object you are focusing to be always present. However, in the case of a look up in a List or in another collection, there might be no element at the index.

You could however use a Traversal, a sort of Lens that focuses to 0 to many elements. With Monocle, you would use the index function to create a Traversal from a List to an element at a given index:

import monocle.SimpleLens
import monocle.syntax.lens._     // to use |-> and |->> instead of composeLens, composeTraversal
import monocle.functions.Index._ // to use index Traversal

// monocle also provides a macro to simplify lens creation
val ingredientsLens = SimpleLens[Recipe, List[Ingredient]](_.ingredients, (recipe, newIngredients)  => recipe.copy(ingredients = newIngredients))  
val quantityLens    = SimpleLens[Ingredient, Int](_.quantity            , (ingredient, newQuantity) => ingredient.copy(quantity = newQuantity))  

val applePie = Receipe(List(Ingredient("apple", 3), Ingredient("egg", 2), ...))


applePie |-> ingredientsLens |->> index(0)   headOption // Some(Ingredient("apple", 3))
applePie |-> ingredientsLens |->> index(999) headOption // None
applePie |-> ingredientsLens |->> index(0) |->> quantityLens headOption // 3
applePie |-> ingredientsLens |->> index(0) |->> quantityLens set 5 
// Receipe(List(Ingredient("apple", 5), Ingredient("egg", 2), ...))
Ringtail answered 30/4, 2014 at 15:45 Comment(0)
M
1

If you want to update based on name, how about having a Map of name -> quantity? Then you could use the solution described here:

Scalaz: how to compose a map lens with a value lens?

If you stick with a List, partial lenses from Scalaz can still be used. The function listLookupByPLens looks promising.

Maurer answered 31/10, 2013 at 22:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.