解读最棘手的Scala方法原型之一(Slick)

8

阅读以下Scala Slick类中的<>方法,来自http://slick.typesafe.com/doc/2.1.0/api/index.html#scala.slick.lifted.ToShapedValue,它让我想起了关于Scala原型的那个具有标志性意义的Stackoverflow线程

def <>[R, U](f: (U) ⇒ R, g: (R) ⇒ Option[U])
(implicit arg0: ClassTag[R], shape: Shape[_ <: FlatShapeLevel, T, U, _]):
MappedProjection[R, U]

有没有熟练且精通的人能提供一个清晰明确的演示,仔细澄清所有类型协变/不变、双参数列表和其他高级Scala方面的那个冗长的原型定义?

这个练习也将极大地帮助处理类似复杂的原型!


3
你开始吧。有一些人特别关心那个问题,但并不多。所以试着解释一下,如果你卡住了,可能会有其他人帮忙。 - The Archetypal Paul
1个回答

20

好的,让我们来看一下:

class ToShapedValue[T](val value: T) extends AnyVal {
  ...
  @inline def <>[R: ClassTag, U](f: (U) ⇒ R, g: (R) ⇒ Option[U])(implicit shape: Shape[_ <: FlatShapeLevel, T, U, _]): MappedProjection[R, U]
}

这个类是一个包装器;虽然我并没有从快速浏览中看到implicit转换,但它看起来像是一种"pimp my library"模式。因此,我猜想这是为了将<><>作为"扩展方法"添加到某些(或者所有)类型中。
@inline是一个注释,一种在任何事物上放置元数据的方式;这是一种提示编译器应该进行内联处理的方法。<>是方法名 - 在scala中有很多像"运算符"的东西实际上只是普通的方法。
你链接的文档已经将R:ClassTag扩展成了普通的R和一个implicit ClassTag[R]——这是一种"上下文界定",它只是语法糖。ClassTag是一个由编译器生成的针对每个(具体)类型存在的东西,它有助于反射,因此,这是一种提示该方法可能会在某个时候对R进行一些反射处理的方法。
现在,进入正题:这是一个通用的方法,由两个类型参数化:[R,U]。它的参数是两个函数:f: U => Rg: R => Option[U]。这看起来有点像函数式Prism概念——从UR的转换总是有效的,而从RU的转换有时不起作用。
签名(有点)有趣的部分是末尾的implicit shapeShape被描述为“typeclass”,因此最好将其视为“约束条件”:它限制了我们可以使用哪些适当的Shape类型来调用此函数的可能类型UR

查看Shape的文档, 我们可以看到四种类型是 Level, Mixed, UnpackedPacked。因此,限制是:必须有一个Shape,其“level”是FlatShapeLevel的某个子类型,其中Mixed 类型为TUnpacked 类型为RPacked类型可以是任何类型)。

因此,这是一个类型级函数,表达了RT的“解包版本”的概念。再次使用Shape文档中的示例,如果T(Column[Int],Column[(Int,String)],(Int,Option[Double])),那么R将是(Int,(Int,String),(Int,Option[Double])(仅适用于FlatShapeLevel,但我会做出判断,认为这可能不重要)。有趣的是,U完全没有限制。

这让我们能够通过在两个方向上提供转换函数,从任何类型的T创建一个MappedProjection[未打包版本的T, U]。因此,在简单的情况下,T可能是Column[String] - 数据库中字符串列的表示形式 - 我们想将其表示为某些特定于应用程序的类型,例如EmailAddress。因此,R=String,U=EmailAddress,并且我们在两个方向上提供转换函数:f: EmailAddress => String和g: String => Option[EmailAddress]。这样做是有意义的:每个EmailAddress都可以表示为String(至少如果我们想将它们存储在数据库中的话),但并非每个String都是有效的EmailAddress。如果我们的数据库在电子邮件地址列中某处有例如“http://www.foo.com/”,那么我们的g将返回None,并且Slick可以优雅地处理这种情况。

MappedProjection本身很遗憾没有文档。但是我猜它是一种可以查询的东西的一种懒惰表示方式;我们有一个Column[String],现在我们有了一个伪列,其(基础)类型为EmailAddress。因此,这可能允许我们编写伪查询,例如“从用户中选择,其中emailAddress.domain =“gmail.com””,这在数据库中直接做是不可能的(数据库不知道电子邮件地址的哪个部分是域),但通过代码却很容易实现。至少,这是我对它可能做什么的最佳猜测。

可以说,如果使用标准的Prism类型(例如来自Monocle的类型)而不是显式地传递一对函数,那么该函数可能会更清晰。使用implicit提供类型级函数是笨拙但必要的;在完全依赖类型的语言(例如Idris)中,我们可以将我们的类型级函数编写为函数(类似于def unpackedType(t: Type): Type = ...)。因此,从概念上讲,这个函数看起来像:

def <>[U](p: Prism[U, unpackedType(T)]): MappedProjection[unpackedType(T), U]

希望这解释了一些阅读新的、不熟悉的函数的思考过程。我完全不了解Slick,所以我不知道这个<>的用途是否准确 - 我理解得对吗?

我只能说一句话。哇。 - matanster
顺便说一下,这是强大、表达力强、静态类型系统的优点之一:你只需查看类型就能知道方法的作用,无需查看文档,更不用说源代码了,甚至不需要知道方法的名称! - Jörg W Mittag
是的,这对我来说似乎很准确 :) 顺便问一下为什么存在Option返回类型。现在想知道在没有值的情况下slick是什么。 - Sebastien Lorber

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