关于后端:编程语言类型系统的本质

8次阅读

共计 14045 个字符,预计需要花费 36 分钟才能阅读完成。

0. 引子

我始终对编写更好的代码有浓重的趣味。如果你能真正了解什么是形象,什么是具象,就能了解为什么古代编程语言中,接口和函数类型为什么那么普遍存在了。在应用函数式语言进行编程后,就可能很清晰地了解为什么随着工夫的推移,更支流的语言开始采纳函数式语言中的一些被认为天经地义的个性。

我将多年间学习类型零碎和编程语言开发的教训汇聚起来,加以提炼,并辅以事实世界的利用,撰写了这篇文章。本文脉络如下:

  1. 概述:什么是类型?为什么要引入类型的概念?
  2. 编程语言中的根本类型
  3. 类型组合
  4. OOP 与接口类型
  5. 函数类型
  6. 函子(Functor)和单子(Monad)

1. 概述:什么是类型?为什么要引入类型的概念?

类型零碎设计的实践与日常生产软件之间存在间接的分割。这并不是一个革命性的发现:简单的类型零碎个性之所以存在,就是为了解决事实世界的问题。

本节介绍类型和类型零碎,探讨它们为什么存在以及为什么有用。咱们将探讨类型零碎的类型,并解释类型强度、动态类型和动静类型。

两个术语:类型、类型零碎

类型

类型是对数据做的一种分类,定义了可能对数据执行的操作、数据的意义,以及容许数据承受的值的汇合。编译器和运行时会查看类型,以确保数据的完整性,施行拜访限度,以及依照开发人员的用意来解释数据。

类型零碎

类型零碎是一组规定,为编程语言的元素调配和施行类型。这些元素能够是变量、函数和其余高级构造。类型零碎通过两种形式调配类型:程序员在代码中指定类型,或者类型零碎依据上下文,隐式推断出某个元素的类型。类型零碎容许在类型之间进行某些转换,而阻止其余类型的转换。

从简单零碎的束缚开始

“零碎”一词由来已久,在古希腊是指复杂事物的总体。到近代,一些科学家和哲学家罕用零碎一词来示意简单的具备肯定构造的整体。在宏观世界和微观世界,从基本粒子到宇宙,从细胞到人类社会,从动植物到社会组织,无一不是零碎的存在形式。

控制论(维纳,1948,《控制论(或对于在动物和机器中管制和通信的迷信)》)通知咱们,负反馈就是零碎稳固的机制,一个组织系统之所以可能受到烦扰后能迅速排除偏差复原恒定的能力,关键在于存在着“负反馈调节”机制:零碎必须有一种安装,来测量受烦扰的变量和维持有机体生存所必须的恒值之间的差异。例如,一个实时零碎复杂性工作的束缚,包含工夫束缚、资源束缚、执行程序束缚和性能束缚。

类型查看:类型查看确保程序恪守类型零碎的规定。编译器在转换代码时进行类型查看,而运行时在执行代码时进行类型查看。编译器中负责施行类型规定的组件叫作类型查看器。如果类型查看失败,则意味着程序没有恪守类型零碎的规定,此时程序将会编译失败,或者产生运行时谬误。“恪守类型零碎规定的程序相当于一个逻辑证实。”

类型零碎,就是简单软件系统的“负反馈调节器”。通过一套类型标准,加上编译监控和测试机制,来实现软件系统的数据抽象和运行时数据处理的平安。

随着软件变得越来越简单,咱们越来越须要保障软件可能正确运行。通过监控和测试,可能阐明在给定特定输出时,软件在特定时刻的行为是符合规定的。但类型为咱们提供了更加一般性的证实,阐明无论给定什么输出,代码都将依照规定运行。

例如,将一个值标记为 const,或者将一个成员变量标记为 private,类型查看将强制限度施行其余许多平安属性。

从 01 到事实世界对象模型

类型为数据赋予了意义。类型还限度了一个变量能够承受的有效值的汇合。

在低层的硬件和机器代码级别,程序逻辑(代码)及其操作的数据是用位来示意的。在这个级别,代码和数据没有区别,所以当零碎误将代码当成数据,或者将数据当成代码时,就很容易产生谬误。这些谬误可能导致系统解体,也可能导致重大的安全漏洞,攻击者利用这些破绽,让零碎把他们的输出数据作为代码执行。

