Polysemy: Design heuristics: Split by responsibility
After a while, it's tempting to have large effects, just because it's easier, or because they target one implementation.
For example, in the codebases I have worked, we had one effect to deal with Users because it was targetting AWS Cognito:
data UserEffect (m :: Type -> Type) (a :: Type) where
SignUp :: UserInfo -> Password -> UserEffect m (Either SignUpError ())
ConfirmSignUp :: ConfirmationInfo -> UserEffect m Bool
SignIn :: EmailAddress -> Password -> UserEffect m (Either SignInError ())
SignOut :: UserToken -> UserEffect m ()
AuthenticateUser :: UserToken -> UserEffect m (Either AuthenticateUserError AuthenticatedUser)
makeSem ''UserEffect
While we had three concerns:
- Registration:
SignUp
,ConfirmSignup
- Authentication management:
SignIn
,SignOut
- Authentication check:
AuthenticateUser
We can rewrite our effects as follows:
data UserRegistration (m :: Type -> Type) (a :: Type) where
SignUp :: UserInfo -> Password -> UserRegistration m (Either SignUpError ())
ConfirmSignUp :: ConfirmationInfo -> UserRegistration m Bool
makeSem ''UserRegistration
data UserAuthenticationManagement (m :: Type -> Type) (a :: Type) where
SignIn :: EmailAddress -> Password -> UserAuthenticationManagement m (Either SignInError ())
SignOut :: UserToken -> UserAuthenticationManagement m ()
makeSem ''UserAuthenticationManagement
data UserAuthenticationCheck (m :: Type -> Type) (a :: Type) where
AuthenticateUser :: UserToken -> UserAuthenticationCheck m (Either AuthenticateUserError AuthenticatedUser)
makeSem ''UserAuthenticationCheck
We can argue that UserAuthenticationManagement
and UserAuthenticationCheck
are closely related, but UserAuthenticationCheck
may be used in all your server's endpoints, while UserAuthenticationManagement
are usecases.
See the full the code here.