0

Is there a way to test whether a type is a member of a type class? E.g.:

trait Foo[A]

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node] extends Marshaller[Node] {
  override def isFoo(n: Node): Boolean = ???
}

Obviously there are solutions for executing code when the membership exists:

def isFoo(a: A)(implicit ev: Foo[A]) = // do something Foo-like with `a`

In my use case, I'm overriding the isFoo method, so I also can't change its signature.


the real-world problem

Sangria is a library for creating GraphQL services in Scala. It has a marshalling subsystem woven into it in the form of what is effectively a type class, InputUnmarshaller[Node]. In the code one can see type parameters qualified by context: In: InputUnmarshaller.

The notion is that one consumes input values and produces the output data set as a Value production, each element of which needs to be marshalled. The Node type can be restricted to, for example, io.circe.Json values, if one is using Circe for the marshalling.

There is also a Scala marshaller, which is quite dumb in that it only handles Map types as being map-like. The goal is to expand it to support case classes, for instance, via Shapeless and a map-like type class.

Mike
  • 323
  • 3
  • 13
  • 4
    `def isFoo[A](implicit ev: Foo[A] = null): Boolean = Option(ev).isDefined` ? Although, what is the purpose of this? A **typeclass** should be used to validate something at compile time, not at runtime. – Luis Miguel Mejía Suárez Oct 07 '21 at 20:32
  • 2
    In Scala 3 you can also use the `NotGiven` to kinda reverse the logic and use a method if no typeclass exists. – Gaël J Oct 07 '21 at 20:35
  • @GaëlJ Interesting. But I'm using Scala 2.12+. I'm curious about "Scala 2’s somewhat puzzling behavior with respect to ambiguity has been exploited to implement the analogue of a “negated” search in implicit resolution" – Mike Oct 07 '21 at 20:39
  • 1
    https://blog.rockthejvm.com/anti-implicits/ – Mike Oct 07 '21 at 20:43
  • @Mike well, that is because my signature is different from yours... you just need to adapt the code... - Anyways, it may be worth it if you can explain what exactly are you trying to do, because the mix of **Typeclasses** with runtime data and overriding smells fishy. – Luis Miguel Mejía Suárez Oct 07 '21 at 20:52
  • I'm not seeing a way to adapt Daniel Ciocîrlan's idea to the problem, since it would have to compile and to not compile. – Mike Oct 07 '21 at 20:58
  • @LuisMiguelMejíaSuárez The use case is that I'm implementing a trait, parametrized by type `Node`, that has an `isMapNode(n: Node): Boolean` method. So it should return `true` when `Node` is `Map[String,_]` or a case class or anything else that can be made to look like a map. A type class seems like the natural way to specify what looks like a map, and how it is made to look like one. – Mike Oct 07 '21 at 21:03
  • @Mike and what is `Node`? – Luis Miguel Mejía Suárez Oct 07 '21 at 21:08
  • @Mike then why do you want to make the check at runtime instead of a compile-time? Even more, why even make a check? Why are you checking if something is `MapLike`? – Luis Miguel Mejía Suárez Oct 07 '21 at 21:17
  • 1
    @Mike Is it possible that **LuisMiguelMejíaSuárez**'s `isFoo` produces "Method 'isFoo' overrides nothing" for you because of extra type parameter `A`? Is your `def isFoo` declared inside `trait Foo[A]`? It's not clear from your question. Where does `A` in `def isFoo(a: A): Boolean` come from? From `trait Foo[A]`? Then it's enough to remove `[A]` in Luis's `isFoo`: `def isFoo(implicit ev: Foo[A] = null): Boolean = Option(ev).isDefined`. – Dmytro Mitin Oct 07 '21 at 22:06
  • 1
    @GaëlJ I guess in Scala 3 in such situation pattern matching over implicits would be more convenient than `NotGiven`. See example `inline def setFor[T]: Set[T] = summonFrom { case given Ordering[T] => new TreeSet[T]; case _ => new HashSet[T] }` at https://docs.scala-lang.org/scala3/reference/metaprogramming/compiletime-ops.html – Dmytro Mitin Oct 07 '21 at 23:31
  • 1
    @Mike I think this is close to being an XY-Problem. - I would suggest editing this question _(or open a new one, whatever you prefer)_, where you explain what comes from a third party, what is yours, and what exactly is what you want to do. – Luis Miguel Mejía Suárez Oct 08 '21 at 01:01

