A software engineer website

Polysemy: Design heuristics: Split by responsibility

Gautier DI FOLCO March 22, 2023 [Haskell] #haskell #polysemy #design #effects systems

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:

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.