编程范式 | 青训营笔记

65 阅读15分钟

这是我参与笔记创作活动的第4天

一、本堂课重点内容:

  1. 编程语言
  2. 编程范式
  3. 领域特定语言

二、详细知识点介绍:

  • 本堂课介绍了哪些知识点?

一、编程语言

1.1机器语言

机器语言.PNG

由于计算机内部只能接受二进制代码,因此,用二进制代码0和1描述的指令称为机器指令,全部机器指令的集合构成计算机的机器语言,用机器语言编程的程序称为目标程序。只有目标程序才能被计算机直接识别和执行。但是机器语言编写的程序无明显特征,难以记忆,不便阅读和书写,且依赖于具体机种,局限性很大,机器语言属于低级语言。

用机器语言编写程序,编程人员要首先熟记所用计算机的全部指令代码和代码的涵义。手编程序时,程序员得自己处理每条指令和每一数据的存储分配和输入输出,还得记住编程过程中每步所使用的工作单元处在何种状态。这是一件十分繁琐的工作。编写程序花费的时间往往是实际运行时间的几十倍或几百倍。而且,编出的程序全是些0和1的指令代码,直观性差,还容易出错。

除了计算机生产厂家的专业人员外,绝大多数的程序员已经不再去学习机器语言了。

机器语言是微处理器理解和使用的,用于控制它的操作二进制代码。

尽管机器语言好像是很复杂的,然而它是有规律的。

存在着多至100000种机器语言的指令。这意味着不能把这些种类全部列出来。

以下是一些示例:

指令部份的示例

0000 代表 加载(LOAD)

0001 代表 存储(STORE)

...

暂存器部份的示例

0000 代表暂存器 A

0001 代表暂存器 B

...

存储器部份的示例

000000000000 代表地址为 0 的存储器

000000000001 代表地址为 1 的存储器

000000010000 代表地址为 16 的存储器

100000000000 代表地址为 2^11 的存储器

集成示例

0000,0000,000000010000 代表 LOAD A, 16

0000,0001,000000000001 代表 LOAD B, 1

0001,0001,000000010000 代表 STORE B, 16

0001,0001,000000000001 代表 STORE B, 1[1]

1.2汇编语言

汇编语言.PNG

汇编语言的实质和机器语言是相同的,都是直接对硬件操作,只不过指令采用了英文缩写的标识符,更容易识别和记忆。它同样需要编程者将每一步具体的操作用命令的形式写出来。汇编程序的每一句指令只能对应实际操作过程中的一个很细微的动作。例如移动、自增,因此汇编源程序一般比较冗长、复杂、容易出错,而且使用汇编语言编程需要有更多的计算机专业知识,但汇编语言的优点也是显而易见的,用汇编语言所能完成的操作不是一般高级语言所能够实现的,而且源程序经汇编生成的可执行文件不仅比较小,而且执行速度很快。

汇编的hello world,打印一句hello world, 需要写十多行,也是醉了。

; hello.asm 
section .data            ; 数据段声明
        msg db "Hello, world!", 0xA     ; 要输出的字符串
        len equ $ - msg                 ; 字串长度
section .text            ; 代码段声明
global _start            ; 指定入口函数
_start:                  ; 在屏幕上显示一个字符串
        mov edx, len     ; 参数三:字符串长度
        mov ecx, msg     ; 参数二:要显示的字符串
        mov ebx, 1       ; 参数一:文件描述符(stdout) 
        mov eax, 4       ; 系统调用号(sys_write) 
        int 0x80         ; 调用内核功能
                         ; 退出程序
        mov ebx, 0       ; 参数一:退出代码
        mov eax, 1       ; 系统调用号(sys_exit) 
        int 0x80         ; 调用内核功能

1.3高级语言

高级语言.PNG

高级语言是大多数编程者的选择。和汇编语言相比,它不但将许多相关的机器指令合成为单条指令,并且去掉了与具体操作有关但与完成工作无关的细节,例如使用堆栈、寄存器等,这样就大大简化了程序中的指令。同时,由于省略了很多细节,编程者也就不需要有太多的专业知识。

高级语言主要是相对于汇编语言而言,它并不是特指某一种具体的语言,而是包括了很多编程语言,像最简单的编程语言PASCAL语言也属于高级语言。

高级语言所编制的程序不能直接被计算机识别,必须经过转换才能被执行,按转换方式可将它们分为两类:

编译类:编译是指在应用源程序执行之前,就将程序源代码“翻译”成目标代码(机器语言),因此其目标程序可以脱离其语言环境独立执行(编译后生成的可执行文件,是cpu可以理解的2进制的机器码组成的),使用比较方便、效率较高。但应用程序一旦需要修改,必须先修改源代码,再重新编译生成新的目标文件(* .obj,也就是OBJ文件)才能执行,只有目标文件而没有源代码,修改很不方便。

