这篇文章里,我们试图略微讨论一下类与类型的概念。当然,内容是很浅薄甚至是更关注实践的。不过这种解读可能更有裨益。

我在前面水管模型的叙述中,一直将函数式的假想敌人想象成「过程式」编程。这里当然我们就不免地对编程语言做一些简单的分类。许多人认为函数式编程相反的概念是面向对象编程,但其实这里存在了非常多的误解。我在前面的文章里,一直强调如果要使用一个新的概念,就必须至少在那篇文章里做说明。一路走来,函数式的基本概念已经帮我们解决了大部分问题。这个到实现列表为止,都让人觉得可以接受。但后面,为了更方便地获取一些值已经做一些类型标注,我们在此必须要引入类的概念了,理由如下。

  1. 之前获取List头的函数使用head(pair)这种语法,再各种嵌套的时候,会降低很多可读性(虽然我们实现的compose/and_then部分解决了这个问题)。
  2. 比如pair(a, b)这个数据,我们更像稳定地作为一些数据/值的结构。这个结构体本身是稳定的。然后单纯用函数的定义会使它们的使用过于松散。
  3. 在写一个代码的时候h(g(f(x)))这种写法往往不和人的思考逻辑,事实上我们是考虑f再考虑g这样的。而类的调用使得我们可以按正常思维逻辑完成这件事x.f().g().h()
  4. 最后就是对于类型标注的需要,Python的类型标注,大多依赖于相关类的定义。所以有必要引入类的概念。

不过,我们首先要理清一下类型的区别。大部分语言里,类型在狭义上就是指程序语言自带的值的类别;则是面向对象的概念,和我们所谓的对象、实例化这些概念有关。而如果你考察大部分类型标注系统,你可以发现类的新建的时候,就是在创造一个新的类型。真正比较好和简答地理解,就是类型表示的是一些值的集合,它们共同有一部分性质。而则是比类型提供更多的概念,比如「属性」、「方法」、「继承」这些。当然,在Python之中,类型也就理所当然承担了元类,即的概念。

但是,事实上也有「过程式」和「函数式」的两面。譬如,我们在使用下面一个学生类的时候,add_age涉及到了对自己属性的变化,这就涉及到了「变量」或者我们之前提到的屋子模型的概念。这是我们想要避免的事。

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

    def add_age(self, add_num):
        self.age += add_num

另一个涉及到的问题是恒等性的概念,就是我们如何判断两个事物是否相等的逻辑。类编程事实上,每一次实例化的结果都是一个不同结果,比如上面的例子中,我们可以尝试定义下面两个学生,他们虽然名字和年龄一样,但是我们却得出他们不是一个人的结果。

>>> a = Student("a", 11)
>>> b = Student("a", 11)            
>>> a == b
False

在技术实现上说,Python比较的是ab的hash值,或者说他们是两个屋子,Python比较的是屋子而不是里面的值。我们打印ab的hash值可以发现他们是不同的。

>>> a.__hash__()
137886633561
>>> b.__hash__()
137886633567

而在函数式例子里,我们只想把类作为一个数据组合工具,并提供部分继承概念的东西,这就显得没有必要了。一个方法是,我们对==的逻辑改写,比如上面的例子里,我们要重载__eq__方法:

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age  = age
    
    def add_age(self, add_num):
        self.age += add_num
    
    def __eq__(self, other):
        return (self.name == other.name) and (self.age == other.age)


>>> a = Student("a", 11) 
>>> b = Student("a", 11)
>>> a == b
True

一个更好的方法,就是使用dataclass的概念。它的名字本身也就是说我们在把这个类的对象当做数据来看。在上面的例子里,我们就可以使用dataclass的修饰器就行了,并且我们甚至可以省略__init__方法:

from dataclasses import dataclass

@dataclass
class Student:
    name: str
    age: int

我们可以发现,这样执行,我们得到了我们之前期待的答案:

>>> a = Student("a", 11) 
>>> b = Student("a", 11)
>>> a == b
True

但是dataclass并不限制你产生副作用的函数,比如我们举例中的add_age。在函数式编程中,我们可以使用一个Point Free的写法,通过返回修改过参数的对象就行了,比如上面的例子可以改写为:

from dataclasses import dataclass

@dataclass
class Student:
    name: str
    age: int

    def add_age(self, add_num):
        return Student(self.name, self.age + add_num)

这种写法有个好处,就是我们能改成链式的调用。缺点就是我们可能需要新建一个变量名储存这个结果:

>>> Student("a", 1).add_age(2).add_age(3).add_age(-1)
Student(name='a', age=5)

这基本上构成了这个系列文章后面的主要风格,除了少部分不可缺少的副作用,以及通过隔离它们到一个很小的范围内。其他部分我们将非常好地利用对象式编程的结构分层和项目代码管理的能力,以及函数式的特性来解决大部分的问题。