通过对编程语言的钻研,人们正在设计出越来越弱小的类型零碎(例如,Elm 或 Idris 语言的类型零碎)。Haskell 正变得越来越受欢迎。同时,在动静类型语言中增加编译时类型查看的工作也在推动中:Python 增加了对类型提醒的反对,而 TypeScript 这种语言纯正是为了在 JavaScript 中增加编译时类型查看而创立的。

显然,为代码增加类型是很有价值的,利用编程语言提供的类型零碎的个性,能够编写出更好、更平安的代码。

编程语言中的数据类型

类型零碎是每个编程语言都会有的基本概念。

  1. Lisp 数据类型可分类为:
  2. 标量类型 – 例如,数字类型,字符,符号等。
    - 数据结构 – 例如,列表,向量,比特向量和字符串。
  3. C 语言的类型零碎分为:根本类型和复合类型。根本类型又能够细分为:整型数值类型和浮点数数值类型,不同类型所占用的内存长度不雷同:

整型数值根本类型

char 占用一个字节
short 占用两个字节
int 目前根本都是 4 字节
long int (能够简写为 long) (32 位零碎是 4 字节,64 位零碎是 8 字节)
long long int (能够简写为 long long) 占用 8 节字

浮点数数值根本类型

float 占用 4 字节 (单精度)
double 占用 8 节字 (双精度浮点数)

复合类型蕴含如下几种

struct 构造体
union 联合体
enum 枚举 (长度等同 int)
数组
指针

  1. Go 语言中有丰盛的数据类型,除了根本的整型、浮点型、布尔型、字符串外,还有数组,切片(slice),构造体(struct),接口(interface),函数(func),map , 通道(channel)等。
  2. 整型:int8 int6 int32 int64;对应的无符号整型:uint8 uint16 uint32 uint64。uint8 就是咱们熟知的 byte 型,int16 对应 C 语言中的 short 型,int64 对应 C 语言中 long 型。
  3. 浮点类型:float32 和 float64, 浮点这两种浮点型数据格式遵循 IEEE 754 规范。
  4. 切片:可变数组,是对数组的一种形象。切片是援用类型。
  5. 接口:实现多态,面向接口编程。定义一个接口 I , 而后应用不同的构造体对接口 I 进行实现, 而后利用接口对象作为形式参数, 将不同类型的对象传入并调用相干的函数, 实现多态。接口能够进行嵌套实现, 通过大接口蕴含小接口。

类型强度

强类型和弱类型的区别没有权威的定义。大多数晚期对于强类型和弱类型的探讨能够概括为动态类型和动静类型之间的区别。

但风行的说法是强类型偏向于不容忍隐式类型转换,而弱类型偏向于容忍隐式类型转换。这样,强类型语言通常是类型平安的,也就是说,它只能以容许的形式拜访它被受权拜访的内存。

通常,动静类型语言偏向于与 Python、Ruby、Perl 或 Javascript 等解释型语言相关联,而动态类型语言偏向于编译型语言,例如 Golang、Java 或 C。

我总结了一个常见编程语言类型的分类图,留神拆分的四个区域是分区,比方 PHP 和 JS 都是动静弱类型。

动态类型与动静类型

咱们常常听到“动态与动静类型”这个问题,其实,两者的区别在于类型查看产生的工夫。

  1. 动态类型零碎在编译时确定所有变量的类型,并在应用不正确的状况下抛出异样。动态类型零碎,将运行时谬误转换成编译时谬误,可能使代码更容易保护、适应性更强,对于大型应用程序,尤其如此。
  2. 而在动静类型中,类型绑定到值。查看是在运行时进行的。动静类型零碎在运行时确定变量类型,如果有谬误则抛出异样,如果没有适当的解决,可能会导致程序解体。动静类型不会在编译时施加任何类型束缚。日常交换中有时会将动静类型叫作“鸭子类型”(duck typing),这个名称来自俗语:“如果一种动物走起来像鸭子,叫起来像鸭子,那么它就是一只鸭子。”代码可依照须要自在应用一个变量,运行时将对变量利用类型。