编译后程序运行时不需要重新翻译,直接使用编译的结果就行了。程序执行效率高,依赖编译器,跨平台性差些。如C、C++、Delphi等

解释类:执行方式类似于我们日常生活中的“同声翻译”,应用程序源代码一边由相应语言的解释器“翻译”成目标代码(机器语言),一边执行,因此效率比较低,而且不能生成可独立执行的可执行文件,应用程序不能脱离其解释器(想运行,必须先装上解释器,就像跟老外说话,必须有翻译在场),但这种方式比较灵活,可以动态地调整、修改应用程序。如Python、Java、PHP、Ruby等语言。

1.4语言代表

  • 过程式语言代表 C.PNG

  • 面向对象语言代表 C++.PNG

  • 函数式语言代表 Lisp.PNG

  • 基于原型和头等函数的多范式语言 javascript.PNG

二、编程范式

2.1 常见编程范式

常见编程范式.PNG

2.2 过程式编程

过程式编程.PNG

2.3 运行顺序

运行顺序.PNG

2.4 结构化编程

结构化编程.PNG

2.5 JS的面向过程

js的面向过程.PNG

2.6 面向对象过程

面向对象过程.PNG

2.7 面向对象编程的特性

面向对象_封装.PNG

面向对象_继承.PNG

多态.PNG

依赖注入.PNG

2.8面向对象编程的五大原则

五大原则.PNG

2.9函数式编程

函数式编程

而函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程

函数式编程是面向数学的抽象,将计算描述为一种表达式求值,其实,函数式程序就是一个表达式。

函数式编程本质

函数式编程中函数并部署指计算机中的函数,而是指数学中的函数,即自变量的映射。函数的值取决于函数的参数的值,不依赖于其他状态,比如abs(x)函数计算x的绝对值,只要x不变,无论何时调用、调用次数,最终的值都是一样。

函数式编程的特点

  • 函数是第一等公民
  • 函数是纯函数

接下来我们分别介绍下函数式编程的这两个特点

函数是第一等公民

函数是第一等公民:是指函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值。正如我们开头提到的闭包的实现好体现了这个特点,例如如下代码:

// 赋值
var func1 = function func1() {  }
// 函数作为参数
function func2(fn) {
    fn()
}   
// 函数作为返回值
function func3() {
    return function() {}
}

函数是纯函数

纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数。纯函数的两个特点:

  • 相同的输入必有同输出
  • 没有副作用

无副作用 指的是函数内部的操作不会对外部产生影响(如修改全局变量的值、修改 dom 节点等)。

// 是纯函数
function sum(x,y){
    return x + y
}
// 输出不确定,不是纯函数
function random(x){
    return Math.random() * x
}
// 有副作用,不是纯函数
function setFontSize(el,fontsize){
    el.style.fontsize = fontsize ;
}
// 输出不确定、有副作用,不是纯函数
let count = 0;
function addCount(x){
    count+=x;
    return count;
}

函数式编程的基本运算

函数合成(compose)

指的是将代表各个动作的多个函数合并成一个函数。

上面讲到,函数式编程是对过程的抽象,关注的是动作。看下下面的例子

function add(x) {
    return x + 10
}
function multiply(x) {
    return x * 10
}

console.log(multiply(add(2)))  // 120

将合成的动作抽象为一个函数 compose如下:

function compose(f,g) {
    return function(x) {
        return f(g(x));
    };
}
// 这样我们我们可以通过如下的方式得到合成函数
// 执行动作的顺序是从右往左
let calculate=compose(multiply,add); 
console.log(calculate(2))  // 120

只要往 compose 函数中传入代表各个动作的函数,我们便能得到最终的合成函数。但上述 compose 函数的局限性是只能够合成两个函数,如果需要合成的函数不止两个呢,所以需要一个通用的 compose 函数。

function compose() {
  let args = arguments;
  let start = args.length - 1;
  return function () {
    let i = start - 1;
    let result = args[start].apply(this, arguments);
    while (i >= 0){
      result = args[i].call(this, result);
      i--;
    }
    return result;
  };
}

// 使用
function add(str){
    return x + 10
}
function multiply(str) {
    return x * 10
}
function minus(str) {
    return x - 10
}

let composeFun = compose(minus, multiply, add);
composeFun(2) // 110

通过 compose 将上述三个动作代表的函数合并成了一个,并最终输出了正确的结果。

函数柯里化(Currying)

函数柯里化又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值

柯里化函数有如下两个特性:

  • 接受一个单一参数
  • 返回接受余下的参数而且返回结果的新函数
