在2019年刚刚接触Rust的时候(那会每周例行去医院光公交车要坐一个小时,就拿着手机在车上看,好怀念),就会看到Rust的官方文档里用Java等语言的interface
来类比自己的trait
,并且还会声明“这只是一种比喻,trait
与interface
其实有着相当大的区别”,不仅Rust如此,类似Scala这样的语言也会用这种类比来描述自己的trait
特性,自那时起,“trait
和interface
到底有什么区别”这个问题就困扰了我相当一段时间。恰巧前一段时间看到某群群友讨论Rust的impl...for...
到底是什么东西;借着这个机会,来顺便讨论一下Rust中trait
的设计哲学。
多态
讨论一个语言中某个特性的设计哲学,必然要涉及到一些前置知识,如果仅仅通过给出例子来描述,恐怕看一万个trait
和interface
的例子也难解其中的区别(我当初没有学过PL相关知识的时候就是因为使用这种方法才始终弄不明白这两者的区别到底在哪),在开始讲进一步的内容之前,我想先聊一下多态(Polymorphism)
对于大部分人而言,多态这个词都是在学习OOP语言诸如Java的时候了解到的,许多人可能都耳闻过OOP的三原则继承(Inheritance),封装(Encapsulation),多态(Polymorphism)。多态在OOP中通常被作为接口来体现,在这类语言中,同一个类型上面的同一个方法可以有着不同的表现,例如
public interface Drawable {
void draw();
}
public class Rectangle implements Drawable {
public void draw() { System.out.println("Drawing Rectangle"); }
}
public class Triangle implements Drawable {
public void draw() { System.out.println("Drawing Triangle"); }
}
public class Circle implements Drawable {
public void draw() { System.out.println("Drawing Circle"); }
}
public class Painter {
public static void doDraw(Drawable drawable) {
drawable.draw();
}
}
在这样的一个例子中,doDraw(new Rectangle())
,doDraw(new Triangle())
与doDraw(new Circle())
显然有着不同的行为,分别输出Drawing Rectangle
,Drawing Triangle
与Drawing Circle
。但是在doDraw
内部来看,这些对象,无论是new Rectangle()
还是new Triangle()
,还是new Circle()
——都是Drawable
类型的,在doDraw
中调用的总是同一个类型Drawable
上的同一个方法draw()
,但是却表现出了不同的行为。你可以用多种不同的形式来总结这一点,例如“多态是多种不同的类型可以被视为一个统一的父类/接口”,或者“同一个类型上的同一个方法可以有不同的表现”。不管是哪种描述,其核心都是“相同的一段代码可以在被提供不同类型时有着不同的表现”。
多态是这样的,对吧?NO。多态是一个涵盖范围相当广泛的词,这个词并不仅仅意味着OOP这种形式的多态,事实上,还有另外两种多态的表现形式,如果用大部分人更为熟悉的术语来描述的话,它们分别是——泛型(Generics)与重载(Overloading)
Parametric Polymorphism与泛型
上面提到了,多态的含义是“同一段代码可以在被提供不同类型时产生不同的行为”,这是一种很模糊的描述,因此自然也有着多种不同的实现方法。一种与OOP中的多态不同的另一种实现就是Parametric Polymorphism,有时翻译做参数多态性,他的核心在于“类型也可以被当作参数”,也就是说,你可以写一段代码,但是把这段代码中的类型空出来,用一个占位符表示(就像函数的形式参数是实际参数的占位符一样),接着在使用的时候,再去用某个具体的类型去实例化它。在多数语言中,这种特性被称为泛型,在Java中,你可以写一个这样的函数
public static <T> T doubleFun(Function<T, T> f, T x) {
return f.apply(f.apply(x))
}
这是一个能把一个函数两次应用到同一个对象上的函数(类似f(f(x))),让它与众不同的原因是它的类型参数<T>
,当你想要把他应用在一个Double
类型的函数上面的时候,你只需要用Double
去实例化它:<Double>doubleFun(doubleFunc, 1.0d)
,当你想要把它应用在一个Character
类型的函数上面的时候,只需要用Character
去实例化即可:<Character>doubleFun(charFunc, 'a')
。在Parametric Polymorphism,或者说泛型中,类型也是参数,他们会在函数被调用的时候被实例化,用以搭配不同类型的参数。
说到这里,Parametric Polymorphism和OOP中传统意义上的基于继承的多态到底有什么区别?如果他们两个是一种东西的话,不就陷入“既生瑜何生亮”的泥沼了吗?事实上,这个问题相当微妙,Parametric Polymorphism和基于继承的多态(下文中讲称其为Subtyping)可以称得上是“对立且统一”,他们都有着多态所描述的现象,但是这两种多态在几种属性上却又完全不同。
Universality
Parametric Polymorphism之所以能被称之为泛型,是因为在这样的函数中,类型是“泛化”的:一个泛型函数可以用任何类型来实例化,例如上面的doubleFun
这样一个函数,可以用Integer
,Double
,Character
,等等任何你想的类型去实例化它,从这个角度来看,他是通用的(Universal),而接口则不享有这个属性,回过头来看上面的doDraw
函数,虽然也能做到传进去若干种不同的类型,诸如Rectangle
,Triangle
等不一而足,但是他们都被限制为是Drawable
接口的实现类,也就是说,他们与Drawable
接口之间存在一个非常明显的关系——父子类型的关系,并且由于只有Drawable
的实现类才能适用,doDraw
显然没有Universal的属性:你不可能给doDraw
传进去一个Integer
类型的对象,能用来“实例化”doDraw
的类型,被限制在了“Drawable
的子类”这样一个范围内。
Parametricity
Parametric Polymorphism,从这个名字中可以引申出它的一个重要属性——Parametricity,Parametricity是所有泛型函数享有的一个属性——不管用什么样的类型去实例化他们,这些函数的表现区别仅仅体现在类型的不同上,而行为上则完全相同,例如无论你用什么类型去实例化doubleFun
,他所做的都是“把f
应用在x
上两次”,不可能因为你应用在了不同的类型上就变成了应用三次或者额外多打印几句话,doubleFun
用不同类型实例化的唯一区别只是f
与x
的类型随之不同。
实际上,如果我们把函数都看做纯函数(不考虑函数外部的环境,没有副作用),那么我们甚至可以根据函数的类型来写出函数的实现,例如唯一能满足类型\forall X. X \to X(X可以被看做是泛型参数,而X \to X代表一个接受X作为参数,同时返回一个X类型的值的函数类型)的值就是\lambda X.\lambda x:X. x,如果用Java来举例子,那就是唯一能够满足函数签名
<X> X f(X x)
的实现就是
<X> X f(X x) { return x; }
因为这个函数需要返回一个
T
类型的值,而他唯一能获取到这个值的地方就是来自参数x
,因此他能做的事情也就仅仅是返回这个x
——注意,虽然在类似Java的语言中,你可以给泛型参数加上约束从而保证这些泛型一定满足某些条件,比如都定义了某个方法(例如通过约束<X extends Drawable>
来允许调用x.draw()
),但是当我们考虑纯粹的Parametric Polymorphism,或者说纯粹的System F时,泛型参数是不带有任何信息的,也不存在这些约束:它就仅仅是一个占位符,你无法从它身上得知任何额外的信息。
而对于Subtyping来说,则没有这个限制,一个接口的不同实现完全可以不仅在类型上,而且在行为上都完全不同。个中原因十分好理解:对于接口来说,每个实现类都写了自己的实现,三个实现类的三个draw()
方法就有三处不同的定义,而对泛型函数来说,无论是什么类型,都只拥有doubleFun
这一个定义。
那么,说了这么多看似无关的内容,Rust的trait
到底属于什么呢?看上去是Subtyping和Parametric Polymorphism的结合体?毕竟你用impl...for...
实现trait
的时候也必须实现trait
中定义的函数,似乎和接口的作用完全一致了起来?这个问题的答案同样是NO:Rust的trait
的确涉及了Parametric Polymorphism这一种多态,因为在Rust中也有泛型的概念,但是它的另一半却是一种和函数重载同源的多态,与OOP中的接口并无半点关系——尽管他们看起来很像。
Rust的impl
函数重载,一般指的是几个名字一样,但是参数不同的函数(他们的返回值可以相同也可以不同,但是参数类型或者参数数量一定不能相同),例如在Java中,你可以写这样的两个函数:
String toString(int i);
String toString(char c);
他们的名字完全相同,但是参数的类型不同——一个是int
,一个是char
,因此他们互相都是对方的函数重载,当你在调用toString(1)
和toString('1')
的时候,虽然看上去像是在调用同一个函数,但是实际上前者调用的是第一个toString
的实现,而后者调用的是第二个toString
的实现——这同样是一种多态,同一段代码(对toString
的调用)在不同类型上(1
的类型是int
,'1'
的类型是char
)有着不同的表现(int
类型的字符串化和char
类型的字符串化逻辑和实现都是不相同的)。这样的多态,被称为Ad-hoc Polymorphism,其中Ad-hoc是拉丁文,翻译成英文是"to this/for this",而在中文语境下,可以背理解为“特别的,专门的”,换言之,Ad-hoc Polymorphism是一种“针对每个类型拥有自己的实现”的多态,这个描述应用在函数重载上面非常合适,毕竟,函数重载就是针对不同类型的参数的若干个定义不同但是同名的函数。
But wait,如果我没记错的话,Rust应该是不允许多态的,也就是在Rust中,你不能在同一个作用域下写出
fn stringify(i: &u32) -> String
fn stringify(c: &char) -> String
这两个重载出来。没错,Rust不允许重载,但是,你可以把to_string
写进一个trait
里:
trait Stringify {
fn stringify(&self) -> String;
}
然后为他提供两个u32
和char
上的impl
:
impl Stringify for u32 {
fn stringify(&self) -> String {
format!("{}", &self)
}
}
impl Stringify for char {
fn stringify(&self) -> String {
format!("{}", &self)
}
}
然后我们就可以分别给u32
和char
调用stringify
函数了:
println!("{}", u32::stringify(&1));
println!("{}", char::stringify(&'1'));
这种方法,在调用方式上和重载有几分神似,在实现方式上和接口也有几分神似,那么为什么我会说他是和重载一样的Ad-hoc Polymorphism,而不是和接口一样的Subtyping呢?原因在于类型之间的依赖关系上。在接口对应的Subtyping中,implements
代表了接口与其实现类之间的父子类型关系——这两个类型之间存在一个很强的依赖关系,它们互为父子类型。而在Ad-hoc Polymorphism中,则不存在这样的关系,在函数重载的例子中,一方是函数本身,而另一方是其参数的类型,这两者甚至不属于同一范畴下的概念,自然不可能有任何关系;而Rust的trait
也是如此,在上面的例子中,一方是Stringify
,另一方是他的“实现类”&u32
,我们能给一个&u32
类型的变量调用stringify
方法,但是u32
本身却并不是Stringify
这个trait
的子类型,严格来说,它们两个之间什么关系都没有,在类型系统中完完全全是两个互不相干的个体,唯一的关联是Stringify
有一个针对&u32
的实现,而如同函数重载中,编译器会根据参数的不同选择不同的重载一样,Rust编译器也会根据类型不同,选择trait
的不同impl
。除此之外,Ad-hoc与Subtyping对应多态类型的方式也不相同:前者是“应用在不同类型的参数上的同一函数(stringify
)行为不同”,而后者是“应用在被看作是同一类型(父类/接口)的实则不同的类型(子类/实现类)的参数上的同一函数(doDraw
)行为不同”
正因如此,在Rust中,impl
对应了与函数重载一样的Ad-hoc Polymorphism,而不是与OOP中的接口一样的Subtyping,我们可以做一个简单的表格,来划分Parametric,Ad-hoc,以及Subtyping三种不同的多态之间的区别,而这些区别体现在Universality,Parametricity,类型之间的关系,以及该类型多态的本质这四个维度上:
属性\多态种类 | Parametric | Ad-hoc | Subtyping |
---|---|---|---|
Parametricity (不同类型享有统一的行为) | 是 | 否 | 否 |
Universality (可以应用于任意类型,而不需要针对每个类型写出实现) | 是 | 否 | 否 |
类型之间的关系 (承载多态的类型之间存不存在依赖) | 不存在 | 不存在 | 存在 |
多态的本质 (该类型多态的实现方法) | 同一段代码可以应用在不同的类型上 | 同一函数在不同类型的参数上有不同的行为 | 同一函数在被看做是同一类型,实则是不同类型的参数上有不同的行为 |
值得注意的是,Non-Parametricity(也就是Parametricity的反面:不同类型可以拥有完全不同的行为)和Universality这两个属性在现代编程语言中缺少任意一个都会带来相当大的麻烦。例如这样一个例子:
trait Eq {
fn equiv(lhs: &Self, rhs: &Self) -> bool;
}
struct tuple2<K, V> {
key: K,
value: V
}
struct tuple3<K, V, C> {
key: K,
value: V,
column: C
}
impl <K, V> Eq for tuple2<K, V> where K: Eq, V: Eq {
fn equiv(lhs: &Self, rhs: &Self) -> bool {
lhs.key.equiv(&rhs.key) && lhs.value.equiv(&rhs.value)
}
}
impl <K, V, C> Eq for tuple3<K, V, C> where K: Eq, V: Eq, C: Eq {
fn equiv(lhs: &Self, rhs: &Self) -> bool {
lhs.key.equiv(&rhs.key) && lhs.value.equiv(&rhs.value) && lhs.column.equiv(&rhs.column)
}
}
fn check_equiv<T: Eq>(lhs: &T, rhs: &T) -> bool {
T::equiv(lhs, rhs)
}
就是Ad-hoc的Non-Parametricity,与Parametric的Universality结合才能做到的事情:如果没有Ad-hoc的trait
以及impl
,那么我们就无法为tuple2
和tuple3
两个不同类型的Eq
实现定义不同的行为——它们将会不得不一个实现Eq2
一个实现Eq3
这两个不同的类型来分别定义两个元素的元组和三个元素的元组的比较逻辑,这显然失去了多态的意义,而如果没有Parametric的泛型带来的Universality,我们就不得不写一堆
impl Eq for tuple2<u32, u32>
impl Eq for tuple2<String, String>
impl Eq for tuple3<char, char, char>
...
等等等等一系列类型上特化的Eq
实现,同时, check_equiv
也要变成一系列check_equiv_tuple
,check_equiv_str
,check_equiv_u32
等等针对每个类型特化的比较函数。从这里可以看到,Non-Parametricity与Universality缺一不可,而这也正是大部分现代多范式语言采用的策略:在诸如Java/C#等语言中,达成这一点靠的是基于接口/父子类型的Subtyping和基于泛型的Parametric;而在Rust/Haskell这些语言中,则是基于trait
/impl
这样的Ad-hoc Polymorphism和同样是泛型的Parametric,只不过在Haskell中,相比起trait
,它有一个更加响亮的名字——Typeclass;还有一些语言,比如Scala,同时支持三种多态:既能用trait
和extends
实现Subtyping,也能用泛型实现Parametric,还能用泛型trait
与given
,using
结合在一起实现Ad-hoc/Typeclass。
可以看出,Ad-hoc Polymorphism的表现方式相当广泛,从函数重载,到Haskell的class
,到Rust、Scala中的trait
,甚至还有本文中虽然没有提到,但是实际上同样属于Ad-hoc的扩展(Extensions),在Swift/Scala中,它们用extension
关键字声明,而C#/Kotlin虽然没有完善的对扩展的支持,却也支持扩展函数这种略弱于扩展的同种特性,有兴趣的话,可以思索一下为什么扩展函数这样的特性同样属于Ad-hoc Polymorphism的范畴。
Ad-hoc Polymorphism,相比起它的两姐妹:Subtyping和Parametric,显得更加鲜为人知,毕竟对于大部分人来说,函数重载似乎是一个理所当然的东西,不值得作为一个专门的特性来介绍。但是今天,在这篇文章中,我们发掘了Rust的impl
的设计哲学,探索了Ad-hoc Polymorphism除开重载之外的几种实现方式,了解了它的优点——尤其是两个类型之间的无关联性,或者,用软件工程的术语来说:“解耦”。在现代编程语言中可以找到这个略显阿卡林的重要角色越来越广的应用。应该说,Ad-hoc Polymorphism是一个极为优秀的设计,并且应当更加广泛的被计算机从业者,尤其是负责写代码的程序员本身了解并学习。各种教程,书籍与课程与其抓着已经被嚼烂的OOP三板斧和各种设计模式不放,不如拓宽一下视野,尝试尝试旁征博引,授读者以更加灵活的思维方式,更加广袤,才能正确、有效的发挥现代编程语言的全部潜力。
Comments 56 条评论
博主 n0099
疑似以下ts实现:
https://www.typescriptlang.org/play?#code/C4TwDgpgBA4gwgewHYGdgCcCuBjYD0A8AKlALxQDeAvgHxlRIQDuUAFAHScCG6A5igC4oXJCADaAXQCUZOkQDcUAPRKo2BGACWEACZQAZugQBbKAAtgwMIJVM77UJBTZ0msMAA2I3u3y8lOgjYKEpmIjoARggIANZKxpoAHpqo7BbGHgCwAFA5jtAAogCO9BQQRZiaAG5CrB5mglDFADRQ6A1CxTKkdFEIHhAiVPI5eeDQRJhgAwBMBADSrQBqdOQUOVCbUDEQIEKLG1tVXB6YEEJLOVSj2flQk9MQAMwLy61wq5SHmzt7UAfZLZQY6nc5QJbNb5qfqYYxIIRwK43fSYJC4TTIKDAKazADqmmAZmKxAAQlwUNAIIlgBAkDoULBEKgMDg8IQHrNXuCaDRWvMoFSaXSGS1wQLqbT6U0ijRWGSKUIiPKIDJ1oCtugINj0Eg1F4UAzsY8ZuKhVLlV91UDNuVKlU6h0sWZNChWu1GoSXVIhH0BiJLdbA20tZgdVB6ih2L92Lbqg7I783Q0o7spFCg9aAGSZ8PJkFnGMVOMR9j5iBJyNlqQjK3W661+v1lFo4AY3VGgZPfGE4lK8mUiXCxnINBYXD4Yg455ciFQD68-6myUiorLJdD0VwddS4qy5WK5WqqGa7W67D6w1Tp7bhkWtUZ2P2iNCT2utqO1-eqC+wa6+8ZjUQzDEto0feMUxACsILTWsAK2bNc0rE4CzAksyygqt0zgqAEJLdRTjhQs7XA-DYSQKDSLhassM2esgUbZFUXRTFsDMCBsBiYo7WIG9pVlZ97grRUvx-f1-02E9Q11EtUIaCtqOyesgA
博主 dylech30th
@n0099 我草,我今天上线咋发现你回复了这么多,你别急等我慢慢看,这评论区的md语法的样式都被干烂了
博主 n0099
@dylech30th 另外所有的完整的url字符串(
scheme ":" ["//" authority] path ["?" query] ["#" fragment]
)都没有被自动套上对应的<a target="_blank">
如 https://daniel.haxx.se/blog/2022/09/08/http-http-http-http-http-http-http/
我建议阁下直接使用被phpbb flarum等老牌知名大型php项目所使用的ugc文本预处理器:
s9e/TextFormatter
:https://github.com/s9e/TextFormatter/pull/181
https://s9etextformatter.readthedocs.io/Getting_started/Using_predefined_bundles/
博主 dylech30th
@n0099 我不懂这个啊,我搭blog是直接套模板的
博主 n0099
@dylech30th 套模板:直接复制粘贴奥利金德jvm底层壬上壬反fp代表irol的眼中钉mc圈皇帝梨木利亚的仇敌虾米神的部落格之 https://chariri.moe
建议立即致电隔壁 https://syzx.me 站长缘之空吧资源贩子黑兔给您亲自指挥亲自部署php82赋能wp
博主 dylech30th
@n0099 草,https://chariri.moe是CJ的blog,不是虾米的
博主 n0099
@dylech30th irol女士经典分不清奥利金德众神之皇帝LG dc神 CFPA会员Cyl18a CJ 当妈 jvm底层壬上壬虾米
博主 n0099
不如说在类型系统的层面看来任何函数本就只有类型信息而不存在body中实现的逻辑,也就是后文提及的
如果我们把函数都看做纯函数(不考虑函数外部的环境,没有副作用),那么我们甚至可以根据函数的类型来写出函数的实现
因此在类型层面看来
是
而如果手动把T特化成int这样的具体类型:
那么类型上
https://www.typescriptlang.org/play?#code/C4TwDgpgBAJg9gVwEYBsIDEEDsDGAeAFQD4oBeKACgDMAuSgQzoIEoySCAaKADyddPYBuALAAoUJCiBPJ0BoyoDC5QCFu8ZGky4ysRKgzZ8WBAFskEAE5FBQA
所以可以看出参数化多态在类型信息层面来看就是允许 具体的 硬编码的 在doubleFun内部声明好了的
int/number
类型变成不具体的T,从而允许caller(使用类型doubleFun的人)以后再来具体指定何谓T这也是为什么c++的参数化多态叫做模板而不是泛型,早已自愿退出邪恶组织四叶重工的cpp中级高手上海贵族信安底层壬上壬杨博文阁下也早已道明cpp模板本质上就是对类型信息做查找替换的宏,也就有了 https://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error
博主 dylech30th
@n0099 “不如说在类型系统的层面看来任何函数本就只有类型信息而不存在body中实现的逻辑”——这一点并不蕴含了任何类型系统都享有Parametricity,类似forall X. X → X这种类型之所以能直接写出他的实现是因为类型X是被抹除的,本身不蕴含任何其他有价值的信息
博主 n0099
@dylech30th 然而以四叶CS硕士PLT中级高手irol阁下经常复读的对奥利金德人的刻板印象为例:
forall 奥利金德人. is魔怔人 -> true
我们只能从中推导出
is魔怔人
是恒真式T -> true
博主 dylech30th
@n0099 我咋没看懂你这个例子是写的啥
博主 n0099
@dylech30th 就是
所有奥利金德人都是魔怔人
的意思这句话最早由irol阁下于20年提出,从此之后我每次在她面前提及奥利金德她都会下意识的复读这句话
博主 dylech30th
@n0099 如果你对这个感兴趣的话,可以了解一下Universal Introduction和Universal Elimination,也就是Natural Deduction中有关全称量化的推理规则
博主 n0099
@dylech30th 与此同时:我还在研读奥利金德皇帝lg神于22年写下的非理性哲学分析: https://lasm.dev/2022/09/28/first-order-logic-a-semiotics-approach/
博主 dylech30th
@n0099 里面其实有关我的那部分理解有错误的地方,正确的解释在那篇谈罗素的blog里,就是有关为什么量化的域不能是无限的那个问题
博主 dylech30th
@n0099 我提这个的原因是,在类似Natural Deduction里,你想要推导出一个全称量化的句子是有前提条件的,这个前提条件就是Universal Introduction
博主 n0099
@dylech30th 而以本文中的
doubleFun = (T, T) -> T -> T
为例您也可以写出直接返回第二层的参数而不是把第一层参数拿去apply两次,毕竟都能满足最终返回类型是T的约束博主 dylech30th
@n0099 你可以只看签名就知道
doubleFun
能有几种写法,parametricity的重点是同一个多态函数的所有实例表现都是一致的,也就是doubleFun<int>
和doubleFun<String>
的行为都是一样的博主 n0099
@dylech30th 然而我完全可以在doubleFun的实现中做任何事,甚至是导致永不停机的死循环,只要在这个死循环退出(而这是不可能的)之后能够返回一个类型T的东西就能符合函数签名
这应该是由签名唯一性来约束,也就是 父类-函数名称-参数列表(每个参数的类型,但不考虑名字)-可选的返回值类型 (有些语言衡量签名唯一时不考虑返回值类型)构成的一个tuple
而如果不具有唯一性也就是编译器允许有着多个签名完全相同的函数/类方法存在那就无法保证只根据类型信息就能保证其类型所代表的行为(函数body)的唯一性
据我所知没有编译器允许您声明复数个相同签名的函数/类方法
这就像dc神阁下在另一篇文章 https://sora.ink/archives/1631 中所提到的:
博主 dylech30th
@n0099 你能在
doubleFun
里做其他事情的大前提是 1:如果你要在doubleFun
里干其他事情比如print
意味着你引用了外部环境,而我们说的例子里默认doubleFun
是孤立的,纯的函数。2:如果你要写一个不停机的死循环,那么你需要支持递归——这一点在简单类型λ演算中是不成立的,只有支持递归类型的时候才能支持递归,原因在于不动点组合子fix
需要递归类型:博主 n0099
您可以品鉴一下现代php界最复杂高度集成开箱即用基于symfony抄ror起家的mvc框架laravel内部源码中的魔怔oop套娃风格,而不是dc神阁下目前对贵站所使用的wordpress其本质是一座历史悠久具有高度可扩展性的无类型命令式屎山
我一直认为以四人帮设计模式为代表的生搬硬套魔怔oop思维本质上是在基于subtyping的oop类型系统以及泛型所带来的参数化多态容许的自由度范围内的极限,是戴着脚镣的舞蹈
而php一直以来又没有泛型(https://github.com/PHPGenerics/php-generics-rfc/issues/49)使其比具有编译时泛型擦除(其导致java人无法new T,只能像php人(当然php没有泛型)那样传类名然后反射创建类实例,也就是
container.get(service.getClass().getName())
,而c#人直接Container.Get<Service>()
)的java还要受限的多(也就是类型表达力不足)而在这里之中垫底自然是js其甚至没有连pyphp都有的type hint,因此js库运行时检测类型十分的样板代码
当然npm细粒度包魔怔人在一个leftpad函数也是包的思想指引下早已npm publish了十万甚至九万个运行时类型检测和常用带类型约束的数据结构库轮子:
https://github.com/ajv-validator/ajv
https://github.com/hapijs/joi
https://github.com/imbrn/v8n
https://github.com/gcanti/io-ts
https://github.com/fabiandev/ts-runtime
博主 n0099
四叶PLT头子CS硕士仏皇irol阁下一直以来也是这样定性c#/kotlin的扩展函数的
但我始终认为
c#扩展函数本质语法糖
,在我看来他只是把对一个普通静态函数的调用变成了对静态函数的第一个参数的成员的引用调用(通过.
类成员访问运算符),并且两者完全等价resharper还提供了一键互转而v2ex热心用户在数十小时前某热门讨论串早已道明这本质上是 https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax 之争:
https://www.v2ex.com/t/903396#r_12481897:
https://www.v2ex.com/t/903396#r_12482208:
https://www.v2ex.com/t/903396#r_12483206
建议立即以irol最爱的haskell和cn神最爱的scala中的adhoc多态实现:
typeclass
概念为抓手赋能python博主 dylech30th
@n0099 他的确是语法糖,但是我认为我们在讨论这个问题的时候应当去观察行为而不是其实现,就好比java的泛型也可以被称为语法糖,他甚至在进入JVM内部之前就销声匿迹了,但是从类型系统上看,他依然是一个标准的泛型,其本质是泛型,和其实现是语法糖,这两点之间并不冲突
博主 n0099
@dylech30th 就好比ts的类型系统也可以被称为语法糖,他甚至在变成js之前就销声匿迹了,因为js没有类型所以tsc必须编译时就删除所有类型信息
如同jvm没有泛型所以javac必须编译时泛型擦除,而这导致上一条评论中提到的java无法
new T
因为运行时不知道何谓T,而c#则是编译时给所有代码中硬编码了的已知的可能出现的泛型参数组合生成对应的类让运行时缓存起来,如果需要new T那就是运行时反射动态创建新的填充了泛型参数的类(并缓存他)。c#不支持HKT(T1<T2<ConcreteType>>
)也使得运行时不需要处理可能无限嵌套的泛型参数,而只需要限制只有泛型嵌套树的第一层类型可以是泛型参数,更深的层都不允许:https://learn.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-examine-and-instantiate-generic-types-with-reflection
https://learn.microsoft.com/en-us/dotnet/api/system.type.isgenerictypedefinition
但是从类型系统上看,ts依然是一个标准的system F,其本质比js无类型强,和其最终产物还是js,这两点之间并不冲突
博主 dylech30th
@n0099 typescript可不能说是标准的System F,标准的System F只包含多态类型不包含OOP和类似
b ? int : string
这种类型,typescript核心类型系统严格来说是带拓展的System F-Omega-Sub,也就是多态类型+Type Operator+子类型博主 n0099
@dylech30th ts那种ducktype也算传统oo的严格subtype?
就像
四叶CS硕士PLT中级高手irol阁下
此前锐评java魔怔人为了让两个结构十分相似的类能够兼容而写出一个逐类prop去复制粘贴的converter(常见于bean中,而我也被迫在c#中写出了这样的恶俗玩意: https://github.com/n0099/TiebaMonitor/blob/e84a230fa0eb1c1095f6b6aa74b34a29f1f6a69d/crawler/src/Tieba/Crawl/Parser/ThreadParser.cs#L45 )的刷代码函数和运行时开销的罪恶行径,而在ducktyping中只要结构相似那他们就是互相兼容同一个类型(如果不考虑逆变协变不变)博主 dylech30th
@n0099 nominal typing是原罪,structural typing(也可以说ducktype是一种structural typing)从来不会有这个问题,另外subtype当然可以是在structural type system(ducktype)上面,而非必须手动声明
A extends B
这种的。举个例子,函数类型A -> B
之间的subtyping关系就是通过判断类型结构实现的(C <: A B <: D 推出 A -> B <: C -> D
),而不是手动声明A -> B
是C -> D
的子类。博主 n0099
@dylech30th
于是对于
Enumerable.Count()
,有var a = Enumerable.Range(1, 10)
行为和结果完全一致的两个:
Enumerable.Count(a)
a.Count()
我们却要定义后者是adhoc多态而前者不是,即便从类型信息上看两种调用
Count()
的方式都不影响Count()
需要接收一个IEnumerable
类型的参数(不论是以this的形式隐式传入还是显式作为第一个参数传入)才能求他的count(也就是其签名:int Count<TSource> (this System.Collections.Generic.IEnumerable<TSource> source)
)而与此
同时四叶CS硕士PLT中级高手irol阁下
一直认为现代前端娱乐圈壬上壬代表日冕开发组成员镜头工坊创始人伊欧神
和泛银河系格雷科技分部邪恶组织四叶重工炼铜本部叶独群组联合体叶独头子陈意志第三帝国元首深圳uniapp小程序开发者炼铜傻狗橙猫
所热衷的js oop实现之:而在c#扩展方法上看到一个static全局函数却有着一个叫this的符号可用并且this来自参数但查找该扩展方法的所有usage(除非有usage写的是1.风格而不是2.)时却找不到任何入参(如
a.Count()
没有argument)时就不会批判这是反直觉的错误设计了博主 n0099
@n0099 恰恰相反我也可以
合理假设
(同为irol的口头禅)irol女士和此前向我演示如何在scala中写haskell typeclass以实现adhoc多态并指出ts不可能typeclass的土澳应用统计学带手子文科生cn神
在读完阁下的论述后也会将实现为语法糖的c#/kotlin扩展方法抬升到与haskell typeclass和rust trait同为adhoc多态的程度然而按照这样的逻辑链:
web2.0 ugc时代前端老js人
最热衷的原型链污染(如core-js那样的poly fill) monkey patch也是adhoc多态早已自愿退出邪恶组织四叶重工的cpp中级高手上海贵族信安底层壬上壬杨博文阁下也早已道明cpp模板本质上就是对类型信息做查找替换的宏
)博主 dylech30th
@n0099
博主 n0099
@n0099 1.
method overload对于老oop壬上壬而言早已司空见惯习以为常以至于某些java魔怔人就以此为第一刻板科班印象认定所有语言都有且只有基于subtyping的多态实现
而
泛银河系格雷科技分部邪恶组织四叶重工炼铜本部叶独群组联合体叶独头子陈意志第三帝国元首炼铜傻狗橙猫
在正式宣布叶独并彻底清洗其陈意志第三帝国
qq本部与tg分部中的一切四叶人前数天也曾道明:人的刻板影响是很严重的,我早年在四叶主群发幼女图给四叶壬带来了炼铜傻狗的第一印象后即便我再怎么改善每日往群里倾倒p站萌图的幼女成分比例,甚至完全不发幼女图也无法改变他们的刻板印象
文中也进一步指出了:
阁下原文中提到了override in subtpyings吗,我ctrl+f全文检索
重写
只有来自评论区的匹配,而检索override
0匹配博主 dylech30th
@n0099 “这种方法,在调用方式上和重载有几分神似,在实现方式上和接口也有几分神似,那么为什么我会说他是和重载一样的Ad-hoc Polymorphism,而不是和接口一样的Subtyping呢?原因在于类型之间的依赖关系上。”——原文如此。
顺便太多层的评论似乎没法回复了,之后你要回复的话直接发顶层评论吧
博主 n0099
@n0099
以本文中的rust代码片段为例:
可以写出如下js代码片段:
js原型链污染是实现monkey patch(如polyfill)的主要方式:
例如您的目标浏览器是远古libcef版本套壳的国产浏览器甚至IE家族(及其
mshtml.dll
套壳浏览器)那么在这些浏览器中不存在自带的
Array.includes()
方法所以您需要手动往
Array.prototype
上增加这个includes()
方法并在其中手写js实现来模拟高版本浏览器的native实现行为当然这种常见的兼容需求在此前评论中就业已应验:
所以您可以选择直接引入
core-js
那样的npm包来帮您做这些原型链污染,并且他们的js实现基本都能完全模拟浏览器的native实现行为但由于大家早已对无处不在的运行时全局原型链污染带来的问题感到深恶痛绝: https://medium.com/walkme-engineering/jaco-labs-the-most-ridiculous-monkey-patches-weve-seen-6ef5db307bdd
所以后来的现代前端娱乐圈壬上壬们又引入了ponyfill的概念: https://github.com/sindresorhus/ponyfill
博主 n0099
@n0099
php trait在我最初对本文所作的那批评论中的最后一条中就已经指出
的本质只不过是复制粘贴TraitA的body中所有的代码到ClassA之中,所以他才不存在任何类型层面的信息(毕竟其引入trait概念是在php5从java那抄完了其所有传统oo魔怔subtyping思想后再引入,但仍然远远早于php7首次引入typehint之前(因此php5时代不存在任何可以让您来写的类型信息,除了phpdoc那种给壬和静态分析器(php5时代也没几个,psalm也是后来的)看的注释))
博主 n0099
@n0099
阁下讨论的是类型间依赖关系,但并没有对override进行任何提及
如果在阁下修改wp设置中对发表评论时检测深度的值后只能发表更深的评论但仍然无法在主题前端上看到该评论,那么您也可以尝试使用此插件: https://github.com/CKMacLeod/WordPress-Nested-Comments-Unbound
博主 dylech30th
@n0099 重写和接口的subtyping本质上是一样的啊,比如你有类
A
和B
,B
继承自A
并且重写了M
方法,那么你同样可以A a = new A()
和A b = new B()
,此时a
和b
都是A
类型的,但是a.M()
和b.M()
行为不同,这和接口是一个意思博主 dylech30th
@n0099 OK,评论嵌套改到最大10层了
博主 dylech30th
@n0099 因为
this
在函数体和函数签名的语义是不同的啊,在函数签名里表示的意思是这是一个receiver,而且只是作为一个关键字,但是出现在函数体里的时候他却是有operational semantic的,是“出现在函数体内部的有operational semantic的this
反直觉”而不是“有this
就反直觉”。一般来说this
是出现在类定义里面的,而不会出现在这个类的对象里,而一个函数定义更像是声明一个“函数类型的对象”而非“一个函数类型”,因此this
出现在这里显得反直觉——个人意见博主 n0099
@dylech30th
请问阁下所指的类定义是否包括所有类方法的body?在非静态类方法中使用隐式的this引用不是基本功能?
我此前就已经约束了范围:
按照java培训班指导的传统oo思维八股文,任何带有static修饰的方法都不存在this可用
但在扩展方法中却必须得有this,也就是是
static方法中的this反直觉
而不是有this反直觉
博主 dylech30th
@n0099 正是因为“函数内的
this
反直觉”,所以才要用class
,getter/setter的写法虽然令人痛恨但是至少不反直觉啊博主 n0099
@dylech30th 上一评论中阁下的回复曾经指出:
请问您所指的函数是类成员中的方法还是孤立存在的全局函数?据我所知在java/c#中都无法声明原本来自c但后来成为脚本语言人最爱的全局函数,哪怕能也都是语法糖给您套了一个编译时生成类其类名随机字符串
博主 dylech30th
@n0099 全局孤立的函数,很明显类似
C#
或者Java
的类中的函数中的this
同样并不指向函数本身而是指向定义函数的类型,此时的this
更像一个被函数引用的外部变量博主 dylech30th
@n0099 说“扩展函数是adhoc”的原因是1. 多态的类型之间没有依赖关系。2. 同一个类型上的同一个函数调用可以有不同的行为,比如你可以定义
PrintType(this string str)
和PrintType(this int i)
,他们的行为是不同的,但是对于caller来说,他们是名字相同的函数,行为不同的原因只是因为receiver不同,同时多态的类型(int
和string
)之间又没有依赖关系,因此是adhoc博主 n0099
@dylech30th 我并没有说我不认同阁下在本文中曾经指出的
扩展方法是adhoc多态的一种实现
这一观点:但我同样想要表达不能因为
就忽略了
然后推导出
却同时又忽视了
等上一评论中提到的4种甚至9种alternative ad-hoc polymorphism(疑似仏国白左仏皇irol女士最痛恨的alt-right)
博主 dylech30th
@n0099 没错啊,一个语言可能有除了overload之外的adhoc,它的实现也确实可能是语法糖,这两者是不冲突的
博主 n0099
@dylech30th
我猜阁下在这里所指的
同一个类型
是声明static扩展方法所在的那个类因为根据上一评论中的回复:
那么阁下完全可以应用老c人和弱类型动态脚本语言的思维直接忽略掉存放扩展方法的那个类,而是将static扩展方法视作全局可用的(实践中大多数人也都global using了System.Linq以便使用其带来的一大堆针对IEnumerable接口类型的扩展方法: https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable )
这时候您会发现调用同一个全局函数却发生了不同的行为本就是一直存在的,如同没人(类型系统,编译器,运行时)会阻止您把全局函数/类方法的形参类型约束为object然后在body里不断if typeof来判断该实参的类型并disptach不同的行为,尽管这种上一评论的回复中所提到的
穷人的adhoc
实现形式早已被我们的CS硕士irol阁下批倒批臭博主 dylech30th
@n0099 呸,这里说错了,是“不同类型上调用同一个函数可以有不同的行为”,当时打字打错了
博主 dylech30th
@n0099 有关py的
map(len, list)
的问题:这问题确实能用typeclass解决博主 n0099
@dylech30th https://medium.com/@decorator_factory/typeclasses-in-python-3c21ae9327af
https://sobolevn.me/2021/06/typeclasses-in-python
https://docs.python.org/3/library/functools.html#functools.singledispatch
这就是pythonic吗
博主 扫地机器人🤖
@n0099 我一直认为通用运算符只能设计在一个封闭的模块内. 比如我打算设计一套通用的数学计算系统, 我将math+这个符号设为运算符号, 这样我就可以做到(math+ ComplexNumber Polynomial), 让类型系统(动态类型可以把一切都变成一个Pair模拟一个类型系统)取代自己找名称这种"官僚主义".
但是这只能局限于一个模块内, 你让语言内的+能处理一切, 那(+ string polynomial)这种东西是什么, 一个字符串和一个多项式相加是啥意思? 所以大多数Lisp实现没有len()这种东西, (length lst)处理的lst就一定是个List不是别的东西. (string-length str)处理的是String不是别的东西. +只能处理数学有关的东西, 你把字符串和数字加起来? 那你得回答我(+ "11" 1)得到的是"12"还是"111"或者是一个单纯的数字12. 因为我觉得这几个都说的道理.
不要试图让你的语言能容纳下整个世界. 这样做你的语言就能拿来形容一群长了三个头的人和能够容纳他们的世界, 这种世界会在你打算给他们戴上口罩的一瞬间崩溃.
对了, DC姐姐能不能多加一些B站的表情, 我想要那个抱爱心的. :bbd:
博主 dylech30th
@扫地机器人🤖 呜呜呜DC姐姐直接套用的模板,不知道怎么加表情
博主 扫地机器人🤖
@dylech30th
博主 扫地机器人🤖
@扫地机器人🤖
用markdown的图片语法不行喵
博主 n0099
php的trait和late static binding,以及java8引入的interface default method implement是否也属于adhoc多态?
可以在php中重新实现此rust例子: https://3v4l.org/Hdmot
但在最后对check_equiv的调用中肉眼可见123456这几个int并不与Eq类型兼容:
这意味着对于上述php代码中的类型约束,您只能写出无限嵌套永不结束的
new Tuple2或3(new Tuple2或3(...))
,使得Tuple2和3类几乎没有任何实际用途即便试图写一个实现了Eq的类来包装int使得其能够放进Tuple2或3的ctor中: https://3v4l.org/2T5dP
我们也会发现我们不可能实现Eq trait中抽象的equiv方法,因为根本没有正确的对应类型能够填入equiv实现函数签名:
进一步的我们可以学习java8精神给Eq::equiv()加上默认实现: https://3v4l.org/QKBbK
然后
但人生自古谁无死,不幸地,php的类型系统无法将任何实现了一个trait的类视作是trait的subtyping,因此使得您不可能类型约束任何东西为某个trait,因为您不可能在运行时创建任何东西使其是某个trait的subtype:
而堆栈溢出oo带师们早已道明真相:
https://stackoverflow.com/questions/73172902/how-to-type-hint-a-trait-when-using-the-trait-to-realize-multi-inheritance
https://stackoverflow.com/questions/14157586/php-type-hinting-traits
博主 1846913566
我在 GraiaProject/Avilla 中实现了一个 Python 的 typeclass,这个实现虽不包含一些比较重要的特性,但可以通过现有的类型检查并获得完整的 IDE 支持(使用 Pylance/Pyright)。
由于在原实现中我因为特殊的需求而没有使用设计中的原实现(局限于 Pyright 实现的类型检查的 Features),这里的代码并不适用于上述的项目。。
这直接引用了 Trait 声明本身,为了通过类型检查,舍弃了隐式查询虚函数表的功能。但这效果仍是出色的。
博主 1846913566
@1846913566 wait 我有一个天才的想法 来绕开hkt对trait做类型信息附加 让我先试试看