动态类型零碎的晚期类型错误报告保障了大规模利用程序开发的安全性,而动静类型零碎的毛病是编译时没有类型查看,程序不够平安。只有大量的单元测试能力保障代码的健壮性。然而应用动静类型零碎的程序,很容易编写并且不须要破费很多工夫来确保类型正确。所谓“鱼和熊掌不可兼得”,这就是对于“效率”与“品质”的哲学问题了。

不过,古代类型查看用具有弱小的类型推断算法,使它们可能确定变量或者函数的类型,而不须要咱们显式地写出类型。

小结

  • 类型是一种数据分类,定义了能够对这类数据执行的操作、这类数据的意义以及容许取值的汇合。
  • 类型零碎是一组规定,为编程语言的元素调配并施行类型。
  • 类型限度了变量的取值范畴,所以在一些状况中,运行时谬误就被转换成了编译时谬误。
  • 不可变性是类型施加的一种数据属性,保障了值在不应该发生变化时不会发生变化。
  • 可见性是另外一种类型级别的属性,决定了哪些组件能拜访哪些数据。
  • 类型标识符使得浏览代码的人更容易了解代码。
  • 动静类型(或叫“鸭子类型”)在运行时决定类型。
  • 动态类型在编译时查看类型,捕捉到本来有可能成为运行时谬误的类型谬误。
  • 类型零碎的强度掂量的是该零碎容许在类型之间进行多少隐式转换。
  • 古代类型查看用具有弱小的类型推断算法,使它们可能确定变量或者函数的类型,而不须要咱们显式地写出类型。

2. 编程语言中的根本类型

本节介绍编程语言类型零碎的个性,从根本类型开始,到函数类型、OOP、泛型编程和高阶类型(如函子和单子)。

根本类型

罕用的根本类型包含空类型、单元类型、布尔类型、数值类型、字符串类型、数组类型和援用类型。

函数类型

“函数类型是类型零碎在根本类型及其组合的根底上倒退的又一个阶段。”

大部分古代编程语言都反对匿名函数,也称为 lambda。lambda 与一般的函数相似,然而没有名称。每当咱们须要应用一次性函数时,就会应用 lambda。所谓一次性函数,是指咱们只会援用这种函数一次,所以为其命名就成了多余的工作。

lambda 或匿名函数:lambda,也称为匿名函数,是没有名称的函数定义。lambda 通常用于一次性的、短期存在的解决,并像数据一样被传来传去。

函数可能承受其余函数作为实参,或者返回其余函数。承受一个或多个非函数实参并返回一个非函数类型的“规范”函数也称为一阶函数,或一般函数。承受一个一阶函数作为实参或者返回一个一阶函数的函数称为二阶函数。

咱们能够持续往后推,称承受二阶函数作为实参或者返回二阶函数的函数为三阶函数,然而在理论使用中,咱们只是简略地把所有承受或返回其余函数的函数称为高阶函数。

咱们能够应用“函数类型”简化策略模式。如果一个变量是函数类型(命名函数类型),并在应用其余类型的值的中央可能应用函数,就能够简化一些罕用构造的实现,并把罕用算法形象为库函数。

泛型编程

泛型编程反对弱小的解耦合以及代码重用。
泛型数据结构把数据的布局与数据自身分隔开。迭代器反对遍历这些数据结构。泛型算法(例如,最经典的 sort 排序算法)是可能在不同数据类型上重用的算法。迭代器(Iterator)用作数据结构和算法之间的接口,并且可能依据迭代器的能力启用不同的算法。

例如,一个泛型函数:

(value:T) => T

它的类型参数是 T。当为 T 指定了理论类型时,就创立了具体函数。具体类图示例如下:

再例如,一个泛型二叉树。

泛型高阶函数 map() , filter() , reduce() 代码和示意图如下。

  • map()
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

  • filter()

    public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {return filterTo(ArrayList<T>(), predicate)
    }
  • reduce()

    public inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S {val iterator = this.iterator()
      if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
      var accumulator: S = iterator.next()
      while (iterator.hasNext()) {accumulator = operation(accumulator, iterator.next())
      }
      return accumulator
    }

高阶类型

