Daniel McHenry
Daniel McHenry

Reputation: 325

Why does creating a map function whose parameter is of type `Nothing => U` appear to work?

I'm writing Scala code that uses an API where calls to the API can either succeed, fail, or return an exception. I'm trying to make an ApiCallResult monad to represent this, and I'm trying to make use of the Nothing type so that the failure and exception cases can be treated as a subtype of any ApiCallResult type, similar to None or Nil. What I have so far appears to work, but my use of Nothing in the map and flatMap functions has me confused. Here's a simplified example of what I have with just the map implementation:

  sealed trait ApiCallResult[+T] {
    def map[U]( f: T => U ): ApiCallResult[U]
  }

  case class ResponseException(exception: APICallExceptionReturn) extends ApiCallResult[Nothing] {
    override def map[U]( f: Nothing => U ) = this
  }

  case object ResponseFailure extends ApiCallResult[Nothing] {
    override def map[U]( f: Nothing => U ) = ResponseFailure
  }

  case class ResponseSuccess[T](payload: T) extends ApiCallResult[T] {
    override def map[U]( f: T => U ) = ResponseSuccess( f(payload) )
  }

  val s: ApiCallResult[String] = ResponseSuccess("foo")
  s.map( _.size )  // evaluates to ResponseSuccess(3)

  val t: ApiCallResult[String] = ResponseFailure
  t.map( _.size )  // evaluates to ResponseFailure

So it appears to work the way I intended with map operating on successful results but passing failures and exceptions along unchanged. However using Nothing as the type of an input parameter makes no sense to me since there is no instance of the Nothing type. The _.size function in the example has type String => Int, how can that be safely passed to something that expects Nothing => U? What's really going on here?

I also notice that the Scala standard library avoids this issue when implementing None by letting it inherit the map function from Option. This only furthers my sense that I'm somehow doing something horribly wrong.

Upvotes: 2

Views: 187

Answers (2)

wheaties
wheaties

Reputation: 35980

Three things are aligning to make this happen, all having to do with covariance and contravariance in the face of a bottom type:

  1. Nothing is the bottom type for all types, e.g. every type is its super.
  2. The type signature of Function1[-T, +R], meaning it accepts any type which is a super of T and returns any type for which R is a super.
  3. The type ApiCallResult[+R] means any type U for which R is a super of U is valid.

So any type is a super of Nothing means both any argument type is valid and the fact that you return something typed around Nothing is a valid return type.

Upvotes: 3

Bob Dalgleish
Bob Dalgleish

Reputation: 8227

I suggest that you don't need to distinguish failures and exceptions most of the time.

type ApiCallResult[+T] = Try[T]
case class ApiFailure() extends Throwable

val s: ApiCallResult[String] = Success("this is a string")

s.map(_.size)

val t: ApiCallResult[String] = Failure(new ApiFailure)
t.map(_.size)

To pick up the failure, use a match to select the result:

t match {
case Success(s) =>
case Failure(af: ApiFailure) =>
case Failure(x) =>
}

Upvotes: 0

Related Questions