1

I've got a trait which has an implicit cats.Applicative, and I have a case class which implements that trait. I want to be able to call methods from cats.syntax.apply on tuples of that case class, but the compiler doesn't realize it needs to go looking for the Applicative for the trait, and instead fails while trying to look for the Applicative for the case class.

For example: (note this is not my actual class, just a distillation of the problem)

import cats.arrow.FunctionK
import cats.Applicative
import cats.syntax.apply._

// The trait
trait GenericLookup[+A] {
  def get(map: FunctionK[Key, Option]): Option[A]
}
object GenericLookup {
  implicit val applicative: Applicative[GenericLookup] = /* ... */
}

// The subclass
case class Key[A](keyword: String) extends GenericLookup[A] {
  def get(map: FunctionK[Key, Option]) = map(this)
}

// Some instances
val Foo = Key[String]("foo")
val Bar = Key[Boolean]("bar")
val Age = Key[Int]("age")

I want to be able to do e.g. (Foo, Bar, Age).tupled to get a GenericLookup[(String, Boolean, Int)], or call (Foo, Bar, Age).mapN { /* ... */ } to get a GenericLookup[Whatever]. But:

val fba = (Foo, Bar, Age).tupled
// could not find implicit value for parameter invariant: cats.Invariant[Key]

It wants to find a typeclass for Key, whereas I want it to find the typeclass for GenericLookup.

The best thing I've come up with so far is to add

def generic: GenericLookup[A] = this

to the Key class, which lets me do

(Foo.generic, Bar, Age).tupled

which seems to convince the compiler to go find the typeclass for GenericLookup instead of Key...

Is there a more automatic way I could set things up that would allow the use of (Foo, Bar, Age).tupled without having to add extra operators or syntax?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Dylan
  • 13,645
  • 3
  • 40
  • 67
  • 3
    I doubt it can work, this is basically the same reason why there are not instances for `Some` and `None` only for `Option`. I would guess the best you can do is have a trait that the companions of each subtype of `GenericLookup` can inherit and automatically get an instance of `Applicative` for that. - Another one, maybe to do something like `implicit def applicative[F[x] <: GenericLookup[x]]: Applicative[F] = ...` – Luis Miguel Mejía Suárez Jan 31 '23 at 15:41
  • Unfortunate, but expected. Thanks for your comment. Sadly, for my use-case I can't define an `Applicative[Key]` (even using that `F[x] <: G[x]` approach) due to the actual class having special semantics around being a "single" thing, so I guess I'm stuck with the "down-cast" approach. – Dylan Jan 31 '23 at 16:12
  • The problem with the downcast is that then `tupled` returned a `GenericLookup` instead of `Key` which I assume is not what you want and then you would need to cast with `asInstanceOf` - I wonder why the `trait` doesn't work? – Luis Miguel Mejía Suárez Jan 31 '23 at 16:16
  • Actually it _is_ what I want. I mentioned special semantics on the Key type so I _don't_ want an `Applicative[Key]` to be defined, since for my particular use-case, combining multiple Keys together _should_ result in a `GenericLookup` instead of a `Key`. If it weren't for that, I probably would not have split it up this way in the first place. – Dylan Jan 31 '23 at 18:43
  • Ah okay, that makes sense and yeah the upcast seems like the best option just like **cats** provides the smart constructors `foo.some` and `none[Bar]` to get better type inference. - Having said that, I wonder if this is _"fixable"_ in **Scala 3**? An automatic upcast doesn't sound like a bad idea in the general sense but maybe I am being too naive. Nevertheless, I think it would be worth raising this on the contributors **Discourse**. – Luis Miguel Mejía Suárez Jan 31 '23 at 18:47
  • https://stackoverflow.com/questions/47181109/calling-generic-function-with-functor-using-subclass-cats-scalaz – Dmytro Mitin Feb 01 '23 at 07:24
  • @Dylan In `def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]` `F` in `F[A => B]` and `F[A]` is in a contravariant position while in `F[B]` it's in a covariant position. So the only option for `F` is to be invariant. That's the reason why it's `Applicative[F[_]]` and not `Applicative[+F[_]]` or `Applicative[-F[_]]`. So generally there is no way to derive `Applicative` for a subtype or supertype if there is an instance for `F`. – Dmytro Mitin Feb 01 '23 at 13:06
  • @Dylan *"I can't define an `Applicative[Key]` (even using that `F[x] <: G[x]` approach) due to the actual class having special semantics around being a "single" thing"* Just curious, what does this mean – Dmytro Mitin Feb 01 '23 at 13:08
  • @DmytroMitin it's related to the business logic. A `Key` represents a field to eventually be displayed in the UI, and so it has a variety of methods that only make sense for a single field (like `asTextInput` which returns a text field builder). If I combine two keys, those methods no longer make sense, therefore an `Applicative[Key]` doesn't make sense. Whereas `GenericLookup` is more concerned with interpreting values from a submitted form. – Dylan Feb 01 '23 at 14:47
  • 2
    If you don't need to use `Key` type directly you can define `def key[A](name: String): GenericLookup[A] = Key(name)` and use it like `x.some` or `none` from @LuisMiguelMejíaSuárez example from Cats. – Mateusz Kubuszok Feb 02 '23 at 13:29

0 Answers0