Reputation: 325
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
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:
Nothing
is the bottom type for all types, e.g. every type is its super. Function1[-T, +R]
, meaning it accepts any type which is a super of T
and returns any type for which R
is a super. 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
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