在Scala HList上可以执行Map操作吗?

30

我现在已经完成了几个HList的实现。其中一个是基于Daniel Spiewak的Scala之地中的高级巫术演讲,另一个是基于Apocalisp博客中的一篇文章。目标是拥有一个异构列表,其主要类型不是异构的,而是更高级的种类。例如:

val requests = Request[String] :: Request[Int] :: HNil

我可以对列表进行映射,执行请求并生成更高级别的异构列表。所以:

requests.map(execute)
应该相等。
String :: Int :: HNil

可悲的是,我所有的尝试都导致了一个包含 Any 类型的 HList。以下是最近一次尝试的代码:

class Request[+Out](o:Out) {
  type O = Out

  def v:O = o
}

object HList {
  trait Func[-Elem,Out] {
    type Apply[E <: Elem] <: Out
    def apply[N <: Elem](e:N):Apply[N]
  }
  sealed trait HList[Base] {
    type Head <: Base
    type Tail <: HList[Base]
    type Map[Out,F <: Func[Base,Out]] <: HList[Out]
    def head:Head
    def tail:Tail

    def ::[A <: Base](a:A):HList[Base]
    def map[Out,F <: Func[Base,Out]](f:F):Map[Out,F]
  }

  case class HNil[Base]() extends HList[Base] {
    type Head = Nothing
    type Tail = Nothing
    type Map[Out,F <: Func[Base,Out]] = HNil[Out]

    def head = error("Head of an empty HList")
    def tail = error("Head of an empty HList")

    def ::[A <: Base](a:A) = HCons(a,this)
    def map[Out,F <: Func[Base,Out]](f:F) = new HNil[Out]
  }

  case class HCons[Base,A <: Base,B <: HList[Base]](head: A, tail: B) extends HList[Base] {
    type Head = A
    type Tail = B    
    type Map[Out,F <: Func[Base,Out]] = HCons[Out,F#Apply[Head],Tail#Map[Out,F]]

    def ::[C <: Base](c:C) = HCons(c,this)
    def map[Out,F <: Func[Base,Out]](f:F) =
      HCons(f(head),tail.map(f))
  }

  val :: = HCons 
}

object Test extends Application {
  import HList._

  val HNil = new HNil[Request[_]]

  val list = new Request[Int](1) :: new Request[String]("1") :: HNil

  val (a :: b :: HNil) = list
  val y:Request[String] = b

  val results = list.map[Any,Unwrap.type](Unwrap)

