2

I'm trying to create some simple custom String interpolator, and I'm successful as long as I don't try to use a type parameter.

import scala.concurrent.Future

object StringImplicits {
  implicit class FailureStringContext (val sc : StringContext) extends AnyVal {

    // This WORKS, but it's specific to Future :(  

    def fail[T](args : Any*): Future[T] = {
      val orig = sc.s (args : _*)
      Future.exception[T](new Exception(orig))
    }

    // I want this to work for Option,Try,Future!!

    def fail[M,T](args:Any*): M[T] = {
      val orig = sc.s (args : _*)

      // Obviously does not work.. 
      M match {
        case Future => Future.exception(new Exception(orig))
        case Option => None
        case Try => Failure(new Exception(orig))
        case  _ => ???
      }
    }
  }
}

Can I get this to work? I can't use parametric polymorphism because I'm not the one defining those three types.

What's the equivalent in the type level for that pseudo-code pattern match?

LATEST ATTEMPT

My latest attempt was to use implicitly, but I don't have such implicit! I'd be actually interested to grab a hold of the type that the compiler wants me to return according to type inference.

def fail[T, M[T]](args:Any*): M[T] = {
  val orig = sc.s(args: _*)

  implicitly[M[T]] match {
    case _:Future[T] => Future.exception(new Exception(orig))
    case _ => ???
  }
}


<console>:18: error: could not find implicit value for parameter e: M[T]
             implicitly[M[T]] match {
                       ^
<console>:19: error: value exception is not a member of object scala.concurrent.Future
               case _: Future[T] => Future.exception(new Exception(orig))
                                           ^
sscarduzio
  • 5,938
  • 5
  • 42
  • 54
  • See: `def test[A,B](a:Any):A[B] = ???` => compilation error: `A` does not take type parameters. `A` is generic and can be `Int` for example. Int[String] does not exist. So compiler will always complain. – Andrzej Jozwik Jul 07 '16 at 11:08
  • so maybe I need to tell the compiler that I want A to be a higher kind? How? – sscarduzio Jul 07 '16 at 11:12
  • 1
    `def fail[T, M[T]](...)` gets you part way. But I don't see how you will specify the M at the call site. Are you expecting the compiler to infer what to use for M based on the return type required? – The Archetypal Paul Jul 07 '16 at 11:26
  • @sscarduzio: Sorry to derail your question, but it soooo much seems like a bad idea. Is there a compelling reason why you'd feel in need to save a few key strokes (and trade `Future.failed` with a much less obvious `fail"..."`, making it polymorphic on top of that... **and** having the string silently ignored in case of an `Option`)? Or is it just for kicks? Anyway, if you feel like you need it, what is wrong with good old overloading? Defining a distinct overload for each of the types you want to support seems like the sanest solution. – Régis Jean-Gilles Jul 07 '16 at 11:29
  • @RégisJean-Gilles completely agree with you, but you see, this is how I learn things: exploratory exercises trying to flex the language's muscles and get a feel of what can be done! Thanks for your feedback :) – sscarduzio Jul 07 '16 at 11:33
  • 1
    Fair enough, that's a totally reasonable way to learn things (I tends abuse features too, just for fun and to explore). Just wanted to make sure you were not genuinely thinking it was a good idea for production code. – Régis Jean-Gilles Jul 07 '16 at 11:35
  • @TheArchetypalPaul I read type parameters can be passed to string interpolators like this xx""[Type1,Type2] But ideally The type inference would fill it in! – sscarduzio Jul 07 '16 at 11:43
  • f you're just going to be explicit about the type when you call it, might as well have xx_Type1"" and xx_Type2"".... I understand the need to see where the edges are, but this just feels like the wrong design – The Archetypal Paul Jul 07 '16 at 11:49

1 Answers1

2

In my opinion the simplest is to rely on good old overloading: just define a different overload for each type that you want to handle.

Now of course, there is the problem of having different overloads with the same signature, and as usual in scala, you can use tricks to work around them. Here we'll add dummy implicit parameters to force each overload to have a distinct signature. Not pretty but it works and will suffice in this case.

import scala.concurrent.Future
import scala.util.{Try, Failure}

implicit class FailureStringContext (val sc : StringContext) extends AnyVal {

  def fail[T](args : Any*): Future[T] = {
    Future.failed[T](new Exception(sc.s (args : _*)))
  }

  def fail[T](args : Any*)(implicit dummy: DummyImplicit): Option[T] = {
    Option.empty[T]
  }

  def fail[T](args : Any*)(implicit dummy: DummyImplicit, dummy2: DummyImplicit): Try[T] = {
    Failure[T](new Exception(sc.s (args : _*)))
  }
}

And tada:

scala> fail"oops": Option[String]
res6: Option[String] = None

scala> fail"oops": Future[String]
res7: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$KeptPromise@6fc1a8f6

scala> fail"oops": Try[String]
res8: scala.util.Try[String] = Failure(java.lang.Exception: oops)
Régis Jean-Gilles
  • 32,541
  • 5
  • 83
  • 97
  • Neat but the need to specify the type when you call it rather removes the point of overloading in the first place, it seems to me. Just call the `fail` functions different names - `failOption` etc. – The Archetypal Paul Jul 07 '16 at 11:50
  • There is no way around it, you have to give the expected type one way or another. But like for any type ascription, it is only necessary if you're not already in a context where the expected type is known. If you're using `fail` as the return value of a method with an explicit return type) by example, or passing as an argument to a non-generic method, no need to specify the type any more. – Régis Jean-Gilles Jul 07 '16 at 11:55
  • And admitedly, it's cleaner to just specify the type you want than to perform manual name-mangling... – Régis Jean-Gilles Jul 07 '16 at 11:56
  • 1
    You're using `dummy` to allow overloading, right? That's a hacky trick right there :) – Yuval Itzchakov Jul 07 '16 at 12:04
  • 1
    TIL "DummyImplicit" – sscarduzio Jul 07 '16 at 12:09
  • yeah with this solution I always need to coerce the type all the time, it's like not having any type inference `fail"boom":Future[Boolean]`. The only advantage here is that I'm using the same prefix `fail` for all the types. Close but no cigar! – sscarduzio Jul 07 '16 at 13:23
  • There's no coercion, it's a type ascription. In other words, you're telling the compiler what is the expected type, when inference cannot figure it out. As I said, you don't have to do it when the expected type is known. By example the following works perfectly: `def foo(o: Option[Int]) { println(o) }; foo(fail"oops")`. One way or another, the compiler needs to know which is the expected type (in my former snippet it is inferred from the context). It's not going to roll a dice to know which type to instantiate, right ^^ ? – Régis Jean-Gilles Jul 07 '16 at 13:28
  • So I tried to do this, and only two times the inference fails (when it's supposed to return Future[T] from a pattern match case, for some reason. But you're right, in other call sites, it normally it picks up the right one. Thanks! – sscarduzio Jul 07 '16 at 13:50