A Sighting of filterA in Typelevel Rite of Passage

pjschwarz 40 views 23 slides Jun 03, 2024
Slide 1
Slide 1 of 23
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23

About This Presentation

Slide deck home: https://fpilluminated.com/deck/220


Slide Content

filterA
a sighting of
in
@philip_schwarzslides byhttp://fpilluminated.com/
by
Typelevel Rite of Passage
Build a Full-Stack Application with
Scala 3 and the Typelevel Stack

Thisdeckisbasedonabout60secondsof
a35-hourvideocoursecalledTypelevel
RightofPassage,anintroductiontowhich
isavailableonYouTube.
@philip_schwarz

Daniel Ciocirlan
@rockthejvm
Iamgoingtocomparethisuser’spasswordagainstthehashthatisalreadystoredinthedatabase.
/** Check against a bcrypt hash in a pure way
*
* It may raise an error for a malformed hash
*/
defcheckpwBool[F[_]](password: String, hash: PasswordHash[A])
(implicitP: PasswordHasher[F, A]): F[Boolean] = …
deflogin(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))
yieldmaybeJwtToken
deffind(email: String): F[Option[User]]
BCrypt
The Typelevel Rite of Passage

Daniel Ciocirlan
@rockthejvm
/** Check against a bcrypt hash in a pure way
*
* It may raise an error for a malformed hash
*/
defcheckpwBool[F[_]](password: String, hash: PasswordHash[A])
(implicitP: PasswordHasher[F, A]): F[Boolean]= …
deflogin(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))
yieldmaybeJwtToken
deffind(email: String): F[Option[User]]
However,thecalltocheckpwBoolreturnsanF[Boolean],soourtypesarealittlebitscreweduphere.
BCrypt
The Typelevel Rite of Passage

Daniel Ciocirlan
@rockthejvm
/** Check against a bcrypt hash in a pure way
*
* It may raise an error for a malformed hash
*/
defcheckpwBool[F[_]](password: String, hash: PasswordHash[A])
(implicitP: PasswordHasher[F, A]): F[Boolean] = …
deflogin(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))
yieldmaybeJwtToken
deffind(email: String): F[Option[User]]
WehaveanOption[User],andthenIamgoingtocallfilteronafunctionUser=>IO[Boolean],
andIwouldneedtoreturnanIO[Option[User]], so that later I can use maybeValidatedUser.
BCrypt
The Typelevel Rite of Passage

Daniel Ciocirlan
@rockthejvm
Atthispointwehavetwoimpropertypes.OneistheIO[Boolean],duetothefactthat
filterdoesnotacceptafunctionreturninganIO[Boolean],butratherasimpleBoolean.
/** Check against a bcrypt hash in a pure way
*
* It may raise an error for a malformed hash
*/
defcheckpwBool[F[_]](password: String, hash: PasswordHash[A])
(implicitP: PasswordHasher[F, A]): F[Boolean] = …
deflogin(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))
yieldmaybeJwtToken
deffind(email: String): F[Option[User]]
BCrypt
The Typelevel Rite of Passage

Daniel Ciocirlan
@rockthejvm
Andtheotheristhereturnvalueoffilter,becauseitisan
Option[User],ratherthananeffectwrappingOption[User].
/** Check against a bcrypt hash in a pure way
*
* It may raise an error for a malformed hash
*/
defcheckpwBool[F[_]](password: String, hash: PasswordHash[A])
(implicitP: PasswordHasher[F, A]): F[Boolean] = …
deflogin(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))
yieldmaybeJwtToken
deffind(email: String): F[Option[User]]
BCrypt
The Typelevel Rite of Passage

Daniel Ciocirlan
@rockthejvm
WhichiswhyIamgoingtousealittletrick.IamgoingtocallfilterA,whichisanextensionmethod(Ithinkit
comesfromtheTraversetypeclass).
OntheOptionofaparticulartype[e.g.User],youcancallfilterAwithafunctionreturningadifferentkindof
effectGwrappingaBoolean,soyou’llthenreturnaneffectGwrappingthisOption[User].
/** Check against a bcrypt hash in a pure way
*
* It may raise an error for a malformed hash
*/
defcheckpwBool[F[_]](password: String, hash: PasswordHash[A])
(implicitP: PasswordHasher[F, A]): F[Boolean] = …
deflogin(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))
yieldmaybeJwtToken
deffind(email: String): F[Option[User]]
BCrypt
The Typelevel Rite of Passage

filterAistotraversewhatfilteristomap
FunctionFromGivenToType Class
mapF[A]A => BF[B]Functor[F]
filterF[A]A => BooleanF[A]FunctorFilter[F]
traverseF[A]A => G[B]G[F[B]]Traverse[F]
filterAF[A]A => G[Boolean]G[F[A]]TraverseFilter[F]
G is an Applicative (every Monad is an Applicative)

assert(List(1,2,3,4).map(_.toString) == List("1","2","3","4"))
def isEven(n: Int): Boolean = n % 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)
mapF[A]A => BF[B]Functor[F]
filterAF[A]A => G[Boolean]G[F[A]]TraverseFilter[F]
filterF[A]A => BooleanF[A]FunctorFilter[F]
traverseF[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.

filterAF[A]A => G[Boolean]G[F[A]]
filterAOption[User]User => IO[Boolean]IO[Option[User]]
FunctionFromGivenTo
filterAList[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)
defcheckpwBool[F[_]](p: String, hash: PasswordHash[A])(implicitP: 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.

ObviouslythebehaviouroffilterA,whichisreflectedinitsresult,dependsonthebehaviourofaparticularApplicativeG.
SofarwehaveseenexampleswithG=OptionandG=IO.
JustasanexampleofthetypeofbehaviourthatwecanachievewhenG=List,hereisafunctionthatusesfilterAto
computethepowersetofaset(thelistofsublistsofalist).
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
filterAF[A]A => G[Boolean]G[F[A]]
FunctionFromGivenTo
filterAList[Int]Int => List[Boolean]List[List[Int]]

IjustrealisedtheusingfilterAtocomputeapowersetis
actuallyoneoftheexamplesinthedocumentationoffilterA!

IfyouwanttoknowmoreabouthowfilterAworkswhencomputingthe
powersetfunction,seeslides256–276ofthefollowingslidedeck.

map traverse
Bytheway,didyouknowthatthereisafurtherfunctionthatisacombinationof
mapandfilter,andanotheronethatisacombinationoftraverseandfilterA?
@philip_schwarz

map traverse
Notsurprisingly,thefunctionsarecalledmapFilterandtraverseFilter.

Whataboutsequence,whichiscloselyrelatedtotraverse?
Becausesequenceisjusttraverse(x=>x),itdoesn’tmakesenseforthesequence
equivalentoffilterAtoexist,butsequenceFilterdoesexist(seenextslide).
traversesequence

Seenextslideforafewmorevariations
oftheexamplegivenabove.

Inconclusion,thenextsliderecapsthesignatures
ofallthefunctionsthatwehavementioned.
@philip_schwarz

FunctionFromGivenToType Class
mapF[A]A => BF[B]Functor[F]
filterF[A]A => BooleanF[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.
mapFilterF[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.
traverseF[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.
filterAF[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.
traverseFilterF[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.
sequenceF[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]].
sequenceFilterF[G[Option[A]] G[F[A]]TraverseFilter[F]traverseFilter with identity
Tags