Haskell version of the Shopping Cart developed in the book Practical FP in Scala.
Within a Nix shell (run nix-shell
- recommended), follow the commands below.
cabal new-run shopping-cart
cabal new-run shopping-cart-tests
The original version of the Shopping Cart has been written in Scala. The Haskell application's design is quite similar.
- Services are represented using polymorphic records of functions.
- The Newtypes / Refined duo is used for strongly-typed data.
- Retry is used for retrying computations compositionally.
- Servant is used as the default HTTP server.
- Wreq is used as the default HTTP client.
- Hedis is used as the default Redis client.
- PostgreSQL Simple, PostgreSQL Resilient, and Raw Strings QQ are used to handle PostgreSQL stuff.
A polymorphic record of functions looks as follows:
data Brands m = Brands
{ findAll :: m [Brand]
, create :: BrandName -> m ()
}
Whereas in Scala, we represent it using trait
s (although case class
/ class
es would work too):
trait Brands[F[_]] {
def findAll: F[List[Brand]]
def create(name: BrandName): F[Unit]
}
We pass them along as explicit dependencies, though, so we don't treat them as typeclasses. In Scala, we use the same encoding for typeclasses (and then pass them implicitly) but in Haskell the encoding differs:
class Brands m where
findAll :: m [Brand]
create :: BrandName -> m ()
Typeclasses were used to encode effects such as Background
, Logger
and Retry
:
class Background m where
schedule :: m a -> Minutes -> m ()
instance Background IO where
schedule fa mins = void $ async (threadDelay (microseconds mins) >> fa)
where microseconds Mins {..} = 60000000 * unrefine unMins
In Scala, this is how it is encoded:
trait Background[F[_]] {
def schedule[A](fa: F[A], duration: FiniteDuration): F[Unit]
}
object Background {
def apply[F[_]: Background]: Background[F] = implicitly
implicit def bgInstance[F[_]](implicit S: Supervisor[F], T: Temporal[F]): Background[F] =
new Background[F] {
def schedule[A](fa: F[A], duration: FiniteDuration): F[Unit] =
S.supervise(T.sleep(duration) *> fa).void
}
}
Having an implicit implementation is how we define coherent instances, though, they can be easily overridden (but this is also possible in Haskell using the right extensions).
There are some features I don't plan to implement due to lack of time and motivation.
- JWT Authentication: if you want to get it done, here's some nice documentation on how to get started.
- Configuration: I recommend using Dhall. You can have a look at how it's done here.
- Tests: the machinery is in place but it's a ton of work to write tests. I'll leave that for another day :)