资讯动态

Cats 逆变(Contravariant)Type Class 完全指南:contramap、Show、Ordering 与子类型提升

发布时间:2026/10/22 17:53:19 来源:尧图企业网站定制
后端【免费下载链接】catsLightweight, modular, and extensible library for functional programming.项目地址https://gitcode.com/gh_mirrors/ca/cats点击查看免费下载Contravariant是 Cats 中与协变Functor对偶的核心类型类它抽象出那些消费类型而非生产类型的计算上下文。本文围绕 docs/typeclasses/contravariant.md 展开以Show、scala.math.Ordering与子类型三个实战场景为主线结合Contravariant的源码实现与定律测试帮助读者彻底理解contramap的作用机制并掌握如何通过已有实例零成本地派生出新类型类实例。Contravariant 是什么在 Cats 中Contravariant[F[_]]是专门为逆变函子设计的类型类。它的核心操作是contramap其类型签名如下def contramapA, B(f: B A): F[B]对比协变 Functor 的mapdef mapA, B(f: A B): F[B]可以发现两者结构完全一致只是传给函数的变换方向被反转了Functor.map给你一个A B把F[A]变成F[B]——生产方向Contravariant.contramap给你一个B A把F[A]变成F[B]——消费方向。直觉上可以这样理解如果手头有一个关于类型A的上下文F[A]并且能够从B中取出一个A值即存在B A那么Contravariant允许你得到一个关于类型B的上下文F[B]。F在这里扮演的角色是消费者/接收者它接收类型A的值所以只要你能把B喂给它它就能处理B。Cats 中典型的Contravariant实例包括Show把值渲染成字符串以及scala.math.Ordering和cats.kernel.Order比较两个值的大小——它们都不产出原类型而是接收原类型的值并生成某种外部结果。源码中的定义从源码看Contravariant定义在 core/src/main/scala/cats/Contravariant.scalatrait Contravariant[F[_]] extends Invariant[F] { self def contramapA, B(f: B A): F[B] override def imapA, B(f: A B)(fi: B A): F[B] contramap(fa)(fi) ... }值得注意的是Contravariant继承自Invariant即不变函子见 Invariant并且通过contramap(fa)(fi)自动实现了imap不变函子需要同时具备A B和B A两个方向的变换能力而逆变函子天然就拥有后者所以只需忽略正向变换即可满足Invariant的接口。此外Contravariant还提供了一系列衍生操作narrowA, B : A: F[B]把F[A]收窄为F[B]B是A的子类型把子类型关系反向提升到F上liftContravariantA, B: F[B] F[A]把普通函数提升为F之间的函数compose[G[_]: Contravariant]与另一个逆变函子复合结果是一个协变FunctorcomposeFunctor[G[_]: Functor]与协变函子复合结果仍是Contravariant。这些复合能力在 core/src/main/scala/cats/Composed.scala 中有完整的底层实现ComposedContravariant、ComposedContravariantCovariant等。Contravariant 实例ShowShow是理解Contravariant最直观的例子。从 core/src/main/scala/cats/Show.scala 可以看到它的Contravariant实例实现implicit val catsContravariantForShow: Contravariant[Show] new Contravariant[Show] { def contramapA, B(f: B A): Show[B] b fa.show(f(b)) }逻辑非常简单要得到一个Show[B]只需先把B通过f转换成A再交给已有的Show[A]去渲染。Show本身是面向类型参数的逆变换的绝佳例子——Show[T]只负责把T消费成字符串从不产出一个T。实战为 Salary 派生出 Show 实例假设我们有一个带Show实例的Money类以及一个包装它的Salary类import cats._ import cats.syntax.all._ case class Money(amount: Int) case class Salary(size: Money) implicit val showMoney: Show[Money] Show.show(m s$$${m.amount})如果我们希望Salary也能被展示最直接的想法是把它转换成一个Money实例然后复用已有的Show[Money]——这正是Contravariant要解决的场景。利用Show的Contravariant实例implicit val showSalary: Show[Salary] showMoney.contramap(_.size) Salary(Money(1000)).show // res0: String $1000showMoney.contramap(_.size)的语义是构造一个Show[Salary]当它被要求渲染某个Salary时先用_.size取出内部的Money再用既有的showMoney完成字符串化。整个过程没有为Salary编写任何新的渲染逻辑。这里的.contramap语法来自 core/src/main/scala/cats/syntax/contravariant.scala它通过Contravariant.ToContravariantOps为任何具有Contravariant实例的F[A]提供语法糖本文所有示例统一使用import cats.syntax.all._引入。Contravariant 实例scala.math.OrderingShow的例子较为浅显下面看看Contravariant在排序场景中更实用的价值。scala.math.Ordering类型类定义了比较操作例如compareOrdering.Int.compare(2, 1) // res1: Int 1 Ordering.Int.compare(1, 2) // res2: Int -1Ordering同样提供了基于已有实例派生出新实例的方法bydef byT, S(implicit ord: Ordering[S]): Ordering[T]细看这个签名会发现它就是contramap的另一种表述传入T S把T转换为S得到Ordering[S] Ordering[T]——方向完全反转正是逆变的体现。借助它我们可以免费获得Ordering[Money]// we need this for to work import scala.math.Ordered._ implicit val moneyOrdering: Ordering[Money] Ordering.by(_.amount) Money(100) Money(200) // res3: Boolean trueOrdering.by(_.amount)创建了一个按金额比较的Ordering[Money]后续所有需要Ordering[Money]的地方例如、排序、sorted等都会自动使用它。源码对照cats.kernel.Order 的 Contravariant 实例Cats 在cats.kernel.Order以及Hash上也提供了同类实例。在 core/src/main/scala/cats/instances/order.scala 中Order的逆变能力通过ContravariantMonoidal暴露implicit val catsContravariantMonoidalForOrder: ContravariantMonoidal[Order] new ContravariantMonoidal[Order] { ... def contramapA, B(f: B A): Order[B] Order.by(f)(using fa) ... }这里Order.by(f)(using fa)与scala.math.Ordering.by异曲同工同样是给定B A与Order[A]派生Order[B]。注释中还特别强调了一个重要约束只有当使用的函数是单射injective一一对应时派生出的实例才满足定律——例如Money到Int的_.amount映射就是单射的。同理core/src/main/scala/cats/instances/hash.scala 中的Contravariant[Hash]也是通过Hash.by(f)(using ha)实现的模式完全一致。除此之外core/src/main/scala/cats/instances/function.scala 还提供了Function1的逆变实例Contravariant[* R]的contramap直接用fa.compose(f)实现即先执行f再执行fa这与Show、Order的语义一脉相承。子类型与 narrow逆变函子与子类型之间存在着一种天然关系它与协变函子恰恰相反对偶class A class B extends A val b: B new B val a: A b val showA: Show[A] Show.show(a a!) val showB1: Show[B] showA.contramap(b b: A) val showB2: Show[B] showA.contramap(identity[A]) val showB3: Show[B] Contravariant[Show].narrowA, B如果F是合法的逆变函子且B : A那么F[A] : F[B]——子类型关系被逆变函子反向提升了。这很好理解Show[A]既然能展示A那么对所有A的子类型B自然也能展示所以Show[A]可以安全地当作Show[B]使用。上述第三种写法Contravariant[Show].narrowA, B使用了Contravariant.narrow它在 core/src/main/scala/cats/Contravariant.scala 中的实现是def narrowA, B : A: F[B] fa.asInstanceOf[F[B]]即直接通过类型转换完成。源码注释说明虽然也可以实现为contramap(identity)但根据函子定律两者是等价的直接转换可以省去一次函数包装。ContravariantSuite中的测试也验证了这一点tests/shared/src/test/scala/cats/tests/ContravariantSuite.scala 断言narrow与contramap(identity)结果相等且引用相同eq。定律与测试验证Contravariant必须满足两条定律定义在 laws/src/main/scala/cats/laws/ContravariantLaws.scaladef contravariantIdentityA: IsEq[F[A]] fa.contramap(identity[A]) - fa def contravariantCompositionA, B, C: IsEq[F[C]] fa.contramap(f).contramap(g) - fa.contramap(f.compose(g))Identity同一律fa.contramap(identity) fa用恒等函数做逆变换不应改变结果Composition复合律连续两次contramap先f后g等价于一次contramap(f.compose(g))即fa.contramap(f).contramap(g) fa.contramap(f andThen g)。这两条定律与协变 Functor 的定律完全对偶确保了contramap的行为可预测且可组合。它们通过 laws/src/main/scala/cats/laws/discipline/ContravariantTests.scala 以 ScalaCheck 属性测试property-based testing的形式自动执行并继承了InvariantTests的测试集。Cats 自身为多个类型检查过这些定律例如ConstContravariantTests[Const[String, *]].contravariant[Int, Int, Int]见 ConstSuiteCokleisliContravariantTests[Cokleisli[Option, *, MiniInt]].contravariant[...]见 CokleisliSuiteAndThenContravariantTests[AndThen[*, Int]].contravariant[MiniInt, Int, Boolean]见 AndThenSuite。在 tests/shared/src/test/scala/cats/tests/ContravariantSuite.scala 中还展示了一个自定义的PredicateA如何实现ContravariantMonoidalcontramap采用x fa.run(f(x))并与Const一同验证narrow行为——这是编写自定义逆变实例时可直接参照的模板。实践要点总结判断何时使用Contravariant当类型构造器F[A]只接收/消费类型A、从不产出类型A时如Show、Order、Hash、Predicate、A R它天然是逆变的应当提供Contravariant实例contramap是派生的利器给出B A即可从F[A]派生F[B]无需为每个新类型重写实例——Show、Ordering、Hash的by模式都是如此注意单射约束用Order.by/Ordering.by派生实例时映射函数应保持单射否则派生实例可能不满足定律善用narrow逆变函子会把子类型关系反向提升B : A时F[A] : F[B]通过Contravariant[F].narrow可以直接收窄实例遵守两条定律同一律与复合律保证contramap行为一致Cats 使用 ScalaCheck 对内置与用户自定义实例自动验证。赞分享后端【免费下载链接】catsLightweight, modular, and extensible library for functional programming.项目地址https://gitcode.com/gh_mirrors/ca/cats点击查看免费下载相关推荐fp-ts Contravariant 详解contramap 逆变函子接口族、实现实例与实战用法fp ts Contravariant 详解contramap 逆变函子接口族、实现实例与实战用法 导读 Contravariant 逆变函子是 fp t开发工具JAX 类型提升Type Promotion语义完全指南提升格、弱类型与严格模式JAX 类型提升Type Promotion语义完全指南提升格、弱类型与严格模式 本篇指南系统讲解 JAX 的 类型提升type promotion语人工智能机器学习深度学习编译器高性能计算Flow 类型型变Variance完全指南协变、逆变与不变Flow 类型型变Variance完全指南协变、逆变与不变 类型型变Variance是 Flow 类型系统中最重要也最容易被忽略的主题之一。它决定了泛开发工具静态分析代码质量创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