我如何以惯用的方式flatMap Try [Option]

时间:2017-12-13 14:38:18

标签: scala

我想使用一些使用Option中的值创建另一个Try的函数来平滑一个Try [Option [A]],我希望解决方案简单而惯用。我用一个例子来说明问题。目标是创建一个Option [Group],其成员和事件包含在一个Try中,可以包含来自三个函数中的任何一个的错误。

def getGroup(id: Long): Try[Option[Group]]

def getMembersForGroup(groupId: Long): Try[Seq[Member]]

def getMeetingsForGroup(groupId: Long): Try[Seq[Meeting]]

我发现从getGroup返回的Try到成员会议功能和会议功能的tryMap很难进行flatMap,因为在"中有一个选项"。这是我到目前为止所提出的:

getGroup(id).flatMap(
  groupOpt => groupOpt.map(
    group => addStuff(group).map(group => Some(group))
  ).getOrElse(Success(None))
)

def addStuff(g: Group): Try[Group] =
  for {
    members <- getMembersForGroup(g.id)
    meetings <- getMeetingsForGroup(g.id)
  } yield g.copy(members = members, meetings = meetings)

我不喜欢我的解决方案是我必须将addStuff返回的组包装在一个选项中以执行getOrElse。此时类型为Option [Try [Option [Group]]],我认为乍一看难以理解。

这个问题是否有更简单的解决方案?

4 个答案:

答案 0 :(得分:3)

Cats的OptionT类型可能会简化此类:documentation heresource here

你的例子是:

def getGroupWithStuff(id: Long): OptionT[Try, Group] = {
  for {
    g <- OptionT(getGroup(id))
    members <- OptionT.liftF(getMembersForGroup(g.id))
    meetings <- OptionT.liftF(getMeetingsForGroup(g.id))
  } yield g.copy(members = members, meetings = meetings)
}

答案 1 :(得分:1)

您可以使用.fold代替.map.getOrElse ...这会让它更好一些:

 getGroup(id)
   .flatMap { 
     _.fold(Try(Option.empty[Group])){ 
       addStuff(_).map(Option.apply) 
     }
   }

或者明确地写下两个案例 - 在这种情况下看起来可能会更清楚一点,因为你可以避免拼出丑陋的类型签名:

  getGroup(id).flatMap {
    case None => Success(None)
    case Some(group) => addStuff(group).map(Option.apply)
  }

答案 2 :(得分:0)

您可能可以将getGroup调用简化为:

public WorkItem CreateWorkItem(string title, string description, string comments)
{
    HttpClient client = new HttpClient();
    var task = client.PostAsync("http://localhost:57765/API.svc/CreateWorkItem?title="+title+"&description="+description+"&history="+comments, null);
task.Wait();
        var contents = task.Result.Content;
    }

然而,这将以忽略来自addStuff调用的潜在故障信息为代价。如果不可接受,那么您不太可能进一步简化代码。

答案 3 :(得分:0)

试试这个。您可以从三个调用中的任何一个(以先发生者为准)中保留您的理解语法和失败信息。

def getFullGroup(id: Long): Try[Option[Group]] =
  getGroup(id).flatMap[Option[Group]] { _.map[Try[Group]]{ group =>
    for {
      meetings <- getMeetingsForGroup(id)
      members  <- getMembersForGroup
    } yield group.copy(meetings = meetings, members = members)
  }.fold[Try[Option[Group]]](Success(None))(_.map(Some(_)))
}

注意最后的杂技类型:

fold[Try[Option[Group]]](Success(None))(_.map(Some(_)))

没有类型注释和IDE,很难做到正确。在这种特殊情况下,这并不算太糟糕,但想象会议和成员依赖于另一个嵌套的try选项,而后者依赖于原始选项。或者想象一下,如果你想要理解个别的会议和团体,而不是使用整个列表。

您可以尝试使用cat或scalaz中的OptionT monad变换器将Try[Option[Group]]堆叠为非嵌套OptionT[Try, Group]。如果使用monad变换器,它可能如下所示:

def getFullGroup(id: Long): OptionT[Try, Group] =
  OptionT(getGroup(id)).flatMapF { group =>
    for {
      meetings <- getMeetingsForGroup(id)
      members  <- getMembersForGroup(id)
    } yield group.copy(meetings = meetings, members = members)
  }
}

对于这种特殊情况,并没有太大的收益。但是,如果你有很多这样的代码,请调查它。

顺便说一下,翻转Try和Option的第一个示例末尾的样板文件称为sequence。当它跟随地图时,整个事物被称为traverse。它是一种经常出现的模式,被函数式编程库抽象出来。您可以执行以下操作,而不是使用OptionT:

def getFullGroup(id: Long): Try[Option[Group]] =
  getGroup(id).flatMap[Option[Group]] { _.traverse { group =>
    for {
      meetings <- getMeetingsForGroup(id)
      members  <- getMembersForGroup
    } yield group.copy(meetings = meetings, members = members)
  }
}

(通常情况下,如果您正在映射f然后翻转monad,则需要使用f进行遍历。)