Slick for-comprehensions:如何处理可以是 None 或 Some-thing 的枚举器?

Slick for-comprehensions: How to handle an enumerator that can be None or Some-thing?

我有一个用例可能不特定于 Slick,而是特定于理解。高级模式是这样的,但它会导致 enum3 中的编译器错误:

(for {
   enum1 <- // ...
   enum2 <- // ...
   enum3 <- optionArg.fold(empty result).map { ... }
 } yield ())

然后我最终做了以下编译但是有代码重复,即 enum1 和 enum2:

optionArg match { 
  case (Some(arg)) => {
     (for {
        enum1 <- // ...
        enum2 <- // ...
        enum3 <- // do something with arg          
      } yield ())
    }
    case None => {
     (for {
        enum1 <- // ...
        enum2 <- // ...
      } yield ())
    }
  }        
}  

具体来说,我已经编译了这个重复的变体:

/**
 * Returns the inserted `oauth2Info` instance including the params. We first
 * look up the `LoginInfo` by the relevant search criteria, fetching its `userId`
 * which is then used to persist a `OAuth2Info` and multiple `OAuth2InfoParam`.
 *
 * @param extLoginInfo The login info for which the auth info should be added.
 * @param extOAuth2Info The TOTP info to add containing the params.
 * @return the inserted `oauth2Info` instance including the params.
 */
def add(extLoginInfo: ExtLoginInfo, extOAuth2Info: ExtOAuth2Info): Future[ExtOAuth2Info] = {
    val insertion = extOAuth2Info.params match {
      case Some(params) => {
        (for {
          userId <- LoginInfo.filter { loginInfo => loginInfo.providerId === extLoginInfo.providerID && loginInfo.providerKey === extLoginInfo.providerKey }.map(_.userId).result.head
          _ <- (OAuth2Info += OAuth2InfoRow(userId, extOAuth2Info.accessToken, extOAuth2Info.tokenType, extOAuth2Info.expiresIn, extOAuth2Info.refreshToken))
          _ <- DBIOAction.sequence(params.map { param => (OAuth2InfoParam += OAuth2InfoParamRow(userId, param._1, param._2)) })
        } yield ())
      }
      case None => {
        (for {
          userId <- LoginInfo.filter { loginInfo => loginInfo.providerId === extLoginInfo.providerID && loginInfo.providerKey === extLoginInfo.providerKey }.map(_.userId).result.head
          _ <- (OAuth2Info += OAuth2InfoRow(userId, extOAuth2Info.accessToken, extOAuth2Info.tokenType, extOAuth2Info.expiresIn, extOAuth2Info.refreshToken))
        } yield ())
      }
    }
    db.run(insertion.transactionally).map(_ => extOAuth2Info)
}

而这个简洁的期望变体没有:

def add(extLoginInfo: ExtLoginInfo, extOAuth2Info: ExtOAuth2Info): Future[ExtOAuth2Info] = {
    val insertion = (for {
      userId <- LoginInfo.filter { loginInfo => loginInfo.providerId === extLoginInfo.providerID && loginInfo.providerKey === extLoginInfo.providerKey }.map(_.userId).result.head
      _ <- (OAuth2Info += OAuth2InfoRow(userId, extOAuth2Info.accessToken, extOAuth2Info.tokenType, extOAuth2Info.expiresIn, extOAuth2Info.refreshToken))
      _ <- extOAuth2Info.params.fold(DBIOAction.seq()) { params =>
        DBIOAction.sequence(params.map { param => (OAuth2InfoParam += OAuth2InfoParamRow(userId, param._1, param._2)) })
      }
    } yield ()).transactionally
    db.run(insertion).map(_ => extOAuth2Info)
}

给出编译错误:

[play-silhouette-seed] $ compile
[info] Formatting 1 Scala source ProjectRef(uri("file:/home/skywalker/code/play-silhouette-seed/"), "root")(compile) ...
[info] Compiling 1 Scala source to /home/skywalker/code/play-silhouette-seed/target/scala-2.12/classes ...
[error] /home/skywalker/code/play-silhouette-seed/app/models/daos/OAuth2InfoDaoImpl.scala:58:28: type mismatch;
[error]  found   : slick.dbio.DBIOAction[scala.collection.immutable.Iterable[Int],slick.dbio.NoStream,slick.dbio.Effect.Write]
[error]  required: slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect]
[error]         DBIOAction.sequence(params.map { param => (OAuth2InfoParam += OAuth2InfoParamRow(userId, param._1, param._2)) })
[error]                            ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed
[error] Total time: 1 s, completed May 28, 2019 10:42:00 AM

这是 fold 的一个问题,因为编译器无法 type-infer 表达式的正确泛型类型。它没有正确地将第一个表达式返回的类型扩展为第二个表达式的类型。您需要稍微帮助一下编译器。

这种情况也会发生

 Some(123).fold(None)(_ => Some("123"))
 // type mismatch; found: Some[String] required: None.type

您可以将 fold 更改为 map().getOrElse()(然后 "good type" 排在第一位并被推断)。

或者你可以在某处添加类型注释,比如

 private val noAction: slick.dbio.DBIOAction[Iterable[Int],NoStream,Effect.Write] 
   = DBIOAction.sequence()

 theOption.fold(noAction)(params => .... )

此外,如果您发现自己在重复代码,可以通过移出公共表达式来缓解这种情况。对于像 Slick 这样的函数式 (side-effect-free) 代码,这种重构是相当安全的。您可以只构建几个 valdef 以及您需要 运行 的 Slick 操作,然后再组合它们。

def add(extLoginInfo: ExtLoginInfo, extOAuth2Info: ExtOAuth2Info): Future[ExtOAuth2Info] = {
    val getUserId = LoginInfo.filter { loginInfo => loginInfo.providerId === extLoginInfo.providerID && loginInfo.providerKey === extLoginInfo.providerKey }.map(_.userId).result.head
    val insertOAuth = for {
        userId <- getUserId 
        _ = (OAuth2Info += OAuth2InfoRow(userId, extOAuth2Info.accessToken, extOAuth2Info.tokenType, extOAuth2Info.expiresIn, extOAuth2Info.refreshToken))
    } yield userId


    val insertion = extOAuth2Info.params match {
      case Some(params) => {
        (for {
          userId <- insertOAuth
          _ <- DBIOAction.sequence(params.map { param => (OAuth2InfoParam += OAuth2InfoParamRow(userId, param._1, param._2)) })
        } yield ())
      }
      case None => 
         insertOAuth
    }
    db.run(insertion.transactionally).map(_ => extOAuth2Info)
}