1 Answers1

2

(1) Try to introduce type class IsFoo

trait Foo[A] 

trait IsFoo[A] {
  def value(): Boolean
}
trait LowPriorityIsFoo {
  implicit def noFoo[A]: IsFoo[A] = () => false
}
object IsFoo extends LowPriorityIsFoo {
  implicit def existsFoo[A](implicit foo: Foo[A]): IsFoo[A] = () => true
}

def isFoo[A](implicit isFooInst: IsFoo[A]): Boolean = isFooInst.value()

Testing:

implicit val intFoo: Foo[Int] = null

isFoo[Int] // true
isFoo[String] // false

Actually, I guess my isFoo is just a more complicated variant of @LuisMiguelMejíaSuárez's def isFoo[A](implicit ev: Foo[A] = null): Boolean = Option(ev).isDefined


(2) You want to define behavior of Marshaller via inheritance / subtyping polymorphism. And in JVM languages it's dispatched dynamically (lately, at runtime). Now you want to mix it with implicits / type classes (Foo) / ad hoc polymorphism dispatched statically (early, at compile time). You'll have to use some runtime tools like runtime reflection (to persist compile-time information about Node to runtime with TypeTags), runtime compilation.

import scala.reflect.runtime.universe.{TypeTag, typeOf, Quasiquote}
import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox
val tb = currentMirror.mkToolBox()

trait Foo[A]

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node: TypeTag] extends Marshaller[Node] {
// override def isFoo(n: Node): Boolean =
//   tb.inferImplicitValue(
//     tb.typecheck(tq"Foo[${typeOf[Node]}]", mode = tb.TYPEmode
//   ).tpe).nonEmpty
  override def isFoo(n: Node): Boolean = 
    util.Try(tb.compile(q"implicitly[Foo[${typeOf[Node]}]]")).isSuccess
}

implicit val intFoo: Foo[Int] = null
new MyMarshaller[Int].isFoo(1) //true
new MyMarshaller[String].isFoo("a") //false

Scala resolving Class/Type at runtime + type class constraint

Is there anyway, in Scala, to get the Singleton type of something from the more general type?

Load Dataset from Dynamically generated Case Class

Implicit resolution fail in reflection with ToolBox

In scala 2 or 3, is it possible to debug implicit resolution process in runtime?


(3) If you just want to check that Node is Map[String, _] then just runtime reflection is enough

import scala.reflect.runtime.universe.{TypeTag, typeOf}

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node: TypeTag] extends Marshaller[Node] {
  override def isFoo(n: Node): Boolean = typeOf[Node] <:< typeOf[Map[String, _]]
}

new MyMarshaller[Map[String, _]].isFoo(Map()) //true
new MyMarshaller[Int].isFoo(1) //false

See also Typeable

import shapeless.Typeable

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node] extends Marshaller[Node] {
  override def isFoo(n: Node): Boolean = Typeable[Map[String, _]].cast(n).isDefined
}

new MyMarshaller[Map[String, _]].isFoo(Map()) //true
new MyMarshaller[Int].isFoo(1) //false

(4) In Scala 3 you could use pattern matching by implicits

import scala.compiletime.summonFrom

trait Foo[A]

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node] extends Marshaller[Node] {
  override inline def isFoo(n: Node): Boolean = summonFrom {
    case given Foo[Node] => true
    case _ => false
  }
}

given Foo[Int] with {}

new MyMarshaller[Int].isFoo(1) //true
new MyMarshaller[String].isFoo("a") //false

(5) Actually, I guess the simplest would be to move implicit parameter from method to class

trait Foo[A]

