Unify two methods with different return signature

I have two methods that take an Option[String] and a parser (String =>Try[A]). One method returns a Either[ValidationError, A] the other a Either[Validation, Option[A]].

I would like to unify them into one method that also takes a required boolean and use that to switch on the None and Some(Success()) case with a pattern guard. The problem is I don’t know how to make it generic to allows A to represent both an Int and an Option[Int]/None. Is it possible?

object AttributeValidators {

  def requiredAttribute[A](parse: String => Try[A])(input: Option[String]): Either[ValidationError, A] = input match {
    case None => Left(SchemaValidationError("Missing required attribute"))
    case Some(value) => parse(value) match {
      case Success(value) => Right(value)
      case Failure(exception) => Left(TypeValidationError(exception.getMessage))
    }
  }

  def optionalAttribute[A](parse: String => Try[A])(input: Option[String]): Either[ValidationError, Option[A]] = input match {
    case None => Right(None)
    case Some(value) => parse(value) match {
      case Success(value) => Right(Some(value))
      case Failure(exception) => Left(TypeValidationError(exception.getMessage))
    }
  }
}

The JVM doesn’t allow overloading by return type.

You may be able to get what you want using the DummyImplicit in one of the two methods. However, that would imply that whoever you call your method, the compiler must know which return type do you want, which can be tricky an lead to an API that is tedious to use.

IMHO, the best would be to have both with different names, they somehow represent different bussiness behaviours.

1 Like

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…

1 Like

Thank you for the thorough answer. Good to know there wasn’t an obvious easy solution I couldn’t find. I’ve just started delving into Cats so I’ll recheck your answer once I’ve gotten more of the concepts under my belt.

Thank you, then I might’ve been on the right track from the beginning. Just wanted to double check there wasn’t an obviously easier way to do it through generics.

1 Like

Just sharing my two cents. Even if I am a typelevel fanboy myself. I really wouldn’t do all that just for the sake of having just one method name, unless there is a good reason to abstract it.

2 Likes

Oh, totally agreed. I just wound up diving down that rabbit hole because it’s a nice simple example that illustrates a couple of interesting power-Scala concepts. As the problem becomes more complex, it starts becoming worth the overhead…

2 Likes

You’re still missing a piece if you want a nice API (and why else would you do this anyway?).

F can’t be inferred by the compiler but ideally you don’t want to have to explicitly supply A. So in order to fix that you’d have to manually curry the type parameter list.

def generalizedAttribute[F[_] : Applicative : ValidatesMissing] = new GeneralizedAttributeBuilder[F]

class GeneralizedAttributeBuilder[F[_]](
  implicit 
  ap: Applicative[F], 
  validatesMissing: ValidatesMissing[F]
) {
  def apply[A](
    parse: String => Try[A]
  )(
    input: Option[String]
  ): Either[ValidationError, F[A]] = input match {
    case None => validatesMissing.onMissing
    case Some(value) => parse(value) match {
      case Success(value) => Right(ap.pure(value))
      case Failure(exception) => Left(TypeValidationError(exception.getMessage))
    }
  }
}
2 Likes

You can still improve this.

There is not reason for allocating the partially applied class.

object Foo {
  final class GeneralizedAttributePartiallyApplied[F[_]](private val dummy: Boolean) extends AnyVal {
    def apply[A](parse: String => Try[A])
                (input: Option[String])
                (implicit ap: Applicative[F], validatesMissing: ValidatesMissing[F]): Either[ValidationError, F[A]] =
      input match {
        case None => validatesMissing.onMissing
        case Some(value) => parse(value) match {
          case Success(value) => Right(ap.pure(value))
          case Failure(exception) => Left(TypeValidationError(exception.getMessage))
        }
      }
  }
  
  def generalizedAttribute[F[_]] =
    new GeneralizedAttributePartiallyApplied(dummy = true)
}
2 Likes

Just a trivia note: the JVM does allow overloading on return type:

Java the language does not. I guess scala does not since Java couldn’t deal with it, but a language or feature that didn’t care about Java compatibility could emit bytecode that did this.

2 Likes

Oh pretty cool. I always thought the limitation was from the JVM itself.

Thanks for sharing!

To be fair, Scala 3 can do it (which shows that it is possible on the JVM): you could have a return type of A | Option[A]. I suspect that that just kicks the can down the road, though – unifying the kinds is going to be a hassle somewhere, and using a union type with different kinds feels like a recipe for pain…

My solution wasn’t any good yet anyway. You always have to move the implicits from the constructor to the apply method, otherwise generalizedAttribute(s => Try(s.toInt)) would complain that you’re passing the wrong number of parameters and that Function1 doesn’t conform to Applicative. That’s what you get for quickly writing something up without testing first :sweat_smile:

1 Like