编程理论:变体

您好,我叫Dmitry Karlovsky,我...想告诉您类型系统的基本特性,这种类型特性通常是或根本不被理解,或者由于特定语言的实现而被误解了,由于发展的演变,该语言具有许多缺陷。 因此,即使您认为自己知道什么是“变异”,也请尝试以全新的方式看待问题。 我们将从最基础的内容入手,因此即使是初学者也可以理解所有内容。 而且我们会继续用水,因此,即使专业人士也将对他们的知识构成有用。 代码示例将使用类似于TypeScript的伪语言。 然后将研究几种实际语言的方法。 而且,如果您正在开发自己的语言,那么本文将帮助您不要踩别人的耙子。


如果有狐狸怎么办?


参数和参数


参数是我们接受的。 描述参数的类型时,我们对可以传递给我们的类型集设置了限制。 一些例子:


//   function log( id : string | number ) {} //   class Logger { constructor( readonly id : Natural ) {} } //   class Node< Id extends Number > { id : Id } 

一个论点就是我们传递的。 在传输时,参数始终具有某些特定类型。 但是,在静态分析中,可能不知道特定类型,这就是为什么编译器会再次在类型限制下运行。 一些例子:


 log( 123 ) //   new Logger( promptStringOrNumber( 'Enter id' ) ) //       new Node( 'root' ) //   ,   

亚型


类型可以形成层次结构。 子类型超类型的特例。 可以通过缩小超类型的可能值集合来形成子类型。 例如,自然类型是整数和正数的子类型。 并且这三个都是同时的Real的子类型。 Prime类型是上述所有类型的子类型。 同时,Positive和Integer类型是重叠的,但是没有一个会约束另一个。


图片


形成子类型的另一种方法是通过将其与与它正交的另一种类型组合来扩展它。 例如,存在一个具有“颜色”属性的“彩色图形”,而存在一个具有“高度”属性的“正方形”。 通过组合这些类型,我们得到一个“颜色方块”。 加上一个带有“半径”的“圆”,我们可以得到一个“色筒”。


图片


层次结构


为了进一步叙述,我们需要一个小的动物层次结构和一个类似的细胞层次结构。


 abstract class Animal {} abstract class Pet extends Animal {} class Cat extends Pet {} class Dog extends Pet {} class Fox extends Animal {} class AnimalCage { content : Animal } class PetCage extends AnimalCage { content : Pet } class CatCage extends PetCage { content : Cat } class DogCage extends PetCage { content : Dog } class FoxCage extends AnimalCage { content : Fox } 

下面的所有内容都是上面类型的缩小范围。 带宠物的笼子只能容纳家畜,而不能容纳野生动物。 带狗的笼子只能容纳狗。


图片


协方差


最简单和最容易理解的是对超类型或协方差的限制 。 在以下示例中,功能参数与为其指定的类型协变。 也就是说,函数既可以接受此类型本身也可以接受其任何子类型,但不能接受超类型或其他类型。


 function touchPet( cage : PetCage ) : void { log( `touch ${cage.content}` ) } touchPet( new AnimalCage ) // forbid touchPet( new PetCage ) // allow touchPet( new CatCage ) // allow touchPet( new DogCage ) // allow touchPet( new FoxCage ) // forbid 

图片


由于我们不更改笼子中的任何东西,因此我们可以安全地将功能转移给带猫的笼子,因为这只不过是带宠物笼子的特殊情况。


逆差


很难理解亚型的限制或矛盾。 在以下示例中,功能参数与为其指定的类型相反。 也就是说,函数既可以接受此类型本身也可以接受其任何超类型,但不能接受子类型或其他类型。


 function pushPet( cage : PetCage ) : void { const Pet = random() > .5 ? Cat : Dog cage.content = new Pet } pushPet( new AnimalCage ) // allow pushPet( new PetCage ) // allow pushPet( new CatCage ) // forbid pushPet( new DogCage ) // forbid pushPet( new FoxCage ) // forbid 

图片


我们不能将猫带进笼子,因为该功能可以将狗放在那里,这是不允许的。 但是可以将任何动物的笼子安全地转移,因为猫和狗都可以放在那里。


不变性


