Manuel Schmidt
Manuel Schmidt

Reputation: 2489

Type-safe Builder: How to set compiler error message

I am using phantom types in the type-safe builder pattern to ensure methods are called only once as in the following code sample

  sealed trait TBoolean
  sealed trait TTrue extends TBoolean
  sealed trait TFalse extends TBoolean

  class Builder[MethodCalled <: TBoolean] private() {

    def foo()(implicit ev: MethodCalled =:= TFalse): Builder[TTrue] = {
      new Builder[TTrue]
    }
  }

  object Builder {
    def apply() = new Builder[TFalse]()
  }

Builder().foo().foo() does not work as required, however I would like to set the error message to something user-readable. At the moment the message is

Multiple markers at this line - not enough arguments for method foo: (implicit ev: =:=[W.TTrue,W.TFalse])W.Builder[W.TTrue]. Unspecified value parameter ev. - Cannot prove that W.TTrue =:= W.TFalse. - Cannot prove that W.TTrue =:= W.TFalse.

Upvotes: 3

Views: 252

Answers (2)

0__
0__

Reputation: 67330

Using the type parameters here is a bit of an overkill. Better just return a less capable type from the foo method:

object Builder {
  trait CanFoo { def foo() : Builder }
  def apply(): Builder with CanFoo = new Builder with CanFoo {
    def foo() = new Builder {}
  }
}
trait Builder

Builder().foo().foo() // value foo is not a member of Builder

There is an annotation implicitNotFound which can be used to customise the error messages, but it needs to be defined with the type that is sought (=:=) not with the use site (foo), so that is a pretty useless construction...

...unless you create your own replacement for =:=:

import annotation.implicitNotFound

object Called {
  implicit def same[A]: Called[A, A] = instance.asInstanceOf[Called[A, A]]
  private object instance extends Called[Any,Any]
}
@implicitNotFound(msg = "Cannot call this method twice") sealed trait Called[A, B]

class Builder[Foo <: TBoolean] private() {
  def foo()(implicit ev: Called[Foo, TFalse]): Builder[TTrue] = {
    new Builder[TTrue]
  }
}
object Builder {
  def apply() = new Builder[TFalse]()
}

Builder().foo().foo()  // -> "error: Cannot call this method twice"

Upvotes: 5

Rex Kerr
Rex Kerr

Reputation: 167911

You can't customize error messages, but you can customize your trait names. I'd call them Built and Unbuilt or something like that. Then you can warn users of the library or whatever that you'll get a hairy-looking error message, but they really only need to spot something that looks like Cannot prove that Built =:= Unbuilt.

Upvotes: 2

Related Questions