高阶类型与高阶函数相似,代表具备另外一个类型参数的类型参数。例如,T<U> 或 Box<T<U>> 有一个类型参数 T,后者又有一个类型参数 U。

正如高阶函数是承受其余函数作为实参的函数,高阶类型是承受其余品种作为实参的品种(参数化的类型构造函数)。

类型构造函数

在类型零碎中,咱们能够认为类型构造函数是返回类型的一个函数。咱们不须要本人实现类型构造函数,因为这是类型零碎在外部对待类型的形式。

每个类型都有一个构造函数。一些构造函数很简略。例如,能够把类型 number 的构造函数看作不承受实参、返回 number 类型的一个函数,也就是() -> [number type]。

对于泛型,状况则有了变动。泛型类型,如 T[],须要一个理论的类型参数来生成一个具体类型。其类型构造函数为 (T) -> [T[] type]。例如,当 T 是 number 时,咱们失去的类型是一个数值数组 number[],而当 T 是 string 时,失去的类型是一个字符串数组 string[]。这种构造函数也称为“品种”,即类型 T[] 的品种。

高阶类型与高阶函数一样,将形象水平进步了一个级别。在这里,咱们的类型构造函数能够承受另外一个类型构造函数作为实参。

空类型(nil / null pointer)

null vs 亿万美元的谬误

驰名的计算机科学家、图灵奖获得者托尼·霍尔爵士称 null 援用是他犯下的“亿万美元谬误”。他说过:
“1965 年我创造了 null 援用。当初我把它叫作我犯下的亿万美元谬误。过后,我在一种面向对象语言中为援用设计第一个全面的类型零碎。我的指标是让编译器来主动执行查看,确保所有应用援用的中央都是相对平安的。然而,我没能抗拒引诱,在类型零碎中增加了 null 援用,这只是因为实现 null 援用太简略了。这导致了难以计数的谬误、破绽和零碎解体,在过来四十年中可能造成了数亿美元的损失。”
几十年来产生了十分多的 null 解援用谬误,所以当初很显著,最好不要让 null(即没有值)本身成为某个类型的一个无效的值。

接下来,咱们介绍通过组合现有类型来创立新类型的多种形式。

3. 类型组合

本节介绍类型组合,即如何把类型组合起来,从而定义新类型的各种形式。
组合类型,是将类型放到一起,使后果类型的值由每个成员类型的值组成。

代数数据类型(Algebraic Data Type,ADT)

ADT 是在类型零碎中组合类型的形式。ADT 提供了两种组合类型的形式:

  1. 乘积类型
  2. 和类型

乘积类型

乘积类型就是本章所称的复合类型。元组和记录是乘积类型,因为它们的值是各形成类型的乘积。类型 A = {a1, a2}(类型 A 的可能值为 a1 和 a2)和 B = {b1, b2}(类型 B 的可能值为 b1 和 b2)组合成为元素类型 <A, B> 时,后果为 A×B = {(a1, b1), (a1, b2), (a2, b1), (a2, b2)}。

元组和记录类型都是乘积类型的例子。另外,记录容许咱们为每个成员调配有意义的名称。

和类型

和类型,是将多个其余类型组合成为一个新类型,它存储任何一个形成类型的值。类型 A、B 和 C 的和类型能够写作 A + B + C,它蕴含 A 的一个值,或者 B 的一个值,或者 C 的一个值。

可选类型和变体类型是“和类型”的例子。

4. OOP 与接口类型

本节介绍面向对象编程的要害元素,以及什么时候应用每种元素,并探讨接口、继承、组合和混入。

OOP: 面向对象编程

面向对象编程(Object-Oriented Programming,OOP):OOP 是基于对象的概念的一种编程范式,对象能够蕴含数据和代码。数据是对象的状态,代码是一个或多个办法,也叫作“音讯”。在面向对象零碎中,通过应用其余对象的办法,对象之间能够“对话”或者发送音讯。

OOP 的两个要害特色是封装和继承。封装容许暗藏数据和办法,而继承则应用额定的数据和代码扩大一个类型。

封装呈现在多个档次,例如,服务将其 API 公开为接口,模块导出其接口并暗藏实现细节,类只公开私有成员,等等。与嵌套娃娃一样,代码两局部之间的关系越弱,共享的信息就越少。这样一来,组件对其外部治理的数据可能做出的保障就失去了强化,因为如果不通过该组件的接口,内部代码将无奈批改这些数据。