trait IsFoo[A] {
  def value(): Boolean
}
trait LowPriorityIsFoo {
  implicit def noFoo[A]: IsFoo[A] = () => false
}
object IsFoo extends LowPriorityIsFoo {
  implicit def existsFoo[A: Foo]: IsFoo[A] = () => true
}

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node: IsFoo] extends Marshaller[Node] {
//                       ^^^^^  HERE
  override def isFoo(n: Node): Boolean = implicitly[IsFoo[Node]].value()
}

implicit val intFoo: Foo[Int] = null

new MyMarshaller[Int].isFoo(1) //true
new MyMarshaller[String].isFoo("a") //false

(6) @LuisMiguelMejíaSuárez's idea with default implicit also can be used in such case

trait Foo[A]

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node](implicit ev: Foo[Node] = null) extends Marshaller[Node] {
  override def isFoo(n: Node): Boolean = Option(ev).isDefined
}

implicit val intFoo: Foo[Int] = new Foo[Int] {}

new MyMarshaller[Int].isFoo(1) //true
new MyMarshaller[String].isFoo("a") //false
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • @Mike see update – Dmytro Mitin Oct 07 '21 at 23:12
  • @DmytroMitin I sometimes wonder how are you able to come with so many solutions :) – Luis Miguel Mejía Suárez Oct 08 '21 at 01:00
  • 1
    @LuisMiguelMejíaSuárez Best solutions are found latest :) – Dmytro Mitin Oct 08 '21 at 01:05
  • 1
    @Mike You should provide all necessary details in your question. – Dmytro Mitin Oct 08 '21 at 07:16
  • @Mike Please try solutions starting from the end. – Dmytro Mitin Oct 08 '21 at 07:23
  • The first suggestion won't work, for the same reason I gave earlier: it changes the signature of `isFoo`. The second looks like it could work, but heads deep into reflection that I was trying to avoid with type classes; so, not going there. The third won't work, because I'm also looking for case classes, which can be turned into maps. The fourth won't work for Scala 2.13. The fifth and sixth were the approach that I aimed for. Unfortunately, there's more to the problem. – Mike Oct 14 '21 at 03:06
  • …There's a `getMapValue(node: Node, key: String): Option[Node]` method that needs to be implemented. So it's "designed" to return a value from the map that's the same type as the map itself. For my use case, this would have to be `Any`. That ends up undermining all the work that I'm trying to do to ensure type safety. In short, `InputUnmarshaller` is a poorly designed interface. It was only designed to work in cases where `Node` was something like `Json` from Circe, and not be more restrictive than that. – Mike Oct 14 '21 at 03:10
  • [You also wrote something about `Refute` and `OrElse`](https://stackoverflow.com/a/68049625/1239389) that may be relevant to viewers of this issue. – Mike Oct 14 '21 at 03:15
  • @Mike Type class `Refute` checks that there is no implicit of a type. Type class `OrElse` checks that there is an implicit of at least one of two types. I used them there in order to prioritize implicits (make `default` lower priority than `toHList`). `OrElse`+`Refute` make `default` and `toHList` not intersect (not to be suitable candidates at the same time). I thought in the current question your problem is different. You want to add implicit context not changing the signature (not breaking inheritance/overriding). – Dmytro Mitin Oct 14 '21 at 08:44
  • @Mike Not sure I understood your complication with `getMapValue`. Is `getMapValue` like `isFoo` in your question? Feel free to edit your question so that the code reflects significant requirements (or open a new question). – Dmytro Mitin Oct 14 '21 at 08:44
  • FYI: [`getMapValue`](https://github.com/sangria-graphql/sangria-marshalling-api/blob/23c2319b1d5aa783fd43542bd11e2325227136f8/src/main/scala/sangria/marshalling/InputUnmarshaller.scala#L14) – Mike Oct 14 '21 at 18:46
  • @Mike Thank you for the link and accepting/upvoting my answer. If I understand correctly this `getMapValue` is like `isFoo` in your question. Right? Could you explain your complications of the last two approaches from my answer in your specific use case? If I understood correctly you have some complications. Right? If so you could consider to open a new question. I'm just curious. – Dmytro Mitin Oct 15 '21 at 00:43
  • @Mike Something with `Any` I guess. It's hard to understand :) – Dmytro Mitin Oct 15 '21 at 00:49