|
| 1 | +package ch10 |
| 2 | + |
| 3 | +import cats.data.NonEmptyList |
| 4 | +import cats.data.Kleisli |
| 5 | +import cats.syntax.apply.catsSyntaxTuple2Semigroupal |
| 6 | +import Predicate.* |
| 7 | + |
| 8 | +type Errors = NonEmptyList[String] |
| 9 | + |
| 10 | +def error(s: String): NonEmptyList[String] = |
| 11 | + NonEmptyList(s, Nil) |
| 12 | + |
| 13 | +type Result[A] = Either[Errors, A] |
| 14 | + |
| 15 | +// Kleisli lets us sequence monadic transforms, |
| 16 | +// A => F[B] `flatMap` B => F[C] |
| 17 | +type Check[A, B] = Kleisli[Result, A, B] |
| 18 | + |
| 19 | +def check[A, B](func: A => Result[B]): Check[A, B] = |
| 20 | + Kleisli(func) |
| 21 | + |
| 22 | +def checkPred[A](pred: Predicate[Errors, A]): Check[A, A] = |
| 23 | + // Running the predicate produces a func of type: |
| 24 | + // A => Either[NonEmptyList[String], A] |
| 25 | + // = A => Result[A] |
| 26 | + // |
| 27 | + // We must be able to convert a Predicate to a function, |
| 28 | + // as Kleisli only works with functions. |
| 29 | + // When we convert a Predicate to a function, |
| 30 | + // it should have type A => Either[E, A] rather than |
| 31 | + // A => Validated[E, A] because Kleisli relies on the |
| 32 | + // wrapped function returning a monad. |
| 33 | + Kleisli[Result, A, A](pred.run) |
| 34 | + |
| 35 | +def longerThan(n: Int): Predicate[Errors, String] = |
| 36 | + Predicate.lift( |
| 37 | + error(s"Must be longer than $n characters"), |
| 38 | + str => str.size > n |
| 39 | + ) |
| 40 | + |
| 41 | +val alphanumeric: Predicate[Errors, String] = |
| 42 | + Predicate.lift( |
| 43 | + error(s"Must be all alphanumeric characters"), |
| 44 | + str => str.forall(_.isLetterOrDigit) |
| 45 | + ) |
| 46 | + |
| 47 | +def contains(char: Char): Predicate[Errors, String] = |
| 48 | + Predicate.lift( |
| 49 | + error(s"Must contain the character $char"), |
| 50 | + str => str.contains(char) |
| 51 | + ) |
| 52 | + |
| 53 | +def containsOnce(char: Char): Predicate[Errors, String] = |
| 54 | + Predicate.lift( |
| 55 | + error(s"Must contain the character $char only once"), |
| 56 | + str => str.filter(_ == char).size == 1 |
| 57 | + ) |
| 58 | + |
| 59 | +// Kleisli[[A] =>> Either[Errors, A], String, String] |
| 60 | +val checkUsername: Check[String, String] = |
| 61 | + checkPred(longerThan(3) `and` alphanumeric) |
| 62 | + |
| 63 | +val splitEmail: Check[String, (String, String)] = |
| 64 | + check(_.split('@') match { |
| 65 | + case Array(name, domain) => |
| 66 | + Right((name, domain)) |
| 67 | + |
| 68 | + case _ => |
| 69 | + Left(error("Must contain a single @ character")) |
| 70 | + }) |
| 71 | + |
| 72 | +val checkLeft: Check[String, String] = |
| 73 | + checkPred(longerThan(0)) |
| 74 | + |
| 75 | +val checkRight: Check[String, String] = |
| 76 | + checkPred(longerThan(3) `and` contains('.')) |
| 77 | + |
| 78 | +val joinEmail: Check[(String, String), String] = |
| 79 | + check: |
| 80 | + case (l, r) => |
| 81 | + (checkLeft(l), checkRight(r)).mapN(_ + "@" + _) |
| 82 | + |
| 83 | +val checkEmail: Check[String, String] = |
| 84 | + splitEmail `andThen` joinEmail |
0 commit comments