一个“参数化表达式”的面向对象继承体系的例子。类图如下。

这里的表达式,能够通过 eval() 办法,计算失去一个数字,二元表达式有两个操作数,加法和乘法表达式通过把操作数相加或相乘来计算结果。

咱们能够把表达式建模为具备 eval()办法的 IExpression 接口。之所以能将其建模为接口,是因为它不保留任何状态。

接下来,咱们实现一个 BinaryExpression 抽象类,在其中存储两个操作数。然而,咱们让 eval()是形象办法,从而要求派生类实现该办法。SumExpression 和 MulExpression 都从 BinaryExpression 继承两个操作数,并提供它们本人的 eval()实现。代码如下。

接口类型:抽象类和接口

咱们应用接口来指定契约。接口可被扩大和组合。

接口或契约:接口(或契约)形容了实现该接口的任何对象都了解的一组音讯。音讯是办法,包含名称、实参和返回类型。接口没有任何状态。与事实世界的契约(它们是书面协定)一样,接口也相当于书面协定,规定了实现者将提供什么。

接口又称为动静数据类型,在进行接口应用的的时候, 会将接口对地位的动静类型改为所指向的类型
会将动静值改成所指向类型的构造体。

5. 函数类型

本节介绍函数类型,以及当咱们取得了创立函数变量的能力后可能做些什么,还展现实现策略模式和状态机的不同形式,并介绍根本的 map()、filter()和 reduce()算法。

什么是函数类型?

函数类型或签名

函数的实参汇合加上返回类型称为函数类型(或函数签名)。

函数类型实质上跟接口类型的领域雷同,都是一组映射规定(接口协议),不绑定具体的实现(class,struct)。

函数的实参类型和返回类型决定了函数的类型。如果两个函数承受雷同的实参,并返回雷同的类型,那么它们具备雷同的类型。实参汇合加上返回类型也称为函数的签名。

一等函数

将函数赋值给变量,并像解决类型零碎中的其余值一样解决它们,就失去了所谓的一等函数。这意味着语言将函数视为“一等公民”,赋予它们与其余值雷同的权力:它们有类型,可被赋值给变量,可作为实参传递,可被查看是否无效,以及在兼容的状况下可被转换为其余类型。

“一等函数”编程语言,能够把函数赋值给变量、作为实参传递以及像应用其余值一样应用,这使得代码的表现力更强。

一个简略的策略模式

策略设计模式

策略模式是最罕用的设计模式之一。策略设计模式是一种行为软件设计模式,容许在运行时从一组算法中抉择某个算法。它把算法与应用算法的组件解耦,从而进步了整个零碎的灵活性。下图展现了这种模式。

策略模式由 IStrategy 接口、ConcreteStrategy1 和 ConcreteStrategy2 实现以及通过 IStrategy 接口应用算法的 Context 形成。代码如下:

函数式策略

咱们能够把 WashingStrategy 定义为一个类型,代表承受 Car 作为实参并返回 void 的一个函数。而后,咱们能够把两种洗车服务实现为两个函数,standardWash()和 premiumWash(),它们都承受 Car 作为实参,并返回 void。CarWash 能够抉择其中一个函数利用到一辆给定的汽车,如下图。

策略模式由 Context 形成,它应用两个函数之一:concreteStrategy1()或 concreteStrategy2()。代码如下:

一个简略的装璜器模式

装璜器模式是一个简略的行为软件设计模式,可扩大对象的行为,而不用批改对象的类。装璜的对象能够执行其原始实现没有提供的性能。装璜器模式如图所示。

图阐明:装璜器模式,一个 IComponent 接口,一个具体实现,即 ConcreteComponent,以及应用额定行为来加强 IComponent 的 Decorator。

一个单例逻辑的装璜器

一个单例逻辑的装璜器代码实例如下。

用函数装璜器来实现

上面咱们来应用函数类型实现装璜器模式。
首先,删除 IWidgetFactory 接口,改为应用一个函数类型。该类型的函数不承受实参,返回一个 Widget:() => Widget。

