Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Coproduct with Poly

I'm trying to use shapeless to create a poly2 function that can take a coproduct:

case class IndexedItem(
  item1: Item1,
  item2: Item2,
  item3: Item3
)

case class Item1(name: Int)

case class Item2()

case class Item3()

object IndexUpdater {
  type Indexable = Item1 :+: Item2 :+: Item3 :+: CNil

  object updateCopy extends Poly2 {
    implicit def caseItem1 = at[IndexedItem, Item1] { (a, b) => a.copy(item1 = b) }

    implicit def caseItem2 = at[IndexedItem, Item2] { (a, b) => a.copy(item2 = b) }

    implicit def caseItem3 = at[IndexedItem, Item3] { (a, b) => a.copy(item3 = b) }
  }

  def mergeWithExisting(existing: IndexedItem, item: Indexable): IndexedItem = {
    updateCopy(existing, item)
  }
}

This gives me an error of

Error:(48, 15) could not find implicit value for parameter cse: shapeless.poly.Case[samples.IndexUpdater.updateCopy.type,shapeless.::[samples.IndexedItem,shapeless.::[samples.IndexUpdater.Indexable,shapeless.HNil]]] updateCopy(existing, item)

Which I think makes sense given that the Poly2 is working on instances of the items, and not the expanded coproduct type (i.e. the implicits are generated for Item1 and not Indexable)

However, if I don't apply the poly2 with the PolyApply overload, and instead do:

def mergeWithExisting(existing: IndexedItem, item: Indexable): IndexedItem = {
  item.foldLeft(existing)(updateCopy)
}

Then it does work. I'm not sure what the foldleft is doing such that the types resolve. If this is the accepted way the how do I make this generic such that I can use a Poly3? or Poly4?

Is there any way to widen the type in the poly to get this to work with the apply method? Maybe I'm going about this the wrong way, I'm open to suggestions

like image 912
devshorts Avatar asked Oct 19 '22 00:10

devshorts


1 Answers

In order to left fold a coproduct with a Poly2, the function needs to provide cases of type Case.Aux[A, x, A] where A is the (fixed) accumulator type and x is each element in the coproduct.

Your updateCopy does exactly this for accumulator type IndexedItem and coproduct Indexable, so you can left fold an Indexable with an initial IndexedItem to get an IndexedItem. If I understand correctly, this is exactly what you want—the unique appropriate case in updateCopy will be applied to the initial IndexedItem and the coproduct's value and you'll get an updated IndexedItem.

It's a little unintuitive to think of this operation as a "left fold", and you could alternatively write this as an ordinary fold that simply collapses the coproduct to a value.

object updateCopy extends Poly1 {
  type U = IndexedItem => IndexedItem

  implicit val caseItem1: Case.Aux[Item1, U] = at[Item1](i => _.copy(item1 = i))
  implicit val caseItem2: Case.Aux[Item2, U] = at[Item2](i => _.copy(item2 = i))
  implicit val caseItem3: Case.Aux[Item3, U] = at[Item3](i => _.copy(item3 = i))
}

And then:

def mergeWithExisting(existing: IndexedItem, item: Indexable): IndexedItem =
  item.fold(updateCopy).apply(existing)

I personally find this a little more readable—you're collapsing the coproduct down to an update function and then applying that function to the existing IndexedItem. This is probably mostly a matter of style, though.


You could create a Poly2 with a single Case.Aux[IndexedItem, Indexable, IndexedItem] case that would allow you to use apply directly, but that would be more verbose and less idiomatic than one of the fold approaches (also at that point you wouldn't even need a polymorphic function value—you could just use an ordinary (IndexedItem, Indexable) => IndexedItem).


Lastly, I'm not sure exactly what you mean by extending the fold approach to Poly3, etc., but if what you want is to provide additional initial values to be transformed, then you could make the accumulator type a tuple (or Tuple3, etc.). For example:

object updateCopyWithLog extends Poly2 {
  type I = (IndexedItem, List[String])

  implicit val caseItem1: Case.Aux[I, Item1, I] = at {
    case ((a, log), b) => (a.copy(item1 = b), log :+ "1!")
  }

  implicit val caseItem2: Case.Aux[I, Item2, I] = at {
    case ((a, log), b) => (a.copy(item2 = b), log :+ "2!")
  }

  implicit val caseItem3: Case.Aux[I, Item3, I] = at {
    case ((a, log), b) => (a.copy(item3 = b), log :+ "2!")
  }
}

And then:

scala> val example: Indexable = Coproduct(Item1(10))
example: Indexable = Inl(Item1(10))

scala> val existing: IndexedItem = IndexedItem(Item1(0), Item2(), Item3())
existing: IndexedItem = IndexedItem(Item1(0),Item2(),Item3())

scala> example.foldLeft((existing, List.empty[String]))(updateCopyWithLog)
res0: (IndexedItem, List[String]) = (IndexedItem(Item1(10),Item2(),Item3()),List(1!))

If that's not what you meant by the Poly3 part I'd be happy to expand the answer.


As a footnote, the LeftFolder source suggests that cases can have output types that aren't the same as the accumulator type, since tlLeftFolder has a OutH type parameter. This seems a little odd to me, since as far as I can tell OutH will necessarily always be In (and the Shapeless tests pass if you remove OutH and just use In). I'll take a closer look and maybe open an issue.

like image 118
Travis Brown Avatar answered Nov 15 '22 06:11

Travis Brown