Last active
September 4, 2018 22:49
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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