Skip to content

Instantly share code, notes, and snippets.

@lukestewart13
Last active September 4, 2018 22:49
Show Gist options
  • Save lukestewart13/aacb569a6a0ba997b42d3fd0f8807f69 to your computer and use it in GitHub Desktop.
Save lukestewart13/aacb569a6a0ba997b42d3fd0f8807f69 to your computer and use it in GitHub Desktop.
Implicit class helper implementation for String, etc., validations (e.g. for validating dirty JSON)
import cats.instances.option._
import cats.{Id, Monad}
import scala.language.higherKinds
/**
* This class solves the problem where you need to perform essentially the same operation, via implicit classes, upon both raw values (Int, String, etc.) and
* also upon their optional equivalents (Option[Int], Option[String], etc.). Normally, for each operation, you would need to write two implicit classes:
* {{{
* implicit class StringImplicits(s: String) {
* def someOperation: String = ???
* }
* implicit class OptionStringImplicits(os: Option[String]) {
* def someOperation: Option[String] = os.map(s => s.someOperation)
* }
* }}}
* Alternatively, for each option, in your code, you could call .map on the option and then call the value implicit. Both are tedious, particularly when
* calling the operation numerous times throughout your code.
*
* The implicits in this class allow you to write just a single implicit class that applies to both values and their optional equivalents, and provides two
* utility methods for use in building the single class, depending on the desired behavior of the operation. The single implicit class has a specific template.
* For example:
* {{{
* implicit class ValidateString[A, M[_]](a: A)(implicit e: ValidationBuilder[A, String, M]) {
* def someOperation: M[String] = a.mapIfOption(s => ???)
* def someOtherOperation: Option[String] = a.flatMapToOption(s => ???)
* }
* }}}
* Note that the value types (Int, String, etc.) are specified in the second type parameters of the implicit ValidationBuilder parameter. If the value itself
* has type parameters, such as Seq[T], you can do the following:
* {{{
* implicit class ValidateSeq[A, M[_], T](a: A)(implicit e: ValidationBuilder[A, Seq[T], M]) {
* def someOperation: M[Seq[T]] = a.mapIfOption(seq => ???)
* def someOtherOperation: Option[Seq[T]] = a.flatMapToOption(seq => ???)
* }
* }}}
*
* The two utility methods differ in their behavior. For the mapIfOption method:
* 1) If the operand is non-optional, then the returned value is also non-optional
* 2) If the operand is optional, then the returned value is wrapped in the option also
* For the flatMapToOption method:
* 1) if the operand is non-optional, then the returned value will be wrapped in an option
* 2) if the operand is optional, then the returned value will remain wrapped in an option (not a nested option, assuming the closure does not nest)
* Note that the flatMapToOption method's closure argument requires a function that maps to an option.
*
* Your implicit class can then be used as follows, for example on strings or optional strings using the implicit class defined above:
* {{{
* "foo".someOperation //returns a non-optional value
* Option("foo").someOperation //returns the value in an option
* "foo".someOtherOperation //returns an optional value
* Option("foo").someOtherOperation //also returns an optional value
* }}}
*/
object ValidationTools {
implicit class Mappings[A, B, M[_]](a: A)(implicit e: ValidationBuilder[A, B, M]) {
def mapIfOption[C]: (B => C) => M[C] = e.monad.map(e.apply(a))
def flatMapToOption[C]: (B => Option[C]) => Option[C] = implicitly[Monad[Option]].flatMap(e.asOption(a))
}
sealed trait ValidationBuilder[A, B, M[_]] {
def apply(a: A): M[B]
def monad: Monad[M]
def asOption(a: A): Option[B]
}
object ValidationBuilder {
//I am not sure why the compiler prioritizes OptionValidationBuilders over NonOptionValidationBuilders, but it does
implicit def OptionValidationBuilder[A]: ValidationBuilder[Option[A], A, Option] = {
new ValidationBuilder[Option[A], A, Option] {
def apply(ma: Option[A]): Option[A] = ma
def monad: Monad[Option] = implicitly[Monad[Option]]
def asOption(ma: Option[A]): Option[A] = apply(ma)
}
}
//I give you one hole. Should you wish for more holes then write your own or be fancy and use a type lambda (I honestly have no idea if that will work)
implicit def OptionKindValidationBuilder[A[_], B]: ValidationBuilder[Option[A[B]], A[B], Option] = {
new ValidationBuilder[Option[A[B]], A[B], Option] {
def apply(ma: Option[A[B]]): Option[A[B]] = ma
def monad: Monad[Option] = implicitly[Monad[Option]]
def asOption(ma: Option[A[B]]): Option[A[B]] = apply(ma)
}
}
implicit def NonOptionValidationBuilder[A]: ValidationBuilder[A, A, Id] = {
new ValidationBuilder[A, A, Id] {
def apply(a: A): Id[A] = a
def monad: Monad[Id] = implicitly[Monad[Id]]
def asOption(a: A): Option[A] = Option(a)
}
}
implicit def NonOptionKindValidationBuilder[A[_], B]: ValidationBuilder[A[B], A[B], Id] = {
new ValidationBuilder[A[B], A[B], Id] {
def apply(a: A[B]): Id[A[B]] = a
def monad: Monad[Id] = implicitly[Monad[Id]]
def asOption(a: A[B]): Option[A[B]] = Option(a)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment