逆变函子很奇怪

2020-09-17 12:12:07

在我们开始之前只需注意一下术语;我将使用“functor”来表示该概念的范畴含义:

逆变函子很奇怪,不是吗?协变函数器(由函数器类型类建模)非常简单,但逆变函数器,顾名思义,似乎完全相反。

在我们讨论什么是逆变函数式之前,看看我们所了解和喜爱的函数式类型类是很有用的。

类函数f,其中FMAP::(a->;b)->;f a->;f b。

我们通常将函数理解为某种类型的“容器”或“生产者”,其中提供给FMAP的函数被应用于某些类型构造函数1f中“包含”或“产生”的元素。

一个简单的例子是List([])类型,它可以表示零个或多个值。给定a[a],当给定函数a-&b时,我们可以将其转化为a[b]。

Data[]a=[]|a:[a]--[]数据类型实例函数器[]的近似值,其中fmap_[]=[]fmap f(x:xs)=f x:fmap f xs。

在下面的示例中,我们在给定函数Int->;String的情况下将[Int]转换为[String]:

导入数据。Semigroup((<;>;))myInts::[int]myInts=[1..。5]emptyInts::[int]emptyInts=[]intToString::int->;StringintToString n=(Show N)<;>;";!";myStrings::[String]myStrings=FMAP intToString myInts--[";1!";,";2!";,";3!";,";4!]myEmptyString::[]myEmptyString=FMAP intToString emptyInts--[]。

另一个示例是可能数据类型,它表示一个可能存在也可能不存在的值。

数据可能是a=Nothing|只是一个实例函数器,其中FMAP_Nothing=Nothing FMAP f(Just X)=Just(F X)。

在下面的示例中,我们在给定函数Int->;String的情况下将可能Int转换为可能字符串:

导入数据。Semigroup((<;>;))可能只有10个notInt::Maybe IntnotInt=Nothing intToString::int->;StringintToString n=(Show N)<;>;";!";!";可能字符串::可能StringmaybeString=FMAP intToString可能--只有";10!"。

从本质上说,如果不对函数值做任何操作,就会得到与开始时相同的函数值。

如果您转换函数器的结果,方法是先用函数g进行fmap,然后再用后续函数f进行fmap,这与合成函数g和f(F)是一样的。G)然后映射一次。

现在让我们来看一些稍微不同的东西。让我们创建一个数据类型来包装某种类型的谓词。谓词是指计算结果为布尔值的东西:

为谓词定义一个函数器实例可能很有用-比方说,如果我们有一个谓词Int,并且当我们有一个Int->;String函数时,我们想把它转换成一个谓词字符串。让我们尝试并实现这一点:

实例函数谓词WHERE--FMAP(a-&>b)->;谓词a-&>;谓词b FMAP f(谓词p)=谓词(\b-&>;未定义)FMAP f(谓词(a-&>;Bool))=谓词(\b-&>;未定义)--扩展p FMAP(a-&>b)(谓词(a-&>;Bool)。

我们如何合成(a->;bool)和(a->;b)来得到a(b->;bool)?

我们被赋予了b,但是我们不能访问任何实际使用b的函数。

问题是我们不能,这是因为类型变量a的“极性”,你的谓词没有函数式实例。

极性是使用类型变量的位置表示方差的一种方式。让我们以一个简单的函数a>;b为例。

如果类型变量位于输入位置(如a),则它被赋予负极性。如果它在像b这样的输出位置,则它被赋予正极性。

这意味着函数(实际上是协变函数)需要处于协变位置的类型构造函数,以便为该类型定义函数实例。

让我们来看一个已知具有如下函数器实例的类型:

我们可以看到,类型变量a出现在刚才构造函数的定义中的协变(或输出)位置。

我们可以看到类型变量a出现在逆变量(或输入)位置。这表明我们不能为此数据类型创建(协变)函数器实例。

类矛盾变量f,其中contmap::(a->;b)->;f b->;f a。

太时髦了!ContraVariant也接受某种类型的构造函数f,就像函数器一样,但是它有这个名称奇怪的Contracmap函数,而不是FMAP。

FMAP::(a->;b)->;f a->;f b--函子反映射::(a->;b)->;f b->;f a--逆变量^。

如果在某个上下文中有a,并且有一个函数将a转换为b,我可以给出一个包含b的上下文。

如果您有一个需要a的上下文和一个可以将bs转换为as的函数,我可以给您一个需要bs的上下文。

但这可能没有多大意义。所以让我们试着从我们的非函数式:谓词的角度来看这一点。谓词需要a,然后它使用a来判断关于a的内容是True还是False。

假设我们知道类型aIn谓词出现在输入位置,让我们尝试编写谓词的ContraVariant实例。

实例反变谓词WHERE--Contracmp(a->;b)->;f b->;f a Contracmap(a->;b)->;谓词b-->;谓词a--替换谓词约束映射aToB(谓词bToBool)=谓词(\a->;未定义)的‘f’

假设我们有一个函数a->;b,本质上是一个b->;bool类型的函数(包装在谓词b中),我们可以在给定a的情况下,使用aToB将其转换为b,然后将该b提供给bToBool,从而得到一个Bool。

实例反变谓词WHERE CONTROMAP::(a->;b)->;谓词b->;谓词a禁止映射aToB(谓词bToBool)=谓词$\a->;let b=aToB a bool=bToBool b in bool。

实例逆变谓词WHERE CONTROMAP::(a->;b)->;谓词b->;谓词a禁止映射f(谓词b)=谓词$b。F。

从谓词a的定义可以看出,我们所做的一切都是在谓词b内的函数之前运行所提供的函数f。这样做的原因是为了使新的输入类型与现有的输入类型相匹配,从而获得一些功能。

实例函数器可能FMAP_NOTIES=Nothing FMAP aToB(Just A)=Just(AToB A)

我们可以看到函数aToB是在我们的值为a之后运行的,我们这样做是为了将某种类型的结果转换为另一种类型。

既然我们已经了解了函数器和逆变量之间的本质区别,让我们来看看如何在谓词类中使用禁忌映射。

假设我们已经有一个谓词来确定一个数字是否大于十:

假设我们要编写另一个谓词来验证字符串的长度是否大于10个字符。

当然,那是相当做作的,但请耐心听我说。我们还假设我们有一个Person数据类型,我们想知道一个人的名字是否超过十个字符-如果是,我们认为这是一个长名字。

数据Person=Person{PersonName::String,Personage::int}Personal LongName::谓词PersonsonLongName=Predicate(\p-&>;(Length.。PersonName$p)>;10)。

GetPredicate numGreaterThanTen 5--FalsegetPredicate numGreaterThanTen 20--TruegetPredicate strLengthGreaterThanTen";hello";--FalsegetPredicate strLengthGreaterThanTen&34;hello world";--TruegetPredicate Person LongName$Person";John";30--FalsegetPredicate Person LongName$Person";Bar.。

这很好,但是每个谓词之间都有一些重复-即我们将数字与10进行比较的部分:

(\n->;n>;10)--Int(\s->;(长度s)>;10)--String(\p->;(长度。Person Name$p)>;10)--Person。

如果我们看一看numGreaterThanTen、strLengthGreaterThanTen和Personal LongName之间的区别,我们会发现唯一的区别是一个在Int上工作,而其他的分别在String和Person上工作。StrLengthGreaterThanTen和Personal LongName分别将其输入类型转换为Int,然后执行相同的比较:

谓词(\(n::int)->;let num=id n in num>;10--(1))--numGreaterThanTen谓词(\(s::String)->;let num=length s in num>;10--(1))--strLengthGreaterThanTen谓词(\(p::Person)->;let name=PersonName p Num=length name in num>。

上述函数的扩展表明,尽管谓词本身具有不同的输入类型,但最终它们都被转换为一个数字,并与数字10进行比较。在上面的示例中,这是用(1)标记的。

我们还可以看到,谓词之间的唯一变化是在运行比较函数(1)之前从一种类型转换为另一种类型。这是我们的线索,我们可以在这里使用禁忌映射来重用一些功能。

NumGreaterThanTen::Predicate IntnumGreaterThanTen=Predicate(\n->;n>;10)strLengthGreaterThanTen2::Predicate StringstrLengthGreaterThanTen2=contmap length numGreaterThanTen--将字符串转换为Int,然后将其传递给numGreaterThanTen PersonLongName2::Predicate PersonPersonLongName2=contmap(Length.。PersonName)numGreaterThanTen--将人员转换为Int,然后将其传递给numGreaterThanTen。

GetPredicate strLengthGreaterThanTen2";hello";--FalsegetPredicate strLengthGreaterThanTen2";hello world";--TruegetPredicate Person LongName 2$Person";John";30--FalsegetPredicate Person LongName2$Person";Bartholomew";30--True。

现在,我们已经用numGreaterThanTen重写了strLengthGreaterThanTen和PersLongName,只需在它之前运行一个函数来转换类型。这是一个简单的逆变函数器示例,如果我们可以通过某个映射函数将其他类型转换为该类型,则可以为给定类型重用一些现有功能。

PersLongName3::谓词PersonLongName3=contmap PersonName strLengthGreaterThanTen--将人员转换为字符串,然后将其传递给strLengthGreaterThanTen。

就像函子有规律一样,逆变也有规律。这太棒了-因为法律让我们的生活变得更容易。

本质上,如果您不更改逆变函数值,您将得到与开始时相同的逆变函数值。

如果您通过与函数g进行对比映射将输入转换为某个逆变函数,然后通过再次与函数f进行对比映射将其输入转换为其他类型,则这与合成函数f和g(G)是相同的。F)然后进行一次对比。注意合成的顺序是改变的,而不是我们看函子定律的时候。

让我们以谓词为例,试试恒等定律。谓词的逆变实例定义为:

实例逆变谓词WHERE CONTROMAP::(a->;b)->;f b->;f a Contramap f(谓词p)=谓词(p.。f)。

--身份法则映射id numGreaterThanTen==numGreaterThanTen--LHS谓词(p.。F)--应用反映射谓词(p。Id)--扩展f谓词(P)--应用f谓词(\n->;n>;10)--扩展p--rhsnumGreaterThanTen谓词(\n->;n>;10)--扩展数值大于10--equalitylhs==RHS谓词(\n->;n>;10)==谓词(\n->;n>;10)。

再次以谓词为例,我们来探索逆变式的构成规律。

NumGreaterThanTen::谓词IntnumGreaterThanTen=谓词(\n->;n>;10)长度::[a]->;Int Person Name::Person->;字符串。

--作文法则限制图PersName。禁止映射长度$numGreaterThanTen=禁止映射(长度。PersName)numGreaterThanTen--lhscontmap PersName。冲突映射长度$numGreaterThanTentracmap PersName。Contracmap Length$Predicate(\n-&>;n>;10)--展开numGreaterThanTencontmap PersName(谓词$\str->;let num=length str bool=num>;10 in bool)--应用长度谓词$\Person->;let str=PersName Person Num=Length str bool=num>;10 in bool)--应用PersName=>;谓词。PersonName)numGreaterThanTentracmap(\Person->;let str=PersonName Person Num=length str in num)numGreaterThanTen--扩展长度。PersonName谓词(\Person->;let str=PersonName Person Num=Length str bool=num>;10--在bool中展开numGreaterThanTen)=>;谓词Person--equalitylhs==RHS谓词(\Person->;let str=PersonName Person Num=Length str bool=num>;10 in bool)==谓词(\Person->;let str=PersonName Person Num=Length str bool=num>;10 in bool)==谓词(\Person->;let str。

--infix l 4(>;$<;)::逆变量f=>;(a->;b)->;f b->;f a--contmap::逆变量f=>;(a->;b)->;f b->;f a。

P5::谓词Intp5=谓词$\n->;n==5 pLength5::Predic[a]pLength5=Length>;$<;p5getPredicate pLength5";hello";--TruegetPredicate pLength5";hello world";--false。

--infix l 4(>;$$<;)::逆变量f=>;f b->;(a->;b)->;f a--contmap::逆变量f=>;(a->;b)->;f b->;f a。

这些组合符接受常量输入,并在运行ContraVariable实例时完全忽略提供的输入。

--const在给定两个值时返回第一个值,忽略第二个const::a->;b->;a onst x_=x contmap::ContraVariant f=>;(a->;b)->;f b->;f a(>;$)::b->;f b->;f a(>;$)=contmap。Const(>;$)b=对比表(Const B)--使用b(>;$)b=对比表(a->;b)--应用`const b`(>;$)b fb=对比表(a->;b)fb--使用fb(>;$)b fb=fa--简化`对比表(a-&>;b)fb`。

P5::谓词Intp5=谓词$\n->;n==5 pLength5::Predic[a]pLength5=反映射长度p5getPredicate pLength5";hello";--TruegetPredicate pLength5";hello world";--false pAlwaysFalse::Predic[a]pAlwaysFalse=10>;$p5

让我们看看另一个逆变的例子。假设您有以下数据类型,该数据类型封装了对某个多态类型a执行一些副作用:

出于我们的目的,我们可以假设我们将使用它将一些值记录到控制台、文件或其他介质中。此示例改编自CO-log记录库的LogAction类。绝对可以去图书馆看看ContraVariant和Friends在现实世界中的用法。

正如我们所看到的,类型变量a出现在输入位置,因此我们应该能够为它定义一个ContraVariant实例:

实例矛盾LogAction,其中contmap::(B-&>;a)->;LogAction a->;LogAction b对应图bToA logActionA=LogAction$\b->;unlog logActionA(BToA B)。

这里不应该有什么意外;我们在将输入传递给日志操作之前对输入运行提供的函数bToA。

PutStrLog和putStrLn只是从base开始对putStr和putStrLn进行包装。两者都将字符串记录到控制台,不同之处在于putStrLn在每次调用后都会向控制台发送换行符。

现在因为我们有逆变的能力,所以如果我们可以将其他类型转换为字符串,我们应该能够注销它们。

--LogAction putStringlyLnLog::(a->;string)->;LogAction aputStringlyLnLog f=contmap f putStrLnLog--现在我们可以记录Ints putStrLnInt::LogAction IntputStrLnInt=putStringlyLnLog show data Person=Person{name::string,age::int}-人员show的自定义字符串表示形式。,age:";<;>;(Show Age)<;<;";)";--现在我们可以记录用户putStrLnPerson::LogAction PersonputStrLnPerson=putStringlyLnLog showPerson--仅显示age showPersonAge::Person->;StringshowPersonAge=";age:";<的自定义字符串表示形式

Unlog putStrLnInt 42--42unlog putStrLnPerson$Person";Neelix";60--Person(姓名:Neelix,年龄:60)unlog putStrLnPersonAge$Person";Tuvok&34;240--age:240。

我们可以看到,Person的LogAction需要一个Person实例作为输入来执行日志操作。

可能并不明显的一件事是,我们还可以使输入类型适应其自身。没有必要总是从一种类型转换成另一种类型。

您好::String->;Stringhello=(";Hello";<;>;)here::string->;Stringhere=(";here<;<;>;)Doctor::String->;StringDoctor=(";Doctor";<;>;)空间:String->;Stringspace=(";&34;<;>;)空间::String->;Stringspace=(";";<;>;)空间::String->;Stringspace=(";";&。

PutStrLnGreeting::LogAction StringputStrLnGreeting=对比表空间。禁忌地图医生。对比表空间。对比表在那里。对比表空间。Contracmap hello$putStrLnLog。

哇哦!那甚至很难读懂。是干什么的呢?从逆变第二定律中记住:

PutStrLnGreeting::LogAction StringputStrLnGreeting=tracmap(hello。太空。在那里。太空。医生。空格)$putStrLnLog。

至少这在某种程度上更具可读性--但最棒的是,了解法律有助于我们使代码更易读。不过,这是做什么用的?

对照图f。对比表g=对比表(g.。F)--请注意(g.。F)而不是(f.g)。

PutStrLnGreeting::LogAction StringputStrLnGreeting=tracmap(hello。太空。在那里。太空。医生。空格)$putStrLnLogunlog putStrLnGreeting";Switzer";--使用";Switzer";作为输入运行记录器--输入将通过以下函数序列:--(hello。太空。在那里。太空。医生。空格)--应用空格切换--应用博士&34;<;>;&34;";<;&>切换--应用空格";&34;<;&>;&34;";<;&>;";";";";<;>;切换,应用空格#34;";<;>;";";<;&>;切换--应用空间";<;&>;";";<;>;";";<;&>;切换。那里";<;&>;";";医生";<;&>;";";<;>;开关--应用空间";<;&>;";";<;&>;";";<;>;&##。<;>;";<;>;开关--正在应用hello";hello&34;<;>;";<;>;";";<;>;";医生";<;>;";";";";<;>;";";<;>;";";";";";";<;>;";";<;>;";";&#。Switzer--最终输出:--您好,Switzer博士。

让我们再看一个可能很有趣的LogAction;其中我们忽略输入并返回一些常量输出:

A我们前面提到过,const被定义为a->;b->;a,其中它接受两个输入,但返回第一个输入的值(忽略第二个输入)。

QPutStrLn::LogAction Stringq。

.