限制子类型和超类型可以同时存在。 这种情况称为不变性。 在以下示例中,功能参数对于为其指定的类型是不变的。 也就是说,该函数只能接受指定的类型,不能再接受其他类型。


 function replacePet( cage : PetCage ) : void { touchPet( cage ) pushPet( cage ) } replacePet( new AnimalCage ) // forbid replacePet( new PetCage ) // allow replacePet( new CatCage ) // forbid replacePet( new DogCage ) // forbid replacePet( new FoxCage ) // forbid 

图片


replacePet函数继承了其内部使用的那些函数的限制:它从pushPet接受了对类型的限制,并通过pushPet接受了pushPet类型的pushPet 。 如果我们给她放上任何动物的笼子,她将无法将其转移到touchPet函数中,后者不知道如何处理狐狸(野生动物只会咬断手指)。 而且,如果我们将笼子与猫一起转移,则无法调用pushPet


双方差


人们不能不提到异国情调的缺乏限制 -双方差。 在下面的示例中,函数可以接受子类型或子类型的任何类型。


 function enshurePet( cage : PetCage ) : void { if( cage.content instanceof Pet ) return pushPet( cage ) } replacePet( new AnimalCage ) // allow replacePet( new PetCage ) // allow replacePet( new CatCage ) // allow replacePet( new DogCage ) // allow replacePet( new FoxCage ) // forbid 

图片


您可以在其中随动物一起转移笼子。 然后,她将检查笼子中是否有宠物,否则,将其放入随机宠物中。 例如,您可以转移带猫的笼子,然后她什么也不会做。


概论


有些人认为方差某种程度上与概括有关。 通常是因为通常使用通用容器作为示例来解释差异。 但是,在整个故事中,我们仍然没有一个单一的概括-完全是具体的类:


 class AnimalCage { content : Animal } class PetCage extends AnimalCage { content : Pet } class CatCage extends PetCage { content : Cat } class DogCage extends PetCage { content : Dog } class FoxCage extends AnimalCage { content : Fox } 

这样做是为了证明方差问题与概化无关。 仅需要泛化以减少复制粘贴。 例如,可以通过简单的概括来重写上面的代码:


 class Cage<Animal> { content : Animal } 

现在,您可以创建任何单元的实例:


 const animalCage = new Cage<Animal>() const petCage = new Cage<Pet>() const catCage = new Cage<Cat>() const dogCage = new Cage<Dog>() const foxCage = new Cage<Fox>() 

限制声明


请注意,前面列出的所有四个功能的签名都完全相同:


 ( cage : PetCage )=> void 

也就是说,对函数接受参数的这种描述不完整-从中不能说可以将其传递给函数。 好吧,除非可以清楚地看到,将狐狸笼子放进去绝对不值得。


因此,在现代语言中,有一种方法可以明确指示参数具有哪些类型限制。 例如,C#中的inout修饰符:


 interface ICageIn<in T> { T content { set; } } // contravariant generic parameter interface ICageOut<out T> { T content { get; } } // covariant generic parameter interface ICageInOut<T> { T content { get; set; } } // invariant generic parameter 

不幸的是,在C#中,修饰符的每个变体都必须在单独的接口上启动。 另外,据我了解,C#中的双方差通常是无法表达的。


输出参数


函数不仅可以接受,还可以返回值。 通常,返回值不能为1。 例如,采取带宠物的笼子并归还两只宠物的功能。


 function getPets( input : PetCage ) : [ Pet , Pet ] { return [ input.content , new Cat ] } 

这种功能等效于除一个输入参数外还具有另外两个输出的功能。


 function getPets( input : PetCage , output1 : PetCage , output2 : PetCage ) : void { output1.content = input.content output2.content = new Cat } 

外部代码在堆栈上分配了额外的内存,以便该函数将其要返回的所有内容放入其中。 完成后,调用代码将已经能够将这些容器用于自己的目的。


图片


从这两个函数的等价关系可以得出,与参数相比,函数返回的值始终与指定的输出类型相反。 一个函数可以向它们写入,但不能从它们读取。


对象方法


对象方法是将指向对象的附加指针作为隐式参数的函数。 即,以下两个功能是等效的。


 class PetCage { pushPet() : void { const Pet = random() > .5 ? Cat : Dog this.content = new Pet } } 

 function pushPet( this : PetCage ) : void { const Pet = random() > .5 ? Cat : Dog this.content = new Pet } 

