Rust的设计哲学:Trait与多态

  在2019年刚刚接触Rust的时候(那会每周例行去医院光公交车要坐一个小时,就拿着手机在车上看,好怀念),就会看到Rust的官方文档里用Java等语言的interface来类比自己的trait,并且还会声明“这只是一种比喻,traitinterface其实有着相当大的区别”,不仅Rust如此,类似Scala这样的语言也会用这种类比来描述自己的trait特性,自那时起,“traitinterface到底有什么区别”这个问题就困扰了我相当一段时间。恰巧前一段时间看到某群群友讨论Rust的impl...for...到底是什么东西;借着这个机会,来顺便讨论一下Rust中trait的设计哲学。

多态

  讨论一个语言中某个特性的设计哲学,必然要涉及到一些前置知识,如果仅仅通过给出例子来描述,恐怕看一万个 traitinterface的例子也难解其中的区别(我当初没有学过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 RectangleDrawing TriangleDrawing 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这样一个函数,可以用IntegerDoubleCharacter,等等任何你想的类型去实例化它,从这个角度来看,他是通用的(Universal),而接口则不享有这个属性,回过头来看上面的doDraw函数,虽然也能做到传进去若干种不同的类型,诸如RectangleTriangle等不一而足,但是他们都被限制为是Drawable接口的实现类,也就是说,他们与Drawable接口之间存在一个非常明显的关系——父子类型的关系,并且由于只有Drawable的实现类才能适用,doDraw显然没有Universal的属性:你不可能给doDraw传进去一个Integer类型的对象,能用来“实例化”doDraw的类型,被限制在了“Drawable的子类”这样一个范围内。

Parametricity

  Parametric Polymorphism,从这个名字中可以引申出它的一个重要属性——Parametricity,Parametricity是所有泛型函数享有的一个属性——不管用什么样的类型去实例化他们,这些函数的表现区别仅仅体现在类型的不同上,而行为上则完全相同,例如无论你用什么类型去实例化 doubleFun,他所做的都是“把f应用在x上两次”,不可能因为你应用在了不同的类型上就变成了应用三次或者额外多打印几句话,doubleFun用不同类型实例化的唯一区别只是fx的类型随之不同。

实际上,如果我们把函数都看做纯函数(不考虑函数外部的环境,没有副作用),那么我们甚至可以根据函数的类型来写出函数的实现,例如唯一能满足类型$\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;
}

然后为他提供两个 u32char上的impl

impl Stringify for u32 {
    fn stringify(&self) -> String {
        format!("{}", &self)
    }
}

impl Stringify for char {
    fn stringify(&self) -> String {
        format!("{}", &self)
    }
}

然后我们就可以分别给 u32char调用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,那么我们就无法为tuple2tuple3两个不同类型的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_tuplecheck_equiv_strcheck_equiv_u32等等针对每个类型特化的比较函数。从这里可以看到,Non-Parametricity与Universality缺一不可,而这也正是大部分现代多范式语言采用的策略:在诸如Java/C#等语言中,达成这一点靠的是基于接口/父子类型的Subtyping和基于泛型的Parametric;而在Rust/Haskell这些语言中,则是基于trait/impl这样的Ad-hoc Polymorphism和同样是泛型的Parametric,只不过在Haskell中,相比起trait,它有一个更加响亮的名字——Typeclass;还有一些语言,比如Scala,同时支持三种多态:既能用traitextends实现Subtyping,也能用泛型实现Parametric,还能用泛型traitgivenusing结合在一起实现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三板斧和各种设计模式不放,不如拓宽一下视野,尝试尝试旁征博引,授读者以更加灵活的思维方式,更加广袤,才能正确、有效的发挥现代编程语言的全部潜力。

