The short answer is that you can’t return both A
and Option[A]
like that.
The more interesting answer (and I will note that I’m not an expert in this sort of thing, so forgive any errors) is that you can abstract the return type, if you’re willing to go to some effort. The fundamental problem is that A
and Option[A]
are shaped differently – in technical terms, they are different kinds. Whether something takes a type parameter or not goes into its kind. You can’t casually mix different kinds, the way you’re trying to do.
The way you can combine them is, instead of returning A
, you return Id[A]
, where Id (in the Cats library) is a thin wrapper that exists mainly to solve this sort of problem. Id
and Option
are both Applicative
, so your combined function would essentially return Applicative[A]
.
More precisely, you’d wind up with a function signature like this, which says that you want to write this function in a way that can work for any Applicative
:
def generalizedAttribute[A, F[_] : Applicative](parse: String => Try[A])(input: Option[String]): Either[ValidationError, F[A]]
That’s still not enough, though, because you want to behave differently depending on the type of F
, and you can’t just pattern-match on F
(since it is a type parameter). So you would need to add a typeclass describing the behavior, something along the lines of:
trait ValidatesMissing[F[_]] {
def onMissing[A]: Either[ValidationError, F[A]]
}
You’d then define typeclass instances of ValidatesMissing
for Option
and Id
(returning the values you currently have in requiredAttribute
vs optionalAttribute
), and call ValidatesMissing.onMissing
if the attribute isn’t there. And you’d have to add that into the function signature.
So the function becomes something like (note that this is off-the-cuff, and I haven’t tried compiling it, so there may be mistakes here, but I think it’s in the right direction):
def generalizedAttribute[A, F[_]](
parse: String => Try[A]
)(
input: Option[String]
)(implicit
ap: Applicative[F],
validatesMissing: ValidatesMissing[F]
): Either[ValidationError, F[A]] = {
case None => validatesMissing.onMissing
case Some(value) => parse(value) match {
case Success(value) => Right(ap.pure(value))
case Failure(exception) => Left(TypeValidationError(exception.getMessage))
}
}
That’s delving pretty deeply into higher-end Scala, though, and it’s likely much more effort than it’s worth for just this small case. And it still requires you to declare your F
type at the call site, so it’s a bit annoying to use. If you were doing a lot of this sort of thing, though, or wanted a full chain of functions that abstracted out the returned type (or just want to get your feet wet in the deep end of the pool), this is probably the way to tackle it.
If you’re interested in this sort of thing, I’d recommend reading through the Cats documentation, which provides a lot of guidance about how to think in these terms…