function sum(a, b) {
    return a + b;
}
console.log(sum(2, 2)) // 4

假设函数 sum 的柯里化函数是 sumCurry,那么从上述定义可知,sumCurry(2)(2) 应该实现与上述代码相同的效果,输出 4 。这里我们可以比较容易的知道,sumCurry 的代码如下

// sumCurry 是 sum 的柯里化函数
function sumCurry(a) {
    return function(b) {
        return a + b;
    }
}
console.log(sumCurry(2)(2));  // 4

如果有一个函数 createCurry 能够实现柯里化,那么我们便可以通过下述的方式来得出相同的结果

// sumCurry 返回一个柯里化函数
var sumCurry=createCurry(sum);
console.log(sumCurry(2)(2));  // 4

可以看到,函数 createCurry 传入一个函数 sum 作为参数,返回了一个柯里化函数 sumCurry,函数 sumCurry 能够处理 sum 中的剩余参数。这个过程就称为函数柯里化,我们称 sumCurry 是 add 的柯里化函数。

怎么得到实现柯里化的函数 createCurry 呢?这里我直接给出 createCurry 的代码

// 参数只能从左到右传递
function createCurry(func, arrArgs) {
    var args=arguments;
    var funcLength = func.length;
    var arrArgs = arrArgs || [];

    return function() {
        var _arrArgs = Array.prototype.slice.call(arguments);
        var allArrArgs=arrArgs.concat(_arrArgs)

        // 如果参数个数小于最初的func.length,则递归调用,继续收集参数
        if (allArrArgs.length < funcLength) {
            return args.callee.call(this, func, allArrArgs);
        }

        // 参数收集完毕,则执行func
        return func.apply(this, allArrArgs);
    }
}

// createCurry 返回一个柯里化函数
var sumCurry=createCurry(function(a, b, c) {
    return a + b + c;
});
sumCurry(1)(2)(3) // 6
sumCurry(1, 2, 3) // 6
sumCurry(1)(2,3) // 6
sumCurry(1,2)(3) // 6

柯里化实际上是把简答的问题复杂化了,但是复杂化的同时在使用函数时拥有了更加多的自由度。

柯里化用途

现在需要实现一个功能,将一个全是数字的数组中的数字转换成百分数的形式。按照正常的逻辑,我们可以按如下代码实现

function getPercentList(array) {
    return array.map(function(item) {
        return item * 100 + '%'
    })
}

console.log(getPercentList([1, 0.2, 3, 0.4]));   
// 结果:['100%', '20%', '300%', '40%']

如果通过柯里化的方式来实现

function map(func, array) {
    return array.map(func);
}
var mapCurry = createCurry(map);
var getNewArray = mapCurry(function(item) {
    return item * 100 + '%'
})
console.log(getPercentList([1, 0.2, 3, 0.4])); 
// 结果:['100%', '20%', '300%', '40%']

上述例子太简单以致不能表现出柯里化的强大,具体柯里化的使用还需要结合具体的场景,其实,没有必要为了柯里化而柯里化,不管用什么方式我们的最终目的都是为了更好地解决问题。

高阶函数

满足下列条件之一的函数就可以称为高阶函数:

  1. 函数作为参数被传递

把函数当作参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。

下面例子中js的函数都是对高阶函数的利用:

[1, 4, 2, 5, 0].sort((a, b) => a - b);
// [0, 1, 2, 4, 5]
        
[0, 1, 2, 3, 4].map(v => v + 1);
// [1, 2, 3, 4, 5]
        
[0, 1, 2, 3, 4].every(v => v < 5);
// true

2.函数作为返回值输出

让函数继续返回一个可执行的函数,意味着运算过程是可延续的

const fn = (() => {
    let students = [];
    return {
        addStudent(name) {
            if (students.includes(name)) {
                return false;
            }
            students.push(name);
        },
        showStudent(name) {
            if (Object.is(students.length, 0)) {
                return false;
            }
            return students.join(",");
        }
    }
})();
fn.addStudent("liming");
fn.addStudent("zhangsan");
fn.showStudent(); //输出:liming,zhangsan

同时满足两个条件的高阶函数

const plus = (...args) => {
    let n = 0;
    for (let i = 0; i < args.length; i++) {
        n += args[i];
    }
    return n;
}

const mult = (...args) => {
    let n = 1;
    for (let i = 0; i < args.length; i++) {
        n *= args[i];
    }
    return n;
}

const createFn = (fn) => {
    let obj = {};
    return (...args) => {
        let keyName = args.join("");
        if (keyName in obj) {
            return obj[keyName];
        }
        obj[keyName] = fn.apply(null, args);
        return obj[keyName];
    }
}