在之前应用 IWidgetFactory 并传入 WidgetFactor 实例的中央,当初须要应用() => Widget 类型的函数,并传入 makeWidget(),代码如下。

咱们应用了一种相似于下面的策略模式的技术:将函数作为实参,在须要的时候进行调用。然而,下面的 use10Widgets() 每次调用都会结构生成一个新的 Widget 实例。

接下来看如何增加单例行为。咱们提供一个新函数 singletonDecorator(),它承受一个 WidgetFactory 类型的函数,并返回另外一个 WidgetFactory 类型的函数。代码如下。

当初,use10Widgets()不会结构 10 个 Widget 对象,而是会调用 lambda,为所有调用重用雷同的 Widget 实例。

小结

与策略模式一样,面向对象办法和函数式办法实现了雷同的装璜器模式。

面向对象版本须要申明一个接口(IWidgetFactory),该接口的至多一个实现(WidgetFactory),以及解决附加行为的一个装璜器类。

与之绝对,函数式实现只是申明了工厂函数的类型(() => Widget),并应用两个函数:一个工厂函数(makeWidget())和一个装璜器函数(singletonDecorator())。

6. 函子和单子(Functor and Monad)

概述

函子和单子的概念来自领域论。领域论是数学的一个分支,钻研的是由对象及这些对象之间的箭头组成的构造。有了这些小结构块,咱们就能够建设函子和单子这样的构造。咱们不会深刻探讨细节,只是简略阐明一下。许多畛域(如集合论,甚至类型零碎)都能够用领域论来表白。

函子(Functor)

“Talk is cheap, show me the code”.

函子,就是数据类型 Functor,它有一个属性值 value 和一个 map 办法。map 办法能够解决 value,并生成新的 Functor 实例。函子的代码如下:

class Functor<T> {
    private value:T;

    constructor(val:T){this.value = val}

    public map<U>(fn:(val:T)=>U){let rst = fn(this.value)
        return new Functor(rst)
    }
}

验证一下 Functor 的利用实例,是否合乎咱们想要的数据类型?

new Functor(3)
    .map(d=>add(d))
    .map(d=>double(d.value))
    .map(d=>square(d.value)) // Functor {value: 256}

这就是函子,一种受规定束缚,含有值 (value) 和值的变形关系 (函数 map) 的数据类型(容器)。它是一种新的函数组合形式,能够链式调用,能够用于束缚传输的数据结构,能够映射适配函数的输入值与下一个函数输出值,能够肯定水平上防止函数执行的副作用。

函子的用处是什么呢?这个问题须要从后面讲过的函数组合 (Function Composition) 讲起。

函数组合是一种把多个函数组合成新函数的形式,它解决了函数嵌套调用的问题,还提供了函数拆分组合的形式。

函数的函子

除了函子外,须要晓得的是,还有函数的函子。给定一个有任意数量的实参且返回类型 T 的值的一个函数。

函子在数学与函数式编程中

在数学中,特地是领域论,函子是领域之间的映射(领域间的同态)。由一领域映射至其本身的函子称之为“自函子”。

在函数式编程里,函子是最重要的数据类型,也是根本的运算单位和性能单位。Functor 是实现了 map() 函数并恪守一些特定规定的容器类型。

咱们有一个泛型类型 H,它蕴含某个类型 T 的 0 个、1 个或更多个值,还有一个从 T 到 U 的函数。在本例中,T 是一个空心圆,U 是一个实心圆。map()函子从 H <T> 实例中拆包出 T,利用函数,而后把后果放回到一个 H <U> 中。

其实,下面的 map(transform: (T) -> R): List<R> 高阶函数就是一个 函子

函子:函子是执行映射操作的函数的推广。对于任何泛型类型,以 Box<T> 为例,如果 map()操作承受一个 Box<T> 和一个从 T 到 U 的函数作为实参,并失去一个 Box<U>,那么该 map()就是一个函子。

函子定义(Functor Laws)
恒等定律:fmap id = id
组合定律:fmap (g . h) = (fmap g) . (fmap h)

函子很弱小,然而大部分支流语言都没有很好的形式来表白函子,因为函子的惯例定义依赖于高阶类型(不是“高阶函数”,是“高阶类型”)的概念。

