Observations
Below are some practices we observed in our codebase that we find useful. By the way, by error we mean business-type of problem. We assume exceptions are handled by some kind of wrapper, like Future.
import scala.concurrent.Future
import cats.instances.future._
import cats.Monad
import cats.data.OptionT
implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
// ec: concurrent.ExecutionContext = scala.concurrent.impl.ExecutionContextImpl@2a5eecb
implicit val M: Monad[Future] = implicitly[Monad[Future]]
// M: Monad[Future] = cats.instances.FutureInstances$$anon$1@53be230c
sealed trait Provider
final case class EmailAddress(value: String) extends AnyVal
final case class User(id: Long, email: EmailAddress, archived: Boolean)
final case class AuthMethod(provider: Provider) extends AnyVal
- Operation (method) results are represented as ADTs. Ex.:
sealed trait LoginResponse
object LoginResponse {
final case class LoggedIn(token: String) extends LoginResponse
case object AccountsMergeRequested extends LoginResponse
final case class AccountsMerged(token: String) extends LoginResponse
case object InvalidCredentials extends LoginResponse
case object Deleted extends LoginResponse
case object ProviderAuthFailed extends LoginResponse
}
- Methods (especially in services) are closed units of code, each returning one value out of result ADT for this particular method:
def login(email: String,
findUser: String => Future[Option[User]],
findAuthMethod: (Long, Provider) => Future[Option[AuthMethod]],
issueTokenFor: User => String,
checkAuthMethodAction: AuthMethod => Boolean,
authMethodFromUserIdF: Long => AuthMethod,
mergeAccountsAction: (AuthMethod, User) => Future[LoginResponse]): Future[LoginResponse] = ???
- There's no distinguished error type
We didn't find it useful too often. Also when logging in, if a user is deleted is it "error" or maybe "legit" return value? There's no reason to think about it.
- Error handling should be method-local
Enforcing global or even module-based error handling could be harmful to application architecture - errors are not born equal.
For-comprehensions are nice, programmers like them
Computations create tree-like structures
If-else = branching.