但是,必须注意,与常规函数不同的是,方法也是该类的成员,该类是该类型的扩展。 这导致以下事实:调用此方法的函数会出现一个附加的超类型限制:


 function fillPetCage( cage : PetCage ) { cage.pushPet() } 

图片


pushPet方法尚未定义的情况下,我们不能向其传递这样的pushPet 。 这与不变的情况相似,因为从下方和上方都有限制。 但是, pushPet方法的位置在层次结构中可能更高。 这就是改型限制所在。


芭芭拉·里斯克(Barbara Lisk)换人原则(LSP)


许多人认为,子类型与子类型的比率不是基于前面提到的缩小和扩展类型的方法来确定的,而是通过在使用超类型的任何位置替换子类型的可能性来确定的。 显然,此错误的原因正是在LSP中。 但是,让我们仔细阅读该原理的定义,注意什么是主要的,什么是次要的:


使用基本类型的函数应该能够使用基本类型的子类型而不知道它,也不会破坏程序的正确性。

对于不可变的对象(包括那些不引用可变对象的对象),由于没有地方可以接受子类型限制,因此自动执行此原理。


对于可变变量,它变得越来越困难,因为以下两种情况对于LSP原理是互斥的:


  1. A有一个B的子类,其中字段B::fooA::foo的子类型。
  2. A的方法可以更改A::foo字段。

因此,只剩下三种方式:


  1. 防止对象继承缩小其字段类型。 但是随后您可以将大象推入笼子里养猫。
  2. 不是由LSP指导,而是由每个函数的每个参数的可变性分别指导。 但是随后您必须考虑很多,并向编译器说明类型限制在哪里。
  3. 吐上一切去 修道院 函数式编程,其中所有对象都是不可变的,这意味着它们接受的参数与声明的类型协变。

打字稿


在时间脚本中,逻辑很简单:将函数的所有参数视为协变的(不正确),将返回值视为反变的(正确的)。 先前已证明,函数的参数可以有任何变化,具体取决于此函数对这些参数的作用。 因此,这些是以下事件:


 abstract class Animal { is! : 'cat' | 'dog' | 'fox' } abstract class Pet extends Animal { is! : 'cat' | 'dog' } class Cat extends Pet { is! : 'cat' } class Dog extends Pet { is! : 'dog' } class Fox extends Animal { is! : 'fox' } class Cage<Animal> { content! : Animal } function pushPet( cage : Cage<Pet> ) : void { const Pet = Math.random() > .5 ? Cat : Dog cage.content = new Pet } pushPet( new Cage<Animal>() ) // forbid to push Pet to Animal Cage :-( pushPet( new Cage<Cat>() ) // allow to push Dog to Cat Cage :-( 

要解决此问题,您必须使用不平凡的代码来帮助编译器:


 function pushPet< PetCage extends Cage<Animal> >( cage: Cage<Pet> extends PetCage ? PetCage : never ): void { const Pet = Math.random() > .5 ? Cat : Dog cage.content = new Pet } pushPet( new Cage<Animal>() ) // allow :-) pushPet( new Cage<Pet>() ) // allow :-) pushPet( new Cage<Cat>() ) // forbid :-) pushPet( new Cage<Dog>() ) // forbid :-) pushPet( new Cage<Fox>() ) // forbid :-) 

在线尝试


Flowjs


FlowJS具有更高级的类型系统。 特别地,在类型描述中可以指出其对于通用参数和对象字段的可变性 。 在我们的单元格示例中,它看起来像这样:


 class Animal {} class Pet extends Animal {} class Cat extends Pet {} class Dog extends Pet {} class Fox extends Animal {} class Cage< Animal > { content : Animal } function touchPet( cage : { +content : Pet } ) : void { console.log( `touch ${typeof cage.content}` ) } function pushPet( cage: { -content: Pet } ): void { const Pet = Number((0: any)) > .5 ? Cat : Dog cage.content = new Pet } function replacePet( cage : { content : Pet } ) : void { touchPet( cage ) pushPet( cage ) } touchPet( new Cage<Animal> ) // forbid :-) touchPet( new Cage<Pet> ) // allow :-) touchPet( new Cage<Cat> ) // allow :-) touchPet( new Cage<Dog> ) // allow :-) touchPet( new Cage<Fox> ) // forbid :-) pushPet( new Cage<Animal> ) // allow :-) pushPet( new Cage<Pet> ) // allow :-) pushPet( new Cage<Cat> ) // forbid :-) pushPet( new Cage<Dog> ) // forbid :-) pushPet( new Cage<Fox> ) // forbid :-) replacePet( new Cage<Animal> ) // forbid :-) replacePet( new Cage<Pet> ) // allow :-) replacePet( new Cage<Cat> ) // forbid :-) replacePet( new Cage<Dog> ) // forbid :-) replacePet( new Cage<Fox>) // forbid :-) 

