password against the hash that is already stored in the database. /** Check against a bcrypt hash in a pure way * * It may raise an error for a malformed hash */ def checkpwBool[F[_]](password: String, hash: PasswordHash[A]) (implicit P: PasswordHasher[F, A]): F[Boolean] = … def login(email: String, password: String): F[Option[JwtToken]] = for // find the user in the DB - return None if no user maybeUser <- users.find(email) // check password maybeValidatedUser <- maybeUser.filter(user => BCrypt.checkpwBool[F](password, PasswordHash[BCrypt](user.hashedPassword))) // Return a new token if password matches maybeJwtToken <- maybeValidatedUser.traverse(user => authenticator.create(user.email)) yield maybeJwtToken def find(email: String): F[Option[User]] BCrypt The Typelevel Rite of Passage
a pure way * * It may raise an error for a malformed hash */ def checkpwBool[F[_]](password: String, hash: PasswordHash[A]) (implicit P: PasswordHasher[F, A]): F[Boolean] = … def login(email: String, password: String): F[Option[JwtToken]] = for // find the user in the DB - return None if no user maybeUser <- users.find(email) // check password maybeValidatedUser <- maybeUser.filter(user => BCrypt.checkpwBool[F](password, PasswordHash[BCrypt](user.hashedPassword))) // Return a new token if password matches maybeJwtToken <- maybeValidatedUser.traverse(user => authenticator.create(user.email)) yield maybeJwtToken def find(email: String): F[Option[User]] However, the call to checkpwBool returns an F[Boolean], so our types are a little bit screwed up here. BCrypt The Typelevel Rite of Passage
a pure way * * It may raise an error for a malformed hash */ def checkpwBool[F[_]](password: String, hash: PasswordHash[A]) (implicit P: PasswordHasher[F, A]): F[Boolean] = … def login(email: String, password: String): F[Option[JwtToken]] = for // find the user in the DB - return None if no user maybeUser <- users.find(email) // Option[User].filter(User => IO[Boolean]) => IO[Option[User]] maybeValidatedUser <- maybeUser.filter(user => BCrypt.checkpwBool[F](password, PasswordHash[BCrypt](user.hashedPassword))) // Return a new token if password matches maybeJwtToken <- maybeValidatedUser.traverse(user => authenticator.create(user.email)) yield maybeJwtToken def find(email: String): F[Option[User]] We have an Option[User], and then I am going to call filter on a function User => IO[Boolean], and I would need to return an IO[Option[User]], so that later I can use maybeValidatedUser. BCrypt The Typelevel Rite of Passage
types. One is the IO[Boolean], due to the fact that filter does not accept a function returning an IO[Boolean], but rather a simple Boolean. /** Check against a bcrypt hash in a pure way * * It may raise an error for a malformed hash */ def checkpwBool[F[_]](password: String, hash: PasswordHash[A]) (implicit P: PasswordHasher[F, A]): F[Boolean] = … def login(email: String, password: String): F[Option[JwtToken]] = for // find the user in the DB - return None if no user maybeUser <- users.find(email) // Option[User].filter(User => IO[Boolean]) => IO[Option[User]] maybeValidatedUser <- maybeUser.filter(user => BCrypt.checkpwBool[F](password, PasswordHash[BCrypt](user.hashedPassword))) // Return a new token if password matches maybeJwtToken <- maybeValidatedUser.traverse(user => authenticator.create(user.email)) yield maybeJwtToken def find(email: String): F[Option[User]] BCrypt The Typelevel Rite of Passage
of filter, because it is an Option[User], rather than an effect wrapping Option[User]. /** Check against a bcrypt hash in a pure way * * It may raise an error for a malformed hash */ def checkpwBool[F[_]](password: String, hash: PasswordHash[A]) (implicit P: PasswordHasher[F, A]): F[Boolean] = … def login(email: String, password: String): F[Option[JwtToken]] = for // find the user in the DB - return None if no user maybeUser <- users.find(email) // Option[User].filter(User => IO[Boolean]) => IO[Option[User]] maybeValidatedUser <- maybeUser.filter(user => BCrypt.checkpwBool[F](password, PasswordHash[BCrypt](user.hashedPassword))) // Return a new token if password matches maybeJwtToken <- maybeValidatedUser.traverse(user => authenticator.create(user.email)) yield maybeJwtToken def find(email: String): F[Option[User]] BCrypt The Typelevel Rite of Passage
use a little trick. I am going to call filterA, which is an extension method (I think it comes from the Traverse typeclass). On the Option of a particular type [e.g. User], you can call filterA with a function returning a different kind of effect G wrapping a Boolean, so you’ll then return an effect G wrapping this Option[User]. /** Check against a bcrypt hash in a pure way * * It may raise an error for a malformed hash */ def checkpwBool[F[_]](password: String, hash: PasswordHash[A]) (implicit P: PasswordHasher[F, A]): F[Boolean] = … def login(email: String, password: String): F[Option[JwtToken]] = for // find the user in the DB - return None if no user maybeUser <- users.find(email) // Option[User].filter(User => G[Boolean]) => G[Option[User]] maybeValidatedUser <- maybeUser.filterA(user => BCrypt.checkpwBool[F](password, PasswordHash[BCrypt](user.hashedPassword))) // Return a new token if password matches maybeJwtToken <- maybeValidatedUser.traverse(user => authenticator.create(user.email)) yield maybeJwtToken def find(email: String): F[Option[User]] BCrypt The Typelevel Rite of Passage
From Given To Type Class map F[A] A => B F[B] Functor[F] filter F[A] A => Boolean F[A] FunctorFilter[F] traverse F[A] A => G[B] G[F[B]] Traverse[F] filterA F[A] A => G[Boolean] G[F[A]] TraverseFilter[F] G is an Applicative (every Monad is an Applicative)
2 == 0 assert(List(1,2,3,4).filter(isEven) == List(2,4)) def maybeDigit(c: Char): Option[Int] = Option.when(c.isDigit)(c.asDigit) assert(List('1','2','3','4').traverse(maybeDigit) == Some(List(1,2,3,4))) assert(List('1','2','x','4').traverse(maybeDigit) == None) def maybeEvenDigit(c: Char): Option[Boolean] = maybeDigit(c).map(isEven) assert(List('1','2','3','4').filterA(maybeEvenDigit) == Some(List('2','4'))) assert(List('1','2','x','4').filterA(maybeEvenDigit) == None) map F[A] A => B F[B] Functor[F] filterA F[A] A => G[Boolean] G[F[A]] TraverseFilter[F] filter F[A] A => Boolean F[A] FunctorFilter[F] traverse F[A] A => G[B] G[F[B]] Traverse[F] A = Int B = String F = List A = Char B = Int F = List G = Option Here are some examples of using map, filter, traverse, and filterA.
IO[Boolean] IO[Option[User]] Function From Given To filterA List[Char] Char => Option[Boolean] Option[List[Char]] def isEven(n: Int): Boolean = n % 2 == 0 def maybeDigit(c: Char): Option[Int] = Option.when(c.isDigit)(c.asDigit) def maybeIsEvenDigit(c: Char): Option[Boolean] = maybeDigit(c).map(isEven) assert(List('1','2','3','4').filterA(maybeIsEvenDigit) == Some(List('2','4'))) assert(List('1','2','x','4').filterA(maybeIsEvenDigit) == None) def checkpwBool[F[_]](p: String, hash: PasswordHash[A])(implicit P: PasswordHasher[F, A]): F[Boolean] = … for // find the user in the DB - return None if no user maybeUser <- users.findEmail(email) // check password - Option[User].filter(User => IO[Boolean]) => IO[Option[User]] maybeValidatedUser <- maybeUser.filterA(user => BCrypt.checkpwBool[F](p, PasswordHash[BCrypt](user.hashedPassword))) Here we compare our example of using filterA, with the usage of filterA seen in the course.
result, depends on the behaviour of a particular Applicative G. So far we have seen examples with G = Option and G = IO. Just as an example of the type of behaviour that we can achieve when G = List, here is a function that uses filterA to compute the powerset of a set (the list of sublists of a list). import cats.implicits.* def powerset[A](as: List[A]): List[List[A]] = as.filterA(_ => List(true, false)) assert(powerset(List(1, 2, 3)) == List(List(1, 2, 3), List(1, 2), List(1, 3), List(1), List(2, 3), List(2), List(3), List() ) ) A = Int F = List G = List filterA F[A] A => G[Boolean] G[F[A]] Function From Given To filterA List[Int] Int => List[Boolean] List[List[Int]]
sequence is just traverse(x => x), it doesn’t make sense for the sequence equivalent of filterA to exist, but sequenceFilter does exist (see next slide). traverse sequence
B F[B] Functor[F] filter F[A] A => Boolean F[A] FunctorFilter[F] Apply a filter to a structure such that the output structure contains all A elements in the input structure that satisfy the predicate f but none that don't. mapFilter F[A] A => Option[B] F[B] FunctorFilter[F] A combined map and filter. Filtering is handled via Option instead of Boolean such that the output type B can be different than the input type A. traverse F[A] A => G[B] G[F[B]] Traverse[F] Given a function which returns a G effect, thread this effect through the running of this function on all the values in F, returning an F[B] in a G context. filterA F[A] A => G[Boolean] G[F[A]] TraverseFilter[F] Filter values inside a G context. This is a generalized version of Haskell's filterM . This StackOverflow question about filterM may be helpful in understanding how it behaves. traverseFilter F[A] A => G[Option[B]] G[F[B]] TraverseFilter[F] A combined traverse and filter. Filtering is handled via Option instead of Boolean such that the output type B can be different than the input type A. sequence F[G[A]] G[F[A]] Traverse[F] Thread all the G effects through the F structure to invert the structure from F[G[A]] to G[F[A]]. sequenceFilter F[G[Option[A]] G[F[A]] TraverseFilter[F] traverseFilter with identity