使用Scalaz或Shapeless将选项元组转换为元组选项

19

Having

(Some(1), Some(2))

I expect to get

Some((1, 2))

并且拥有
(Some(1), None)

I expect to get

None

为什么不直接使用 val (aOpt, bOpt) = optPair; aOpt.flatMap(a => bOpt.map(b => (a, b))) - Erik Kaplun
@ErikAllik 这个问题是关于 Scalaz 的。 - Nikita Volkov
5个回答

34

我知道你在询问Scalaz,但值得指出的是标准方法并不过于冗长:

val x = (Some(1), Some(2))

for (a <- x._1; b <-x._2) yield (a,b)

在一般情况下(例如任意元组的情况),Shapeless 是最擅长这种事情的工具。


当然。我觉得这可能是唯一的解决方案,因为我无法想象他们如何在Scalaz中实现它,因为我看不到为元组项指定统一类型的方法,这似乎对于那个问题是必要的。只是出于好奇而已。 - Nikita Volkov
@NikitaVolkov - 我几乎可以确定你可以让Shapeless做到这一点,但我实际上没有使用过它,所以我无法提供示例。 - Rex Kerr
@NikitaVolkov:你说得对,你不能为元组提供一个通用的Traverse实例,但是Bitraverse可以让你在其中获取两种类型。 - Travis Brown

16

您可以利用 Scalaz 7 提供的元组 Bitraverse 实例,然后像平常一样使用 bisequence (而不是 sequence) 进行序列化:

scala> import scalaz._, std.option._, std.tuple._, syntax.bitraverse._
import scalaz._
import std.option._
import std.tuple._
import syntax.bitraverse._

scala> val p: (Option[Int], Option[String]) = (Some(1), Some("a"))
p: (Option[Int], Option[String]) = (Some(1),Some(a))

scala> p.bisequence[Option, Int, String]
res0: Option[(Int, String)] = Some((1,a))

很遗憾,目前Scalaz 7需要在此处加上类型注释。


根据Yo Eight在评论中的说法,类型注释仍然是必需的。我不确定他或她的理由是什么,但实际上很容易编写自己的包装器,该包装器将为任何适当类型的元组提供bisequence方法,并且不需要类型注释:

import scalaz._, std.option._, std.tuple._    

class BisequenceWrapper[F[_, _]: Bitraverse, G[_]: Applicative, A, B](
  v: F[G[A], G[B]]
) {
  def bisequence = implicitly[Bitraverse[F]].bisequence(v)
}

implicit def bisequenceWrap[F[_, _]: Bitraverse, G[_]: Applicative, A, B](
  v: F[G[A], G[B]]
) = new BisequenceWrapper(v)

现在(some(1), some("a")).bisequence将能够正常编译。

我想不出为什么Scalaz不会包含这样的东西。无论是否想要在此期间添加它都是品味问题,但在让编译器处理类型方面肯定没有理论障碍。


不幸的是,在这种情况下,类型注释仍然是必需的。 - Yo Eight
@YoEight:你为什么这么说?看看我的编辑,有个可以运行的例子。 - Travis Brown
@Travis Brown。你只是推迟了问题。你的包装器为你保存了类型信息。这正是你在调用时要注释的信息。当我将MonadWriter推向Scalaz时,我也使用了同样的技巧。 - Yo Eight
@YoEight:我不太明白。它肯定能工作,也就是说隐式调用会生效,包装器会被构建,方法会被调用,而且所有这些都不需要类型注释。包装器所做的仅仅是汇集我们拥有正确的BitraverseApplicative实例的证据。 - Travis Brown
@Travis Brown。我并不是说这个方法不可用;像我之前和MonadWriter一样使用了同样的技巧。在隐式解析后,你的代码如下:bisequenceWrapTuple2, Option, Int, String, some("a"))).bisequence。我的观点是:不能因为你看不到它,就认为它不存在。此外,在这种情况下,你的封装将无法起作用:val v: (String / Int, String / String) = (/-(1), /-("a")); v.bisequence - Yo Eight

7

我认为这里不会重复使用Cats的版本。

@ import cats.implicits._
import cats.implicits._

@ (4.some, 2.some).bisequence
res1: Option[(Int, Int)] = Some((4, 2))

@ (4.some, none).bisequence
res2: Option[Tuple2[Int, Nothing]] = None

6
  • Starting Scala 2.13, this exact behavior is provided in the standard library by Option#zip:

    Some(2) zip Some('b') // Some((2, 'b'))
    Some(2) zip None      // None
    None zip Some('b')    // None
    None zip None         // None
    
  • Before Scala 2.13, Option#zip was returning an Iterable and it was possible to combine it with headOption:

    Some(2) zip Some('b') headOption // Some((2, 'b'))
    Some(2) zip None headOption      // None
    

这正是问题所要求的:只有在两个部分都被定义的情况下才能获得某物。 - undefined
好的,我误会了。我觉得楼主是在问Tuple2<Option<K>, Option<V>> -> Option<Tuple2<K,V>>,而zip提供的是(多多少少)Option<T1> -> Option<T2> -> Option<Tuple2<T1, T2>> - undefined
1
对你原始评论中opposite的使用感到困惑。初始Tuple2<Option<K>, Option<V>>的每个部分都可以按顺序访问,以达到此答案的输入状态(tuple._1 zip tuple._2)。决定不包含那部分,因为提取元组的部分是相当明显的,有趣的部分是zip的部分。 - undefined
相当明显...至少对我来说,一开始并不是这样的 :) - undefined

4
scala> import scalaz._
import scalaz._

scala> import Scalaz._
import Scalaz._

scala> (Tuple2.apply[Int, Int] _).lift[Option].tupled
res5: (Option[Int], Option[Int]) => Option[(Int, Int)] = <function1>

scala> res5((some(3), some(11)))
res6: Option[(Int, Int)] = Some((3,11))

scala> res5((some(3), none))
res7: Option[(Int, Int)] = None

这很好,但我认为使用元组是双向遍历的事实会更好一些。 - Travis Brown
就此而言,这看起来很丑陋,因为Scala在许多情况下无法推断类型。在Haskell中,res5 = uncurry $ liftA2 (,) - missingfaktor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接