56 条回复

  1. 如果没有Ad-hoc的trait以及impl,那么我们就无法为tuple2和tuple3两个不同类型的Eq实现定义不同的行为——它们将会不得不一个实现Eq2一个实现Eq3这两个不同的类型来分别定义两个元素的元组和三个元素的元组的比较逻辑,这显然失去了多态的意义

    疑似以下ts实现:

    type GConstructor<T = {}> = new (...args: any[]) => T; // copied from https://www.typescriptlang.org/docs/handbook/mixins.html
    
    type Eq = {equiv: (lhs: Eq, rhs: Eq) => boolean};
    
    type Tuple2<K, V> = {
        key: K,
        value: V
    }
    
    type Tuple3<K, V, C> = {
        key: K,
        value: V,
        column: C
    }
    
    function tuple2WithEq<TBase extends GConstructor<Tuple2<K, V>>, K extends Eq, V extends Eq>(Base: TBase) {
        return class tuple2 extends Base {
            equiv(lhs: this, rhs: this): boolean {
                return lhs.key.equiv(lhs.key, rhs.key)
                    && lhs.value.equiv(lhs.value, rhs.value);
            }
        }
    }
    function tuple3WithEq<TBase extends GConstructor<Tuple3<K, V, C>>, K extends Eq, V extends Eq, C extends Eq>(Base: TBase) {
        return class tuple3 extends Base {
            equiv(lhs: this, rhs: this): boolean {
                return lhs.key.equiv(lhs.key, rhs.key)
                    && lhs.value.equiv(lhs.value, rhs.value)
                    && lhs.column.equiv(lhs.column, rhs.column);
            }
        }
    }
    
    function checkEquiv<T extends Eq>(lhs: T, rhs: T): boolean {
        return lhs.equiv(lhs, rhs);
    }

    https://www.typescriptlang.org/play?#code/C4TwDgpgBA4gwgewHYGdgCcCuBjYD0A8AKlALxQDeAvgHxlRIQDuUAFAHScCG6A5igC4oXJCADaAXQCUZOkQDcUAPRKo2BGACWEACZQAZugQBbKAAtgwMIJVM77UJBTZ0msMAA2I3u3y8lOgjYKEpmIjoARggIANZKxpoAHpqo7BbGHgCwAFA5jtAAogCO9BQQRZiaAG5CrB5mglDFADRQ6A1CxTKkdFEIHhAiVPI5eeDQRJhgAwBMBADSrQBqdOQUOVCbUDEQIEKLG1tVXB6YEEJLOVSj2flQk9MQAMwLy61wq5SHmzt7UAfZLZQY6nc5QJbNb5qfqYYxIIRwK43fSYJC4TTIKDAKazADqmmAZmKxAAQlwUNAIIlgBAkDoULBEKgMDg8IQHrNXuCaDRWvMoFSaXSGS1wQLqbT6U0ijRWGSKUIiPKIDJ1oCtugINj0Eg1F4UAzsY8ZuKhVLlV91UDNuVKlU6h0sWZNChWu1GoSXVIhH0BiJLdbA20tZgdVB6ih2L92Lbqg7I783Q0o7spFCg9aAGSZ8PJkFnGMVOMR9j5iBJyNlqQjK3W661+v1lFo4AY3VGgZPfGE4lK8mUiXCxnINBYXD4Yg455ciFQD68-6myUiorLJdD0VwddS4qy5WK5WqqGa7W67D6w1Tp7bhkWtUZ2P2iNCT2utqO1-eqC+wa6+8ZjUQzDEto0feMUxACsILTWsAK2bNc0rE4CzAksyygqt0zgqAEJLdRTjhQs7XA-DYSQKDSLhassM2esgUbZFUXRTFsDMCBsBiYo7WIG9pVlZ97grRUvx-f1-02E9Q11EtUIaCtqOyesgA

    1. 我草,我今天上线咋发现你回复了这么多,你别急等我慢慢看,这评论区的md语法的样式都被干烂了

      1. 另外所有的完整的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/

        1. 我不懂这个啊,我搭blog是直接套模板的

          1. 套模板:直接复制粘贴奥利金德jvm底层壬上壬反fp代表irol的眼中钉mc圈皇帝梨木利亚的仇敌虾米神的部落格之 https://chariri.moe
            建议立即致电隔壁 https://syzx.me 站长缘之空吧资源贩子黑兔给您亲自指挥亲自部署php82赋能wp

            1. 草,https://chariri.moe是CJ的blog,不是虾米的

          2. irol女士经典分不清奥利金德众神之皇帝LG dc神 CFPA会员Cyl18a CJ 当妈 jvm底层壬上壬虾米

  2. 参数多态性,他的核心在于“类型也可以被当作参数”,也就是说,你可以写一段代码,但是把这段代码中的类型空出来,用一个占位符表示(就像函数的形式参数是实际参数的占位符一样),接着在使用的时候,再去用某个具体的类型去实例化它。在多数语言中,这种特性被称为泛型

    不如说在类型系统的层面看来任何函数本就只有类型信息而不存在body中实现的逻辑,也就是后文提及的如果我们把函数都看做纯函数(不考虑函数外部的环境,没有副作用),那么我们甚至可以根据函数的类型来写出函数的实现
    因此在类型层面看来

    public static <T> T doubleFun(Function<T, T> f, T x) {
        return f.apply(f.apply(x))
    }

    type doubleFunc<T> = (f: (a: T) => T, x: T) => T;

    而如果手动把T特化成int这样的具体类型:

    public static int doubleFun(Function<int, int> f, int x) {
        return f.apply(f.apply(x))
    }

    那么类型上

    type 特化了的doubleFunc = doubleFunc<number>;
    等效于
    type 特化了的doubleFunc = (f: (a: number) => number, x: number) => number;

    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

    1. “不如说在类型系统的层面看来任何函数本就只有类型信息而不存在body中实现的逻辑”——这一点并不蕴含了任何类型系统都享有Parametricity,类似forall X. X → X这种类型之所以能直接写出他的实现是因为类型X是被抹除的,本身不蕴含任何其他有价值的信息

      1. 然而以四叶CS硕士PLT中级高手irol阁下经常复读的对奥利金德人的刻板印象为例:
        forall 奥利金德人. is魔怔人 -> true
        我们只能从中推导出is魔怔人是恒真式T -> true

        1. 我咋没看懂你这个例子是写的啥

          1. 就是所有奥利金德人都是魔怔人的意思
            这句话最早由irol阁下于20年提出,从此之后我每次在她面前提及奥利金德她都会下意识的复读这句话

            1. 如果你对这个感兴趣的话,可以了解一下Universal Introduction和Universal Elimination,也就是Natural Deduction中有关全称量化的推理规则

          2. 与此同时:我还在研读奥利金德皇帝lg神于22年写下的非理性哲学分析: https://lasm.dev/2022/09/28/first-order-logic-a-semiotics-approach/

            1. 里面其实有关我的那部分理解有错误的地方,正确的解释在那篇谈罗素的blog里,就是有关为什么量化的域不能是无限的那个问题

            2. 我提这个的原因是,在类似Natural Deduction里,你想要推导出一个全称量化的句子是有前提条件的,这个前提条件就是Universal Introduction

      2. 而以本文中的doubleFun = (T, T) -> T -> T为例您也可以写出直接返回第二层的参数而不是把第一层参数拿去apply两次,毕竟都能满足最终返回类型是T的约束

        1. 你可以只看签名就知道doubleFun能有几种写法,parametricity的重点是同一个多态函数的所有实例表现都是一致的,也就是doubleFun<int>doubleFun<String>的行为都是一样的

          1. 然而我完全可以在doubleFun的实现中做任何事,甚至是导致永不停机的死循环,只要在这个死循环退出(而这是不可能的)之后能够返回一个类型T的东西就能符合函数签名

            一个多态函数的所有实例表现都是一致

            这应该是由签名唯一性来约束,也就是 父类-函数名称-参数列表(每个参数的类型,但不考虑名字)-可选的返回值类型 (有些语言衡量签名唯一时不考虑返回值类型)构成的一个tuple
            而如果不具有唯一性也就是编译器允许有着多个签名完全相同的函数/类方法存在那就无法保证只根据类型信息就能保证其类型所代表的行为(函数body)的唯一性
            据我所知没有编译器允许您声明复数个相同签名的函数/类方法
            这就像dc神阁下在另一篇文章 https://sora.ink/archives/1631 中所提到的:

            非虚方法全局只有一个,不存在继承重写的问题

            1. 你能在doubleFun里做其他事情的大前提是 1:如果你要在doubleFun里干其他事情比如print意味着你引用了外部环境,而我们说的例子里默认doubleFun是孤立的,纯的函数。2:如果你要写一个不停机的死循环,那么你需要支持递归——这一点在简单类型λ演算中是不成立的,只有支持递归类型的时候才能支持递归,原因在于不动点组合子fix需要递归类型:

  3. 各种教程,书籍与课程与其抓着已经被嚼烂的OOP三板斧和各种设计模式不放

    您可以品鉴一下现代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

  4. 甚至还有本文中虽然没有提到,但是实际上同样属于Ad-hoc的扩展(Extensions),在Swift/Scala中,它们用extension关键字声明,而C#/Kotlin虽然没有完善的对扩展的支持,却也支持扩展函数这种略弱于扩展的同种特性,有兴趣的话,可以思索一下为什么扩展函数这样的特性同样属于Ad-hoc Polymorphism的范畴。

    四叶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

    认为 len(x)和 x.len()不是一种含义会造成明显的不一致,以至于有不少人认为这两种语法的含义干脆合并得了,定义一个就能两用。但这样就有个灵魂拷问:既然允许 len(x),为啥还要发明 x.len(),浪费一个不同的语法?然后什么 customization point 之类的妖魔鬼怪上来凑一脚,不忍直视……
    所谓“前缀符号更可读,简单胜过复杂”其实倒并非没有道理。
    仔细还原这类语言的语义规则,*this 被作为隐藏的参数,但这其实还有个更一般的形式——静态环境(在静态语言中通常被处理成不可见的符号表)。
    隐藏显式名称查找的语法,都写成前缀,比如 Lisp 语法:(len x)——就能发现完全是一回事。是不是要依赖 this/self ,只不过 len 到底是从哪里来的罢了,这完全可以作为实现细节,而实现这里的简单。

    https://www.v2ex.com/t/903396#r_12482208

    回到前面的问题:为什么是 len(x) ,而不是 x.len(x),这根源于 Python 的什么设计思想呢?
    Python 之父 Guido van Rossum 曾经解释过这个问题( #TODO: add link ),有两个原因:

    对于某些操作,前缀符比后缀更好读——前缀(和中缀)表示法在数学中有着悠久的历史,其视觉效果有助于数学家思考问题。我们可以简单地把公式 x(a + b) 重写成 xa + x*b ,但同样的事,以原生的面向对象的方式实现,就比较笨拙。
    当读到 len(x) 时,我就 知道 这是在求某对象的长度。它告诉我了两点:返回值是一个整数,参数是某种容器。但当读到 x.len() 时,我必须事先知道某种容器 x ,它实现了一个接口,或者继承了一个拥有标准 len() 方法的类。我们经常会目睹到这种混乱:一个类并没有实现映射( mapping )接口,却拥有 get() 或 keys() 方法,或者某些非文件对象,却拥有一个 write() 方法。
    解释完这两个原因之后,Guido 还总结成一句话说:“I see ‘len’ as a built-in operation ”。这已经不仅是在说 len() 更可读易懂了,而完全是在拔高 len() 的地位。
    这就好比说,分数 ½ 中的横线是数学中的一个“内置”表达式,并不需要再实现什么接口之类的,它自身已经表明了“某数除以某数 ”的意思。不同类型的数(整数、浮点数、有理数、无理数…)共用同一个操作符,不必为每类数据实现一种求分数的操作。
    优雅易懂是 Python 奉行的设计哲学 ,len() 函数的前缀表达方式是最好的体现。
    我概括为:为什么前缀 python 的前缀 len()比后缀 len()更好呢,因为 python 的创始人认为前缀更好(原因有二),所以“I see ‘len’ as a built-in operation ”,所以 python 中的 len() 函数的前缀表达方式是最好的体现。

    https://www.v2ex.com/t/903396#r_12483206

    比如 map(X, list) ,对于 list 内的元素是任意类型的情况,这个里面 X 填什么?如果设计的是 .len() 的话,用类方法 str.len, list.len, dict.len 都不符合,要么就要写 lambda 了,而 GvR 本身也不是很喜欢 lambda. 而用函数的话,X 就可以直接写 len ,从这点上 len() 比 .len() 好一点。

    建议立即以irol最爱的haskell和cn神最爱的scala中的adhoc多态实现:typeclass概念为抓手赋能python

    1. 他的确是语法糖,但是我认为我们在讨论这个问题的时候应当去观察行为而不是其实现,就好比java的泛型也可以被称为语法糖,他甚至在进入JVM内部之前就销声匿迹了,但是从类型系统上看,他依然是一个标准的泛型,其本质是泛型,和其实现是语法糖,这两点之间并不冲突

      1. 就好比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

        Use the IsGenericTypeDefinition property to determine whether you can create new types from the current type. If the IsGenericTypeDefinition property returns true, you can call the MakeGenericType method to create new generic types.

        但是从类型系统上看,ts依然是一个标准的system F,其本质比js无类型强,和其最终产物还是js,这两点之间并不冲突

        1. typescript可不能说是标准的System F,标准的System F只包含多态类型不包含OOP和类似b ? int : string这种类型,typescript核心类型系统严格来说是带拓展的System F-Omega-Sub,也就是多态类型+Type Operator+子类型

          1. 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中只要结构相似那他们就是互相兼容同一个类型(如果不考虑逆变协变不变)

            1. 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 -> BC -> D的子类。

      2. 观察行为而不是其实现

        于是对于Enumerable.Count(),有var a = Enumerable.Range(1, 10)
        行为和结果完全一致的两个:

        1. Enumerable.Count(a)
        2. a.Count()

        我们却要定义后者是adhoc多态而前者不是,即便从类型信息上看两种调用Count()的方式都不影响Count()需要接收一个IEnumerable类型的参数(不论是以this的形式隐式传入还是显式作为第一个参数传入)才能求他的count(也就是其签名int Count<TSource> (this System.Collections.Generic.IEnumerable<TSource> source)
        而与此同时四叶CS硕士PLT中级高手irol阁下一直认为现代前端娱乐圈壬上壬代表日冕开发组成员镜头工坊创始人伊欧神泛银河系格雷科技分部邪恶组织四叶重工炼铜本部叶独群组联合体叶独头子陈意志第三帝国元首深圳uniapp小程序开发者炼铜傻狗橙猫所热衷的js oop实现之:

        一切Function都继承自根类Object,所以function可以有this

        function a() {
        this.b = 1;
        return b;
        }
        console.log(a()); // 输出1

        是完全错误的设计,她个人选择使用es6引入的class语法糖:

        class A {
        b = 0;
        constructor() { this.b=1; }
        getB() { return this.b; }
        }
        console.log((new A()).getB());

        来写她最痛恨的java风格的g/setter而不是直接这样手搓反直觉的函数this成员

        而在c#扩展方法上看到一个static全局函数却有着一个叫this的符号可用并且this来自参数但查找该扩展方法的所有usage(除非有usage写的是1.风格而不是2.)时却找不到任何入参(如a.Count()没有argument)时就不会批判这是反直觉的错误设计了

        1. 恰恰相反我也可以合理假设(同为irol的口头禅)irol女士和此前向我演示如何在scala中写haskell typeclass以实现adhoc多态并指出ts不可能typeclass的土澳应用统计学带手子文科生cn神在读完阁下的论述后也会将实现为语法糖的c#/kotlin扩展方法抬升到与haskell typeclass和rust trait同为adhoc多态的程度
          然而按照这样的逻辑链:

          • 本文中提到的method overload的作为oop单继承思维下的受限产物(无法跨类型定义)也是adhoc多态
          • 早已被irol女士批倒批臭的web2.0 ugc时代前端老js人最热衷的原型链污染(如core-js那样的poly fill) monkey patch也是adhoc多态
          • 本质复制粘贴复用代码的copycat之php trait也是adhoc多态(如同此前评论中提到的早已自愿退出邪恶组织四叶重工的cpp中级高手上海贵族信安底层壬上壬杨博文阁下也早已道明cpp模板本质上就是对类型信息做查找替换的宏
          • 甚至无/弱类型语言中写一个泛用函数然后不断typeof判断根据不同的参数类型来做出不同的行为(lodash的内部实现)也是穷人的adhoc多态
            1. 函数重载的确是非常正宗的adhoc,但是subtyping的重写(override)不是,因为涉及到了依赖关系,文中提到了
            2. 不懂js
            3. 不懂php
            4. 你可以说这就是“穷人的adhoc多态”,因为他的行为和函数重载是一样的
          1. 1.
            method overload对于老oop壬上壬而言早已司空见惯习以为常以至于某些java魔怔人就以此为第一刻板科班印象认定所有语言都有且只有基于subtyping的多态实现
            泛银河系格雷科技分部邪恶组织四叶重工炼铜本部叶独群组联合体叶独头子陈意志第三帝国元首炼铜傻狗橙猫在正式宣布叶独并彻底清洗其陈意志第三帝国qq本部与tg分部中的一切四叶人前数天也曾道明:人的刻板影响是很严重的,我早年在四叶主群发幼女图给四叶壬带来了炼铜傻狗的第一印象后即便我再怎么改善每日往群里倾倒p站萌图的幼女成分比例,甚至完全不发幼女图也无法改变他们的刻板印象
            文中也进一步指出了:

            多态是这样的,对吧?NO。多态是一个涵盖范围相当广泛的词,这个词并不仅仅意味着OOP这种形式的多态,事实上,还有另外两种多态的表现形式,如果用大部分人更为熟悉的术语来描述的话,它们分别是——泛型(Generics)与重载(Overloading)
            文中提到了

            阁下原文中提到了override in subtpyings吗,我ctrl+f全文检索重写只有来自评论区的匹配,而检索override0匹配

            1. “这种方法,在调用方式上和重载有几分神似,在实现方式上和接口也有几分神似,那么为什么我会说他是和重载一样的Ad-hoc Polymorphism,而不是和接口一样的Subtyping呢?原因在于类型之间的依赖关系上。”——原文如此。
              顺便太多层的评论似乎没法回复了,之后你要回复的话直接发顶层评论吧

            1. 关于js特色之基于原型链污染的monkey patch

            以本文中的rust代码片段为例:

            impl Stringify for u32 {
                fn stringify(&self) -> String {
                    format!("{}", &self)
                }
            }
            
            impl Stringify for char {
                fn stringify(&self) -> String {
                    format!("{}", &self)
                }
            }

            可以写出如下js代码片段:

            Number.prototype.stringify = function () { // js没有任何int/uin只有本质ieee754的number
                return this.toString();
            }
            String.prototype.stringify = function () { // js没有char所以您可以假设string是rust的char
                return this.toString();
            }

            js原型链污染是实现monkey patch(如polyfill)的主要方式:
            例如您的目标浏览器是远古libcef版本套壳的国产浏览器甚至IE家族(及其mshtml.dll套壳浏览器)
            那么在这些浏览器中不存在自带的Array.includes()方法
            所以您需要手动往Array.prototype上增加这个includes()方法并在其中手写js实现来模拟高版本浏览器的native实现行为
            当然这种常见的兼容需求在此前评论中就业已应验:

            当然npm细粒度包魔怔人在一个leftpad函数也是包的思想指引下早已npm publish了十万甚至九万个运行时类型检测和常用带类型约束的数据结构库轮子:

            所以您可以选择直接引入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

            1. 本质复制粘贴复用代码的copycat之php trait也是adhoc多态(如同此前评论中提到的早已自愿退出邪恶组织四叶重工的cpp中级高手上海贵族信安底层壬上壬杨博文阁下也早已道明cpp模板本质上就是对类型信息做查找替换的宏)

            php trait在我最初对本文所作的那批评论中的最后一条中就已经指出

            class ClassA {
                use TraitA
            ...

            的本质只不过是复制粘贴TraitA的body中所有的代码到ClassA之中,所以他才不存在任何类型层面的信息(毕竟其引入trait概念是在php5从java那抄完了其所有传统oo魔怔subtyping思想后再引入,但仍然远远早于php7首次引入typehint之前(因此php5时代不存在任何可以让您来写的类型信息,除了phpdoc那种给壬和静态分析器(php5时代也没几个,psalm也是后来的)看的注释))

          2. “这种方法,在调用方式上和重载有几分神似,在实现方式上和接口也有几分神似,那么为什么我会说他是和重载一样的Ad-hoc Polymorphism,而不是和接口一样的Subtyping呢?原因在于类型之间的依赖关系上。”——原文如此。

            阁下讨论的是类型间依赖关系,但并没有对override进行任何提及

            顺便太多层的评论似乎没法回复了,之后你要回复的话直接发顶层评论吧
            wp从原理上将是不限制评论嵌套深度的,在 /wp-admin/options-discussion.php 中可以改变 发表评论时对当前评论嵌套深度进行判断并允许或阻止评论发表的深度值
            https://developer.wordpress.org/reference/hooks/thread_comments_depth_max/
            https://wordpress.stackexchange.com/questions/257050/custom-wp-comments-query-with-nested-comments-possible-hierarchy-depth
            但尽管从core上看wp本身不限制嵌套深度,然而阁下所使用的主题可能没有正确实现对无限嵌套层级评论的正确html tag渲染: https://developer.wordpress.org/themes/template-files-section/partial-and-miscellaneous-template-files/comment-template/ 如同我所见的许多主题也都没有正确实现wp menuitem(通常显示在navbar菜单)的无限嵌套层级

            如果在阁下修改wp设置中对发表评论时检测深度的值后只能发表更深的评论但仍然无法在主题前端上看到该评论,那么您也可以尝试使用此插件: https://github.com/CKMacLeod/WordPress-Nested-Comments-Unbound

            1. 重写和接口的subtyping本质上是一样的啊,比如你有类ABB继承自A并且重写了M方法,那么你同样可以A a = new A()A b = new B(),此时ab都是A类型的,但是a.M()b.M()行为不同,这和接口是一个意思

            2. OK,评论嵌套改到最大10层了

        2. 因为this在函数体和函数签名的语义是不同的啊,在函数签名里表示的意思是这是一个receiver,而且只是作为一个关键字,但是出现在函数体里的时候他却是有operational semantic的,是“出现在函数体内部的有operational semantic的this反直觉”而不是“有this就反直觉”。一般来说this是出现在类定义里面的,而不会出现在这个类的对象里,而一个函数定义更像是声明一个“函数类型的对象”而非“一个函数类型”,因此this出现在这里显得反直觉——个人意见

          1. 一般来说this是出现在类定义里面的,而不会出现在这个类的对象里

            请问阁下所指的类定义是否包括所有类方法的body?在非静态类方法中使用隐式的this引用不是基本功能?

            而不是“有this就反直觉”

            我此前就已经约束了范围:

            一个static全局函数却有着一个叫this的符号可用并且this来自参数

            按照java培训班指导的传统oo思维八股文,任何带有static修饰的方法都不存在this可用
            但在扩展方法中却必须得有this,也就是是static方法中的this反直觉而不是有this反直觉

        3. 正是因为“函数内的this反直觉”,所以才要用class,getter/setter的写法虽然令人痛恨但是至少不反直觉啊

          1. 上一评论中阁下的回复曾经指出:

            一个函数定义更像是声明一个“函数类型的对象”而非“一个函数类型”,因此this出现在这里显得反直觉

            请问您所指的函数是类成员中的方法还是孤立存在的全局函数?据我所知在java/c#中都无法声明原本来自c但后来成为脚本语言人最爱的全局函数,哪怕能也都是语法糖给您套了一个编译时生成类其类名随机字符串

            1. 全局孤立的函数,很明显类似C#或者Java的类中的函数中的this同样并不指向函数本身而是指向定义函数的类型,此时的this更像一个被函数引用的外部变量

        4. 说“扩展函数是adhoc”的原因是1. 多态的类型之间没有依赖关系。2. 同一个类型上的同一个函数调用可以有不同的行为,比如你可以定义PrintType(this string str)PrintType(this int i),他们的行为是不同的,但是对于caller来说,他们是名字相同的函数,行为不同的原因只是因为receiver不同,同时多态的类型(intstring)之间又没有依赖关系,因此是adhoc

          1. 我并没有说我不认同阁下在本文中曾经指出的扩展方法是adhoc多态的一种实现这一观点:

            甚至还有本文中虽然没有提到,但是实际上同样属于Ad-hoc的扩展(Extensions),在Swift/Scala中,它们用extension关键字声明,而C#/Kotlin虽然没有完善的对扩展的支持,却也支持扩展函数这种略弱于扩展的同种特性,有兴趣的话,可以思索一下为什么扩展函数这样的特性同样属于Ad-hoc Polymorphism的范畴。

            但我同样想要表达不能因为

            1. 扩展方法给某语言增加了比overload更加自由的adhoc多态

            就忽略了

            1. 扩展方法在某语言中的实现是编译时转译语法糖,类似此前v2ex人讨论py时提及的 https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax

            然后推导出

            合理假设(同为irol的口头禅)irol女士和此前向我演示如何在scala中写haskell typeclass以实现adhoc多态并指出ts不可能typeclass的土澳应用统计学带手子文科生cn神在读完阁下的论述后也会将实现为语法糖的c#/kotlin扩展方法抬升到与haskell typeclass和rust trait同为adhoc多态的程度

            却同时又忽视了

            早已被irol女士批倒批臭的web2.0 ugc时代前端老js人最热衷的原型链污染(如core-js那样的poly fill) monkey patch也是adhoc多态

            等上一评论中提到的4种甚至9种alternative ad-hoc polymorphism(疑似仏国白左仏皇irol女士最痛恨的alt-right)

            1. 没错啊,一个语言可能有除了overload之外的adhoc,它的实现也确实可能是语法糖,这两者是不冲突的

          2. 同一个类型上的同一个函数调用可以有不同的行为

            我猜阁下在这里所指的同一个类型是声明static扩展方法所在的那个类
            因为根据上一评论中的回复:

            据我所知在java/c#中都无法声明原本来自c但后来成为脚本语言人最爱的全局函数

            那么阁下完全可以应用老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阁下批倒批臭

            1. 呸,这里说错了,是“不同类型上调用同一个函数可以有不同的行为”,当时打字打错了

    2. 有关py的map(len, list)的问题:这问题确实能用typeclass解决

    3. 我一直认为通用运算符只能设计在一个封闭的模块内. 比如我打算设计一套通用的数学计算系统, 我将math+这个符号设为运算符号, 这样我就可以做到(math+ ComplexNumber Polynomial), 让类型系统(动态类型可以把一切都变成一个Pair模拟一个类型系统)取代自己找名称这种"官僚主义".
      但是这只能局限于一个模块内, 你让语言内的+能处理一切, 那(+ string polynomial)这种东西是什么, 一个字符串和一个多项式相加是啥意思? 所以大多数Lisp实现没有len()这种东西, (length lst)处理的lst就一定是个List不是别的东西. (string-length str)处理的是String不是别的东西. +只能处理数学有关的东西, 你把字符串和数字加起来? 那你得回答我(+ "11" 1)得到的是"12"还是"111"或者是一个单纯的数字12. 因为我觉得这几个都说的道理.
      不要试图让你的语言能容纳下整个世界. 这样做你的语言就能拿来形容一群长了三个头的人和能够容纳他们的世界, 这种世界会在你打算给他们戴上口罩的一瞬间崩溃.

      我得到一个任务, 写一个能给所有人戴上口罩的程序.
      (foreach wearMask everyone)我觉得这样就行.
      我去, 怎么有三个头的人, 得改改.
      我去, 怎么还有没头的.

      对了, DC姐姐能不能多加一些B站的表情, 我想要那个抱爱心的. :bbd:

      1. 呜呜呜DC姐姐直接套用的模板,不知道怎么加表情

          1. [img]https://images.weserv.nl/?url=https://s2.loli.net/2023/01/03/aK6oUGcMgA4kr7B.png[/img]用markdown的图片语法不行喵

  5. php的traitlate static binding,以及java8引入的interface default method implement是否也属于adhoc多态?

    如果没有Ad-hoc的trait以及impl,那么我们就无法为tuple2和tuple3两个不同类型的Eq实现定义不同的行为——它们将会不得不一个实现Eq2一个实现Eq3这两个不同的类型来分别定义两个元素的元组和三个元素的元组的比较逻辑,这显然失去了多态的意义

    可以在php中重新实现此rust例子: https://3v4l.org/Hdmot

    <?php
    trait Eq
    {
        abstract public function equiv(Eq $lhs, Eq $rhs): bool;
    }
    
    class Tuple2
    {
        use Eq;
        public function __construct(public Eq $key, public Eq $value) {}
        public function equiv(Eq $lhs, Eq $rhs): bool
        {
            return $lhs->key->equiv($lhs->key, $rhs->key) && $lhs->value->equiv($lhs->value, $rhs->value);
        }
    }
    
    class Tuple3
    {
        use Eq;
        public function __construct(public Eq $key, public Eq $value, public Eq $column) {}
        public function equiv(Eq $lhs, Eq $rhs): bool
        {
            return $lhs->key->equiv($lhs->key, $rhs->key)
                && $lhs->value->equiv($lhs->value, $rhs->value)
                && $lhs->column->equiv($lhs->column, $rhs->column);
        }
    }
    
    function check_equiv(Eq $lhs, Eq $rhs)
    {
        return $lhs->equiv($lhs, $rhs);
    }
    
    var_dump(check_equiv(
        new Tuple3(new Tuple2(1, 2), new Tuple2(3, 4), new Tuple2(5, 6)),
        new Tuple3(new Tuple2(1, 2), new Tuple2(3, 4), new Tuple2(5, 6))
    ));

    但在最后对check_equiv的调用中肉眼可见123456这几个int并不与Eq类型兼容:

    Fatal error: Uncaught TypeError: Tuple2::__construct(): Argument #1 ($key) must be of type Eq, int given

    这意味着对于上述php代码中的类型约束,您只能写出无限嵌套永不结束的new Tuple2或3(new Tuple2或3(...)),使得Tuple2和3类几乎没有任何实际用途
    即便试图写一个实现了Eq的类来包装int使得其能够放进Tuple2或3的ctor中: https://3v4l.org/2T5dP

    class Number
    {
        use Eq;
        public function __construct(public int $value) {}
        public function equiv(int $lhs, int $rhs): bool
        {
            return $lhs === $rhs;
        }
    }

    我们也会发现我们不可能实现Eq trait中抽象的equiv方法,因为根本没有正确的对应类型能够填入equiv实现函数签名:

    Fatal error: Declaration of Number::equiv(int $lhs, int $rhs): bool must be compatible with Eq::equiv(Eq $lhs, Eq $rhs): bool

    进一步的我们可以学习java8精神给Eq::equiv()加上默认实现: https://3v4l.org/QKBbK

    trait Eq
    {
        public function equiv(Eq $lhs, Eq $rhs): bool
        {
            return $lhs->equiv($lhs, $rhs);
        }
    }

    然后

    var_dump(check_equiv(
        new Tuple3(new Tuple2(new Number(1), new Number(2)), new Tuple2(new Number(3), new Number(4)), new Tuple2(new Number(5), new Number(6))),
        new Tuple3(new Tuple2(new Number(1), new Number(2)), new Tuple2(new Number(3), new Number(4)), new Tuple2(new Number(5), new Number(6)))
    ));

    但人生自古谁无死,不幸地,php的类型系统无法将任何实现了一个trait的类视作是trait的subtyping,因此使得您不可能类型约束任何东西为某个trait,因为您不可能在运行时创建任何东西使其是某个trait的subtype:

    Fatal error: Uncaught TypeError: Tuple2::__construct(): Argument #1 ($key) must be of type Eq, Number given

    而堆栈溢出oo带师们早已道明真相:
    https://stackoverflow.com/questions/73172902/how-to-type-hint-a-trait-when-using-the-trait-to-realize-multi-inheritance

    你必须重新考虑你的策略。你可以尝试与之抗争,但它只会让你的生活变得悲惨。只需屈服并学习使用 PHP 的创建方式。沮丧和生气真的没有用,因为 PHP 做事的方式与其他语言不同。要么学习以 PHP 方式做事,要么找到另一种适合您喜欢的工作方式的语言。试图强迫它做它做不到的事情是没有意义的。特征不是类。学习使用它。

    https://stackoverflow.com/questions/14157586/php-type-hinting-traits

    确实,您不能将特征视为 PHP 中的特征(具有讽刺意味的是),说您期望具有某些特征的对象只能通过接口(本质上是抽象特征的集合)实现
    其他语言(例如 Rust)实际上鼓励使用特征进行类型提示
    综上所述; 你的想法是不是一个疯狂的!将特征视为词的常识意义实际上很有意义,其他语言也是这样做的。由于某种原因,PHP 似乎卡在了接口和特征之间。
    关于第 2 点:Rust 鼓励使用特征进行类型提示,但与 PHP 不同的是,Rust 不提供接口;trait 是 Rust 中最接近接口的东西。
    我不得不说我不同意这一点。我认为类型提示特征会非常有用。特征允许声明抽象方法,因此如果允许类型提示,它们可以说比接口更有用,因为 a) 它们支持多重继承(如接口)和 b) 它们支持实现(如抽象类,它们是类型-暗示)。由于 PHP 未编译,类型提示几乎是关于抽象的最好部分,因此特征没有它们应有的那么有用。
    答案是正确的,您不想将特征用作类型。traitsuse在类中与关键字一起使用的方式意味着你没有说明你正在创建什么类型,你只是在实现它。这基本上是复制并粘贴到那个地方。这真的很好,因为它允许您实际上不必复制和粘贴,也不必创建重复项。通常使用继承来避免复制和粘贴是很诱人的,但这常常违反适当继承的原则。
    在实际从事大量使用继承和特征的项目之后,我不得不同意上述评论。Traits 是一种无需手动复制和粘贴即可实现接口的方法,因此它们非常有用。在心理上区分特征和类型系统在这里很重要也很有帮助。

  6. 1846913566 的头像
    1846913566

    我在 GraiaProject/Avilla 中实现了一个 Python 的 typeclass,这个实现虽不包含一些比较重要的特性,但可以通过现有的类型检查并获得完整的 IDE 支持(使用 Pylance/Pyright)。
    由于在原实现中我因为特殊的需求而没有使用设计中的原实现(局限于 Pyright 实现的类型检查的 Features),这里的代码并不适用于上述的项目。。

    from ryanvk import wrap_namespace, implements, Trait, Fn, Cassette
    
    class ExampleTrait(Trait):
        @Fn
        async def example_fn(self, arg1, arg2, *args, **kwargs) -> int:
            ...
    
    class ExampleCassette(Cassette):
        with wrap_namespace() as _artifacts:
            @implements(ExampleTrait.example_fn)
            async def impl_fn(arg1, arg2, *args, **kwargs) -> int:
                return 1
    
    a = ExampleCassette()
    b = await a.cast(ExampleTrait).example_fn(1, 2, 3, 4, a=1, b=2)
    reveal_type(b)  # -> int

    这直接引用了 Trait 声明本身,为了通过类型检查,舍弃了隐式查询虚函数表的功能。但这效果仍是出色的。

    1. 1846913566 的头像
      1846913566

      wait 我有一个天才的想法 来绕开hkt对trait做类型信息附加 让我先试试看

回复 n0099 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注