-
-
Save piq9117/823dce0f2bada6d29442da1e02972cdb to your computer and use it in GitHub Desktop.
{-# LANGUAGE EmptyDataDecls #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE GADTs #-} | |
{-# LANGUAGE GeneralizedNewtypeDeriving #-} | |
{-# LANGUAGE MultiParamTypeClasses #-} | |
{-# LANGUAGE OverloadedStrings #-} | |
{-# LANGUAGE QuasiQuotes #-} | |
{-# LANGUAGE TemplateHaskell #-} | |
{-# LANGUAGE TypeFamilies #-} | |
module Main where | |
import Database.Persist | |
import Database.Persist.Postgresql | |
import Database.Persist.TH | |
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| | |
Person | |
name String | |
age Int Maybe | |
deriving Show | |
|] | |
getPerson :: MonadIO m => PersionId -> ( Maybe ( Entity Person ) ) | |
getPerson pid = selectFirst [ PersionId ==. pid ] [ ] | |
connStr = "host=localhost dbname=persistent_expirement user=user password=user port=5432" | |
main :: IO () | |
main = runStderrLoggingT $ withPostgresqlPool connStr 10 $ \pool -> liftIO $ do | |
flip runSqlPersistMPool pool $ do | |
runMigration migrateAll |
When you take this approach, use a module structure analogous to this:
┣ Classes
┃ ┗ GetPerson.hs
┃ ┣ class GetPerson
┃ ┗ getPerson
┃ ...
┣ AppLogic
┃ ┗ EntryPoint.hs
┃ ┣ import Classes.GetPerson
┃ ┗ entryPoint
┃ ...
┃ ...
┗ AppWiring
┣ AppEnv.hs
┃ ┣ import Database.Persist
┃ ┣ data MyBackend
┃ ┣ data AppEnv
┃ ┗ initAppEnv
┣ App.hs
┃ ┣ import Database.Persist
┃ ┣ import Classes.GetPerson
┃ ┣ import AppWiring.AppEnv
┃ ┣ newtype App
┃ ┣ runApp
┃ ┗ instance GetPerson App
┗ Main.hs
┣ import AppWiring.AppEnv
┣ import AppWiring.App
┣ import AppLogic.EntryPoing
┗ main = fmap (runApp entryPoint) initAppEnv
The important characteristic about the above module structure is that GetPerson
doesn't depend on Persist, App
, or MyBackend
. This extends to every submodule of AppLogic
: GetPerson
completely abstracts those dependencies. The only place where we couple our application to Persist is in the GetPerson
instance for App
.
9/n
Also when we take this approach, you probably don't need classes as granular as GetPerson
. You probably want classes like DatabaseRead
, DatabaseWrite
, LocationService
, FileSystemRead
, FileSystemWrite
, Log
, and similar things. The point of writing such bespoke classes is twofold: (1) hide the details of third-party libraries (e.g. selectFirst
) and runtime dependencies (e.g. MyBackend
); and (2) get an idea of what each function might be doing (and not doing) from its signature.
Along those lines, you should never need to write a function with a MonadIO
constraint: if you need to do some IO
, write a bespoke class for it in Classes
and write an instance of that class for App
in AppWiring
. Try not to make your classes general-purpose things like Fetch
. Make them represent particular services that your application needs, like LocationService
. For example a class like
class Fetch m where
fetch :: Url -> m Response
doesn't hide the details of a service from application developers. Neither does it guarantee that the correct URL is used or that the response is parsed correctly. Instead, using a bespoke class like
class LocationService where
findLocation :: Coordinates -> m Location
means you only need to construct the URL
and parse the Response
in one place, in the LocationService
instance for App
.
Remember that the point of these bespoke classes isn't for them to be reused in other projects: the point is to abstract the dependencies of this particular application only and to segregate I/O action by class constraints to enforce principle of least privilege.
10/n
There are a few drawbacks with the above approach. One is that it threads Persistent, a third-party library, throughout your application logic. If, later, you decide to swap out Persistent for something else, you'll have a lot of code to change. Another drawback is that you will tend to write a lot of functions that return an
App a
.App
can do arbitraryIO
, since it has aMonadIO
instance. If we'd like to either decouple our application from third-party libraries or enforce principle of least privilege, we can add another layer.Your
initApp
andmain
stay exactly the same. YourentryPoint
(and all the functions it calls, all the functions that comprise your application logic) never mentionApp
,IO
, orMyBackend
directly: they express all their needs in terms ofGetPerson
and a host of similar bespoke classes you wrote specifically for this application.8/n