let fun1 = createFn(plus);
console.log(fun1(2, 2, 2)); //输出:6

let fun2 = createFn(mult);
console.log(fun2(2, 2, 2)); //输出:8

2.10响应式编程

  • 什么是响应式编程 响应式编程.PNG
  • Observable观察者模式 Observable.PNG
  • 操作符 操作符.PNG
  • Monad设计模式 Monad.PNG

三、领域特定语言

3.1什么是领域特定语言

领域特定语言是在特定领域下用于特定上下文的语言。作为开发者,很有必要了解领域特定语言的含义,以及为什么要使用特定领域语言。

领域特定语言domain-specific language (DSL)是一种旨在特定领域下的上下文的语言。这里的领域是指某种商业上的(例如银行业、保险业等)上下文,也可以指某种应用程序的(例如 Web 应用、数据库等)上下文。与之相比的另一个概念是 通用语言general-purpose language (GPL,LCTT 译注:注意不要和 GPL 许可证混淆),通用语言则可以广泛应用于各种商业或应用问题当中。

DSL 并不具备很强的普适性,它是仅为某个适用的领域而设计的,但它也足以用于表示这个领域中的问题以及构建对应的解决方案。HTML 是 DSL 的一个典型,它是在 Web 应用上使用的语言,尽管 HTML 无法进行数字运算,但也不影响它在这方面的广泛应用。

而 GPL 则没有特定针对的领域,这种语言的设计者不可能知道这种语言会在什么领域被使用,更不清楚用户打算解决的问题是什么,因此 GPL 会被设计成可用于解决任何一种问题、适合任何一种业务、满足任何一种需求。例如 Java 就属于 GPL,它可以在 PC 或移动设备上运行,嵌入到银行、金融、保险、制造业等各种行业的应用中去。

3.2 DSL的类别

从使用方式的角度,语言可以划分出以下两类:

  • DSL:使用 DSL 形式编写或表示的语言
  • 宿主语言host language :用于执行或处理 DSL 的语言

由不同的语言编写并由另一种宿主语言处理的 DSL 被称为 外部external DSL。

以下就是可以在宿主语言中处理的 SQL 形式的 DSL:

SELECT account
FROM accounts
WHERE account = '123' AND branch = 'abc' AND amount >= 1000

因此,只要在规定了词汇和语法的情况下,DSL 也可以直接使用英语来编写,并使用诸如 ANTLR 这样的 解析器生成器parser generator 以另一种宿主语言来处理 DSL:

if smokes then increase premium by10%

如果 DSL 和宿主语言是同一种语言,这种 DSL 称为 内部internal DSL,其中 DSL 由以同一种语义的宿主语言编写和处理,因此又称为 嵌入式embedded DSL。以下是两个例子:

  • Bash 形式的 DSL 可以由 Bash 解释器执行:
if today_is_christmas; then apply_christmas_discount; fi

同时这也是一段看起来符合英语语法的 Bash。

使用类似 Java 语法编写的 DSL:

orderValue = orderValue       
       .applyFestivalDiscount()          
       .applyCustomerLoyalityDiscount()        
       .applyCustomerAgeDiscount();  
  

这一段的可读性也相当强。

实际上,DSL 和 GPL 之间并没有非常明确的界限。

3.3为什么要使用 DSL?

DSL 的目的是在某个领域中记录一些需求和行为,在某些方面(例如金融商品交易)中,DSL 的适用场景可能更加狭窄。业务团队和技术团队能通过 DSL 有效地协同工作,因此 DSL 除了在业务用途上有所发挥,还可以让设计人员和开发人员用于设计和开发应用程序。

DSL 还可以用于生成一些用于解决特定问题的代码,但生成代码并不是 DSL 的重点并不在此,而是对专业领域知识的结合。当然,代码生成在领域工程中是一个巨大的优势。

3.4DSL 的优点和缺点

DSL 的优点是,它对于领域的特征捕捉得非常好,同时它不像 GPL 那样包罗万有,学习和使用起来相对比较简单。因此,它在专业人员之间、专业人员和开发人员之间都提供了一个沟通的桥梁。

而 DSL 最显著的缺点就在于它只能用于一个特定的领域和目标。尽管学习起来不算太难,但学习成本仍然存在。如果使用到 DSL 相关的工具,即使对工作效率有所提升,但开发或配置这些工具也会增加一定的工作负担。另外,如果要设计一款 DSL,设计者必须具备专业领域知识和语言开发知识,而同时具备这两种知识的人却少之又少。

三、课后个人总结:

  • 本章有什么知识点不容易掌握?

    1.面向对象编程

    2.响应式编程

    3.领域特定语言

  • 什么地方容易与其他内容混淆?、

    1.面向对象和响应式编程