Functor 函子的代码实现示例
class Functor {
  // 构造函数,创立函子对象的时候接管任意类型的值,并把值赋给它的公有属性 _value
  constructor(value) {this._value = value}
 
  // 接管一个函数,解决值的变形并返回一个新的函子对象
  map (fn) {return new Functor(fn(this._value))
  }
}

let num1 = new Functor(3).map(val => val + 2)

// 输入:Functor {_value: 5}
console.log(num1)

let num2 = new Functor(3).map(val => val + 2).map(val => val * 2)

// 输入:Functor {_value: 10}
console.log(num2)

// 扭转了值类型
let num3 = new Functor('webpack').map(val => `${val}-cli`).map(val => val.length)

// 输入:Functor {_value: 11}
console.log(num3)

单子(Monad Functor)

函子的 value 反对任何数据类型,当然也能够是函子。然而这样会造成函子嵌套的问题。

Maybe.of(3).map(n => Maybe.of(n + 2)) // Maybe {value: Maybe { value: 5} }

单子(Monad 函子)就是解决这个问题的。

Monad Functor 总是返回一个单层的函子,避免出现嵌套的状况。因为它有一个 flatMap 办法,如果生成了一个嵌套函子,它会取出后者的 value,保障返回的是一个单层函子,避免出现嵌套的状况。
代码如下。

class Monad<T> exteds Functor<T>{static of<T>(val:T){return new Monad(val)
    }

    isNothing() {return this.value === null || this.value === undefined}

    public map<U>(fn:(val:T)=>U){if (this.isNothing()) return Monad.of(null)
        let rst = fn(this.value)
        return Monad.of(rst)
    }

    public join(){return this.value}

    public flatMap<U>(fn:(val:T)=>U){return this.map(fn).join()}
}

Monad.of(3).flatMap(val => Monad.of(val + 2)) // Monad {value: 5}

通常讲,Monad 函子就是实现 flatMap 办法的 Pointed 函子。

Monad 由以下三个局部组成:

  1. 一个类型构造函数(M),能够构建出一元类型 M<T>。
  2. 一个类型转换函数(return or unit),可能把一个原始值装进 M 中。

    unit(x) : T -> M T
  3. 一个组合函数 bind,可能把 M 实例中的值取出来,放入一个函数 fn: T-> M<U> 中去执行,最终失去一个新的 M 实例。

    bind:  执行 fn: T  -> M<U> 

除此之外,它还恪守一些规定:

  • 单位元规则,通常由 unit 函数去实现。
  • 结合律规定,通常由 bind 函数去实现。

代码实例:

class Monad {
  value = "";
  // 构造函数
  constructor(value) {this.value = value;}
  // unit,把值装入 Monad 构造函数中
  unit(value) {this.value = value;}
  // bind,把值转换成一个新的 Monad
  bind(fn) {return fn(this.value);
  }
}

// 满足 x-> M(x) 格局的函数
function add1(x) {return new Monad(x + 1);
}
// 满足 x-> M(x) 格局的函数
function square(x) {return new Monad(x * x);
}

// 接下来,咱们就能进行链式调用了
const a = new Monad(2)
     .bind(square)
     .bind(add1);
     //...

console.log(a.value === 5); // true

上述代码就是一个最根本的 Monad,它将程序的多个步骤抽离成线性的流,通过 bind 办法对数据流进行加工解决,最终失去咱们想要的后果。

领域论中的函子

Warning:下文的内容偏数学实践,不感兴趣的同学跳过即可。

原文:A monad is a monoid in the category of endofunctors(Philip Wadler)。
翻译:Monad 是一个 自函子   领域  上的  幺半群”。

这里标注了 3 个重要的概念:自函子、领域、幺半群,这些都是数学知识,咱们离开了解一下。

什么是领域?

任何事物都是对象,大量的对象联合起来就造成了汇合,对象和对象之间存在一个或多个分割,任何一个分割就叫做态射。

一堆对象,以及对象之间的所有态射所形成的一种代数构造,便称之为 领域

什么是函子?

咱们将领域与领域之间的映射称之为 函子。映射是一种非凡的态射,所以函子也是一种态射。

什么是自函子?

自函子 就是一个将领域映射到本身的函子。

什么是幺半群 Monoid?