  val i:Int = results.head
}

import HList._
object Unwrap extends Func[Request[Any],Any] {
  type Apply[I <: Request[Any]] = I#O
  def apply[N <: Request[Any]](e:N) = null.asInstanceOf[Apply[N]]
}

另一种尝试是基于 Apocalisp 版本,使用 fold 创建新的 HList,但结果仍然是一个包含 Any 类型的 HList。如有建议,请指教。

3个回答

23

shapeless中的HList实现非常丰富,足以包含HListKList功能。它提供了一个map操作,可以对其元素应用高阶函数(可能具有类型特定情况),从而产生一个适当类型的HList结果。

import shapeless.Poly._
import shapeless.HList._

// Define a higher-ranked function from Sets to Options
object choose extends (Set ~> Option) {
  def default[T](s : Set[T]) = s.headOption 
}

// An HList of Sets
val sets = Set(1) :: Set("foo") :: HNil

// Map our choose function across it ...
val opts = sets map choose

// The resulting value
opts == Option(1) :: Option("foo") :: HNil 

请注意,虽然在上面的示例中,HList元素没有共享公共外部类型构造函数的要求,但必须满足高阶函数映射所涉及的所有类型都有对应的情况。
// size is a higher-ranked function from values of arbitrary type to a 'size'
// which is defined as 1 by default but which has type specific cases for
// Strings and tuples
object size extends (Id ~> Const[Int]#λ) {
  def default[T](t : T) = 1
}
implicit def sizeString = size.λ[String](s => s.length)
implicit def sizeTuple[T, U](implicit st : size.λ[T], su : size.λ[U]) =
  size.λ[(T, U)](t => 1+size(t._1)+size(t._2))

size(23) == 1          // Default
size("foo") == 3       // Type specific case for Strings
size((23, "foo")) == 5 // Type specific case for tuples

现在让我们将其映射到一个HList中,
val l = 23 :: true :: "foo" :: ("bar", "wibble") :: HNil
val ls = l map size

ls == 1 :: 1 :: 3 :: 10 :: HNil

在这种情况下,被映射的函数的结果类型是恒定的:无论参数类型如何,它都是一个Int。因此,生成的HList具有相同类型的元素,这意味着它可以有用地转换为普通列表。
ls.toList == List(1, 1, 3, 10)

2
Shapeless 2.0中的size定义是否有效?我收到了一个错误消息:“无法创建对象,因为trait〜>中的方法apply类型[T](f:shapeless.Id [T])Int未定义”。 - James Moore
3
如有人想知道,@JamesMoore报告的错误可以通过将default替换为apply来解决。 - Gabriele Petronella
2
有人知道这个的当前语法是什么吗? - Ryan Stull
看起来当前的语法需要为每种可以应用的类型定义一个案例?请参阅https://books.underscore.io/shapeless-guide/shapeless-guide.html#poly-syntax - Shon
实际上,我通过import poly._成功获取了更高级函数的当前语法。我认为~>运算符可能已经移动到poly包中了? - Shon

3
你需要一个类型构造函数为Request的Klist,以及一个自然变换execute: Request ~> Id。这些都在Apocalisp的奇妙类型级编程系列文章中详细介绍,特别是:
  1. 自然变换字面量
  2. Klist基础知识
你可以从Mark Harrah的up repo检出整个系列的代码。
在你的情况下,你需要像这样的东西:
val reqList = new Request[Int](1) :^: new Request[String]("1") :^: KNil    
val exec = new (Request ~> Id) { def apply[T](reqs: Request[T]): T = reqs.execute }    
val results = reqList down exec

上面的down方法在概念上与M ~> Id的自然变换的map相同;您还可以使用更通用的map,从M到N的nat transf和一个kind M的Klist生成一个kind N的Klist。


我注意到XSBT中有KLists,但还没有时间去研究。谢谢你的技巧。这个问题让我发疯了。 - Jesse Eichar

0
请注意,您可以在最近的文章(2016年10月,距离发帖人五年之后)“使用shapeless的HLists增强Akka Streams中的类型安全”中看到Map与HList的示例,该文章来自Mikołaj Koziarkiewicz
  //glue for the ParserStageDefs
  specs.map(s => Flow[Data].map(s.parser).map(s.processor))
                    .foreach(broadcast ~> _ ~> merge)

问题在于我们规范列表中的类型信息没有被保留。或者说,没有按照我们想要的方式进行保留 - List 元素的类型是 ParserStageDef[_ >: Int with String],因此是我们的装饰器和增量器的最低公共超类型。
上述意味着,在解析器和处理器元素之间进行映射时,编译器无法提供在给定规范中使用的实际类型 T。
解决方案
这就是 HLists 能够帮助我们的地方。因为它们保留了每个元素的完整类型信息,所以可以定义我们的流程与上一次尝试非常相似。
首先,让我们用 HList 替换我们的列表:
import shapeless.ops.hlist._
import shapeless._
//...

val specs = decorator :: incrementer :: HNil
val specsSize = specs.length.toInt

现在,对于从ParserStageDefsFlows的映射,我们需要采用不同的方法,因为HListmap需要一些称为P**oly-多态函数值**的东西。以下是我们这种情况下的一个示例:
import shapeless.PolyDefns.~>
object toFlow extends (ParserStageDef ~> ProcessingFlow) {
  override def apply[T](f: ParserStageDef[T]) = 
                Flow[Data].map(f.parser).map(f.processor)
}

为了使其正常工作,我们还需要将ProcessingFlow更改为类型ProcessingFlow[_] = Flow[Data, Data, _],因为上面的多态函数期望一个高阶类型。现在,我们的核心语句是:
//we convert to a List[ProcessingFlow[_]] for simplicity
specs.map(toFlow).toList.foreach(broadcast ~> _ ~> merge)

而且我们已经准备好了!

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