在线尝试


这里的双方差是无法表达的。 不幸的是,在没有明确描述所有字段的类型的情况下,我找不到更方便地设置方差的方法。 例如,如下所示:


 function pushPet( cage: Contra< Cage<Pet> , 'content' > ): void { const Pet = Number((0: any)) > .5 ? Cat : Dog cage.content = new Pet } 

C锐


C#最初设计时对变化没有任何了解。 但是,稍后又添加了参数修饰符,这使编译器可以正确检查传递的参数类型。 不幸的是,再次使用这些修饰符不是很方便。


 using System; abstract class Animal {} abstract class Pet : Animal {} class Cat : Pet {} class Dog : Pet {} class Fox : Animal {} interface ICageIn<in T> { T content { set; } } interface ICageOut<out T> { T content { get; } } interface ICageInOut<T> { T content { get; set; } } class Cage<T> : ICageIn<T>, ICageOut<T>, ICageInOut<T> { public T content { get; set; } } public class Program { static void touchPet( ICageOut<Pet> cage ) { Console.WriteLine( cage.content ); } static void pushPet( ICageIn<Pet> cage ) { cage.content = new Dog(); } static void replacePet( ICageInOut<Pet> cage ) { touchPet( cage as ICageOut<Pet> ); pushPet( cage as ICageIn<Pet> ); } void enshurePet( Cage<Pet> cage ) { if( cage.content is Pet ) return; pushPet( cage as ICageIn<Pet> ); } public static void Main() { var animalCage = new Cage<Animal>(); var petCage = new Cage<Pet>(); var catCage = new Cage<Cat>(); var dogCage = new Cage<Dog>(); var foxCage = new Cage<Fox>(); touchPet( animalCage ); // forbid :-) touchPet( petCage ); // allow :-) touchPet( catCage ); // allow :-) touchPet( dogCage ); // allow :-) touchPet( foxCage ); // forbid :-) pushPet( animalCage ); // allow :-) pushPet( petCage ); // allow :-) pushPet( catCage ); // forbid :-) pushPet( dogCage ); // forbid :-) pushPet( foxCage ); // forbid :-) replacePet( animalCage ); // forbid :-) replacePet( petCage ); // allow :-) replacePet( catCage ); // forbid :-) replacePet( dogCage ); // forbid :-) replacePet( foxCage ); // forbid :-) } } 

在线尝试


爪哇


在Java中,切换变体的功能添加得很晚,并且仅适用于通用参数,而这些参数本身是相对较新的。 如果该参数不通用,那就麻烦了。


 abstract class Animal {} abstract class Pet extends Animal {} class Cat extends Pet {} class Dog extends Pet {} class Fox extends Animal {} class Cage<T> { public T content; } public class Main { static void touchPet( Cage<? extends Pet> cage ) { System.out.println( cage.content ); } static void pushPet( Cage<? super Pet> cage ) { cage.content = new Dog(); } static void replacePet(Cage<Pet> cage ) { touchPet( cage ); pushPet( cage ); } void enshurePet( Cage<Pet> cage ) { if( cage.content instanceof Pet ) return; pushPet( cage ); } public static void main(String[] args) { Cage<Animal> animalCage = new Cage<Animal>(); Cage<Pet> petCage = new Cage<Pet>(); Cage<Cat> catCage = new Cage<Cat>(); Cage<Dog> dogCage = new Cage<Dog>(); Cage<Fox> foxCage = new Cage<Fox>(); touchPet( animalCage ); // forbid :-) touchPet( petCage ); // allow :-) touchPet( catCage ); // allow :-) touchPet( dogCage ); // allow :-) touchPet( foxCage ); // forbid :-) pushPet( animalCage ); // allow :-) pushPet( petCage ); // allow :-) pushPet( catCage ); // forbid :-) pushPet( dogCage ); // forbid :-) pushPet( foxCage ); // forbid :-) replacePet( animalCage ); // forbid :-) replacePet( petCage ); // allow :-) replacePet( catCage ); // forbid :-) replacePet( dogCage ); // forbid :-) replacePet( foxCage ); // forbid :-) } } 

在线尝试


C ++


C ++凭借其强大的模板系统,可以表达各种变体,但是当然有很多代码。


 #include <iostream> #include <typeinfo> #include <type_traits> class Animal {}; class Pet: public Animal {}; class Cat: public Pet {}; class Dog: public Pet {}; class Fox: public Animal {}; template<class T> class Cage { public: T *content; }; template<class T, class = std::enable_if_t<std::is_base_of<Pet, T>::value>> void touchPet(const Cage<T> &cage) { std::cout << typeid(T).name(); } template<class T, class = std::enable_if_t<std::is_base_of<T, Pet>::value>> void pushPet(Cage<T> &cage) { cage.content = new Dog(); } void replacePet(Cage<Pet> &cage) { touchPet(cage); pushPet(cage); } int main(void) { Cage<Animal> animalCage {new Fox()}; Cage<Pet> petCage {new Cat()}; Cage<Cat> catCage {new Cat()}; Cage<Dog> dogCage {new Dog()}; Cage<Fox> foxCage {new Fox()}; touchPet( animalCage ); // forbid :-) touchPet( petCage ); // allow :-) touchPet( catCage ); // allow :-) touchPet( dogCage ); // allow :-) touchPet( foxCage ); // forbid :-) pushPet( animalCage ); // allow :-) pushPet( petCage ); // allow :-) pushPet( catCage ); // forbid :-) pushPet( dogCage ); // forbid :-) pushPet( foxCage ); // forbid :-) replacePet( animalCage ); // forbid :-) replacePet( petCage ); // allow :-) replacePet( catCage ); // forbid :-) replacePet( dogCage ); // forbid :-) replacePet( foxCage ); // forbid :-) return 0; } 

在线尝试


d


D没有任何明智的方式来明确指示方差,但是他知道如何根据使用情况来推断类型。


 import std.stdio, std.random; abstract class Animal {} abstract class Pet : Animal { string name; } class Cat : Pet {} class Dog : Pet {} class Fox : Animal {} class Cage(T) { T content; } void touchPet( PetCage )( PetCage cage ) { writeln( cage.content.name ); } void pushPet( PetCage )( PetCage cage ) { cage.content = ( uniform(0,2) > 0 ) ? new Dog() : new Cat(); } void replacePet( PetCage )( PetCage cage ) { touchPet( cage ); pushPet( cage); } void main() { Cage!Animal animalCage; Cage!Pet petCage; Cage!Cat catCage; Cage!Dog dogCage; Cage!Fox foxCage; animalCage.touchPet(); // forbid :-) petCage.touchPet(); // allow :-) catCage.touchPet(); // allow :-) dogCage.touchPet(); // allow :-) foxCage.touchPet(); // forbid :-) animalCage.pushPet(); // allow :-) petCage.pushPet(); // allow :-) catCage.pushPet(); // forbid :-) dogCage.pushPet(); // forbid :-) foxCage.pushPet(); // forbid :-) animalCage.replacePet(); // forbid :-) petCage.replacePet(); // allow :-) catCage.replacePet(); // forbid :-) dogCage.replacePet(); // forbid :-) foxCage.replacePet(); // forbid :-) } 

在线尝试


结语


现在就这些了。 我希望介绍的材料可以帮助您更好地理解类型的限制,以及如何以不同的语言实现它们。 总的来说,某处更好,某处更糟,某处没有办法,但总之。 也许是您将开发一种语言,使所有这些语言都可以方便地实现并且是类型安全的。 同时,加入我们的电报聊天,有时我们讨论编程语言的理论概念

Source: https://habr.com/ru/post/zh-CN477448/


All Articles