幺半群是一个存在 单位元 的半群。

什么是半群?

如果一个汇合,满足结合律,那么就是一个 半群

什么是单位元?

单位元 是汇合里的一种特地的元素,与该汇合里的二元运算无关。当单位元和其余元素联合时,并不会扭转那些元素。如:

任何一个数 + 0 = 这个数自身。那么 0 就是单位元(加法单位元)任何一个数 * 1 = 这个数自身。那么 1 就是单位元(乘法单位元)

Ok,咱们曾经理解了所有应该把握的专业术语,那就简略串解一下这段解释吧:

一个 自函子   领域  上的  幺半群,能够了解为:

在一个满足结合律和单位元规则的汇合中,存在一个映射关系,这个映射关系能够把汇合中的元素映射成以后汇合本身的元素。

小结

在不波及领域论的状况下,针对函子和单子,做一个简略的小结。

Functor 和 monad 都为包装输出提供了一些工具,返回包装后的输入。

Functor = unit + map(即工具)

在哪里,

unit= 承受原始输出并将其包装在一个小上下文中的货色。

map= 将函数作为输出的工具,将其利用于包装器中的原始值,并返回包装后的后果。

示例:让咱们定义一个将整数加倍的函数

// doubleMe :: Int a -> Int b
const doubleMe = a => 2 * a;
Maybe(2).map(doubleMe) // Maybe(4)
Monad = unit + flatMap(或绑定或链)

flatMapmap= 顾名思义,就是将 扁平化的工具。


番外篇:自组织实践与简单软件系统

自组织实践是 20 世纪 60 年代末期开始建设并倒退起来的一种零碎实践。它的钻研对象次要是简单自组织系统(生命零碎、社会零碎)的造成和倒退机制问题,即在肯定条件下,零碎是如何主动地由无序走向有序,由低级有序走向高级有序的。

自组织是古代非线性迷信和非平衡态热力学的最令人惊异的发现之一。基于对物种起源、生物进化和社会倒退等过程的深刻察看和钻研,一些新兴的横断学科从不同的角度对自组织的概念给予了界说。

从系统论的观点来说,自组织是指一个零碎在内在机制的驱动下,自行从简略向简单、从毛糙向粗疏方向倒退,一直地进步本身的复杂度和精密度的过程;

从热力学的观点来说,自组织是指一个零碎通过与外界替换物质、能量和信息,而一直地升高本身的熵含量,进步其有序度的过程;

从统计力学的观点来说,自组织是指一个零碎自发地从最可几状态向几率较低的方向迁徙的过程;

从进化论的观点来说,自组织是指一个零碎在遗传、变异和优胜劣汰机制的作用下,其组织构造和运行模式一直地自我完善,从而一直进步其对于环境的适应能力的过程。C. R. Darwin 的生物进化论的最大功劳就是排除了外因的主宰作用,首次从外在机制上、从一个自组织的倒退过程中来解释物种的起源和生物的进化。

什么是简单?

“简单”(Complexity)定义为因为组件之间的依赖关系、关系和交互,而难以对其行为建模的任何零碎。更艰深地说,简单零碎的“整体”大于“局部”之和。也就是说,如果不查看单个组件以及它们如何相互作用,就无奈了解其整体行为的零碎,同时也无奈通过仅查看单个组件而疏忽零碎影响来了解零碎的整体行为。

随着软件系统的扩大,它变得足够大,以至于工作部件的数量,加上对其进行更改的工作程序员的数量,使得零碎的行为十分难以推理。

这种复杂性因许多组织向微服务架构的转变而加剧,例如所谓的“死星”架构,其中圆圈圆周上的每个点代表一个微服务,服务之间的线代表它们的交互。

参考资料

  • 弗拉德·里斯库迪亚(Vlad Riscutia).“编程与类型零碎”(微软资深工程师撰写,从理论利用角度,系统阐述如何应用类型零碎编写更好、更平安的代码)(华章程序员书库)。
  • http://slideplayer.com/slide/…
  • https://dev.to/leolas95/stati…
  • https://towardsdatascience.co…
  • https://stackoverflow.com/que…
  • https://adit.io/posts/2013-04…,_applicatives,_and_monads_in_pictures.html
正文完
 0