Hello, I have this typeclass with a custom implicit Not Found message
@implicitNotFound("${A} seems to be nullable")
sealed trait NotNull[A]
I also have a given
using summonFrom
inline given instance[A]: NotNull[A] =
summonFrom {
case given Nullable[A] => error("Type seems to be nullable")
case given (A <:< AnyVal) => ...
case given (A <:< AnyRef) => ...
}
My problem is that if none of the summonFrom cases matches, I get an ugly error message, not the custom message I defined in first code block.
Solution: Define a catchall case with error
message
inline given instance[A]: NotNull[A] =
summonFrom {
case given Nullable[A] => error("Type seems to be nullable.")
case given (A <:< AnyVal) => ...
case given (A <:< AnyRef) => ...
case _ => error("${A} seems to be nullable")
}
This kinda works, but there is no way of interpolating the A
like implicitNotFound
does.
Can we replace the “cannot reduce summonFrom” with the implicitNotFound message if it exists?
Another solution would be providing a compiletime method def nameOf[A]: String
that returns the type name at the call site.
2 Likes
You can get the name by introducing an implicit from the sourcecode library, no?
Thanks for the suggestion. Unfortunately, it didn’t work because error
needs a literal string.
Here is the code:
inline given instance[A](using inline A: sourcecode.Name): NotNull[A] = {
summonFrom {
case given Nullable[A] => error("Type seems to be nullable. Use .flatMap instead")
case given (A <:< AnyVal) => NotNull.dummyInstance.asInstanceOf[NotNull[A]]
case given (A <:< AnyRef) => NotNull.dummyInstance.asInstanceOf[NotNull[A]]
case _ => error(s"$A seems to be a param or opaque, which we can't prove as NotNull. You need to propagate the evidence to the scope, problably something like `def foo[A: NotNull]` or `def foo[A](a: A)(using NotNull[A])`")
}
}
and here is the error message
[error] 68 | def useMapWithoutNotNullInScope[A](f: Int => A): A | Null = maybeA.map(f) // Shows a compile time error suggesting to use NotNull typeclass
[error] | ^
[error] |A literal string is expected as an argument to compiletime.error
. Got root.scala.StringContext.apply(
[error] | [“”,
[error] |
[error] | " seems to be a param or opaque, which we can't prove as NotNull. You need to propagate the evidence to the scope, problably something like def foo[A: NotNull]
or def foo[A](a: A)(using NotNull[A])
"
[error] |
[error] | : String]*
[error] |).s(
[error] | [sourcecode.Name.apply(“useMapWithoutNotNullInScope”):sourcecode.Name : Any]*
[error] |)
Hmmm… Yeah that’s because sourcecode doesn’t return the singleton, but a String
. You can utilize the same mechanism sourcecode
is using internally and get the real literal, but then you need a macro.
Maybe if sourcecode was modified to return a singleton it would work. But I wonder, is string interpolation allowed at all? I’m not sure that is considered as a constant even if all its part are constant.
String interpolation doesn’t work. But String concatenation is constant folded:
scala> error(A + " seems to be nullable")
1 |error(A + " seems to be nullable")
|^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|Foo seems to be nullable
1 Like
Pretty cool:
scala> import scala.quoted.{Type, Expr, Quotes}
|
| def tpeNmeMacro[A: Type](using Quotes) = {
| val name = Type.show[A]
| Expr(name)
| }
| transparent inline def typeName[A] = ${tpeNmeMacro[A]}
scala> inline given instance[A]: NotNull[A] =
| summonFrom {
| case given (A <:< AnyVal) => ???
| case _ => error(typeName[A] + " seems to be nullable")
| }
|
scala> summon[NotNull[String]]
1 |summon[NotNull[String]]
| ^
| java.lang.String seems to be nullable
3 Likes
Is it worth it to contribute this typeName macro to scala.compiletime? I can see anyone using summonFrom needs it to mimic the implicitNotFound functionality.
1 Like
Forgot to say thanks for the help! Works flawlessly.