编程8年,总结对编程的阶段性认知,延展
软件本质(essence)的理解
笔者从书籍和大量资料中,对软件,软件工程的定义是有很多的,这里我们优先思考软件的本质,而不是软件工程,软件工程是一个很大的话题,定义和边界并不明确,且从软件发展起始混乱的软件工程方法到RUP统一过程,到现在敏捷开发为主旨的发散应用,是一门实践科学,这个需要大家好好思考和根据实际情况应用。
但编程的本质,或者概念是可以在有限边界内被定义,我一直想在八年编程经验庞杂的认知体系中抽象出自己对编程阶段性的理解,这个过程其实并不那么显而易见,或者易于概括,直到笔者重新复习线性代数和矩阵变化时,才越来越明确编程的核心概念。
初识编程
如果你接触过C语言,那么你你应该听说过 软件=数据结构+算法,这句话是极具概括性的,这句话在整个软件的层次其实是适中的,因为数据结构是非常大或者说非常泛化的概念,例如,在Rust等系统级语言中,我们可以定义i8这种数据结构代表-127~128范围的数,或者C中,任意一个指针,指向申请一块大小随意的内存,这块内存的数据我们可以随意安排,然后以某一个数据结构的名字来命名,例如Bitmap。但在其他非系统级别的语言中,例如Dart,我们只有Int, 这个泛化的概念,我们自己定义OOP中的Entity或者Struct, 也是数据结构, 那么这个泛化的概念对于前期接触编程的人来说是有些模糊的,同理算法也是如此, 大学的教育或者面试题等也很少会提及算法的本质,或者作者并不认为这是一个预定义的概念,是所有人都知道的。这是一个很大的误区。
尝试自我下定义
- 程序是一个黑盒子,有若干输入和若干输出
我们具象化一点,类比管道, 管道的概念就是程序的具象化表现
这个概念的明确来源于线性代数,线性方程z = f(x, y), 其中f = 2x + y; 这样来看,算法的概念就明确了,算法是本质上就是 2x+y , 这个类比让笔者切实感受到数学和计算机两门学科的关系。计算机是数据的表征,这也是为什么,中国在核武器研发过程中,可以人力代替计算机。
回到主题,数学和计算机的算法的关系我们已经找到,那么我们可以尝试从数学角度分析数据结构的概念, 假定x 是i8, y 也是 i8, 那么 z = 2x + y的一个实例可以是5=2x2+1, 那么我们也存在x是自定义的数据结构, 例如常见的Size、Point等,我们以Size举例,我们有Size实例 x 大小是Size(w: 2, h: 2), Size实例y大小是Size(w: 1, h: 1), 那么上述方程的此事例的结果就是z = Size(w: 5, h: 5), 于此我们可以得出一个认知:
- 数据结构是约定的、有意义的数据。
这点在开发过程中是有很多的体现的,例如http协议的报文可以被定义为一个struct, 进一步解析其协议内容, 可以被解析为string当作普通的字符串,供日志分析。
函数式
近些年来,函数式编程的概念正在越来越流行,在OOP为主流编程范式的当下,大多数主流语言都原生支持的函数式这一编程范式。
- 函数是一等公民
这个概念很重要,因为在OOP的世界观中,对象有属性和方法,函数并不是一个对象,尤其在Java8中,函数式被缩减为lambda表达式和Stream。一等公民是指,函数是独立的,有独立的类型和约束。具体表现为,函数可以被随处定义,可以作为返回值,可以携带捕获的环境变量等。笔者一直很纠结不可变性这一逐渐成为主流的编程思想是否起源于函数式编程范式或者受其影响。如若排除filter, map等一些函数式的操作符,笔者认为不可变性对编程过程的影响更多一点。
回到函数式编程和软件的本质,我是否可以认为,函数式某些方面想通过, 固定的算法范式解决不同的数据结构的函数定义差异。这点在开发过程中是深有体会的。
上边的图像我们可以描述为flatmap或者concat, 或者一个普通的函数,接受两个参数,进行任何实现了+号运算符类型的算法,然后返回结果。
那么,这个函数就是一个具有抽象的函数,函数的实现可以是通过范型方法,或者高阶函数(2x+y), 这里的x和y取决于上下文, 这其实是一种抽象能力的进步,也是范型方法的补充。极大的提高了应用开发的灵活性。
数学
“数学是一门关于模式的科学,以抽象的形式表达自然界中的结构。” - 斯蒂芬·霍金
当我们聊到函数式带给我们强大的抽象能力时,我们就越接近数学,因为从计算机计算性的本质上来讲,我们编程的一切操作,最终都会落地于计算单元,无论是CPU,或者GPU或者任何处理转换或者说计算的物理结构,最简单的例子,例如上述z = 2x + y, 我们最终会调用加法器这一基础门电路。
纯函数表达
笔者接触函数式编程时,不太理解为什么要区分纯函数和有副状态函数,从概念上来讲,纯函数是指没有副作用的函数,其输出仅取决于输入,不会影响程序执行环境之外的任何东西。我一直有一个误解,纯函数在数学上的概念可以类比为幂等函数。但大部分函数f(x) = y都是幂等的,但实际编程过程中,修改环境变量并不影响方程f(x)=y的成立,也就是纯函数保证的不仅仅是输入和输出的相等,也要保证不会影响程序执行环境之外的任何东西, 这是一个需要在工程当中需要注意容易混淆的概念。
纯函数在数学上的表达我觉得最为贴切的是映射(Mapping), 这个概念会帮助我们理解函数式编程范式在程序开发中合适的领域,对集合的操作是非常擅长的,这也是我们经常使用的领域。
有状态表达
当你在使用函数式编程范式或者阅读相关书籍时,大多数场景下,希望你更多的使用函数式,因为在他们提供的case中,函数式优雅,错误率低,且并行效率高。但很明显我们大多数情况下,无论是现代主流的Flutter(新语言,新框架,无历史包袱)框架,或者其他的UI等框架,并没有主流使用纯函数的函数式编程范式,一方面是大部分App或者程序都是有状态的, 应用开发过程中状态是必然存在的,是一个复杂的集合,我们很少有单纯数学上的求值。
数学和编程的关联
如果计算机的仅仅被用作数值计算,我们可以类比为编程就是在解决数学问题,其中,数据的描述,类似,M =
[abcdefghi]\begin{bmatrix}
a & b & c \\
d & e & f \\
g & h & i \\
\end{bmatrix}adgbehcfi,其中M可以在不同语言中可以描述为class, 或者struct,统称为数据结构 而针对M的操作,例如缩放,平移等代数操作,我们称之为算法。即有f(M)=N, M为数据结构,f为算法。
现代编程中,编程或者软件并不局限于数值计算,IO操作是一种常见的副作用,但这种副作用操作一般是阶段性的,在整个计算过程中占有比例很小,所以我们依然可以简单的认为f(M)=N, M为数据结构,f为算法, 作为编程的核心理解是成立的。
何为协议
笔者相信,大多数程序员面试时都会被问过网络协议相关的内容,无论是底层的OSI七层模型,Tcp/ip或者应用层的Http,Https, 或者我们前后端定义的接口, 我们都可以称之为协议(protocol), 协议我们字面理解可以为约定, 那么约定或者协议是出于什么原因被建立,因小见大的推理,我们可以得出一个基本事实:
- 协议是为了保证双方正确沟通的约定
从这样一个基本事实出发去理解OSI协议族或者http协议,就可以看出协议其实是对信息的结构化表达,例如,一个基础的Http协议由如下格式组成
Method Request-URI HTTP-Version
headers CRLF
message-body
其中,格式一词在某些情况下,又可以被映射为struct或者单纯解析字符串,并返回,例如
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
if request_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
} else {
// some other request
}
}
从上述的例子我们可以推断,协议是一个沟通的约定(不考虑其他因素,如安全,效率等),而协议的实现是灵活的,只要最终传输的信息符合协议规范,都会被协议双方所接受并解析处理。那么数据结构在这个过程中扮演的角色就是如何更好的组织数据, 例如我们在实际开发过程中,经常会用到HttpRequest和HttpResonse这两个数据结构来承载协议的内容。但如何承载,如何更好的(效率,内存等)承载,是数据结构这个议题需要考虑的,例如http code使用u32还是u16等。
- 协议是沟通的约定,约定只规定沟通内容的格式,具体实现可以根据环境决定
笔者尝试为自己理解的协议下一个定义,肯定是不完全的,不全面的,但不妨碍笔者在一个不完整的定义下继续思考对编程的认知。
延展协议
我们将协议拓展来看,拓展到整个计算机,从0101的二进制,到类型系统,到http协议,最终到应用落地的http具体接口,会得出一个不曾被深度思考的结论。
协议贯穿了我们编程或者软件的生命周期, 协议本身是对0101二进制计算机处理数据基本单元的有意义解析。这里需要自我思考。
为何面向对象(OO)
我们稍微提及一些OO,因为本质上笔者认为OO是软件工程的主要议题,因为编程可以没有OO的思想,但当下软件开发却离不开OO的思想和约定。
例如在前端,客户端中中,我们都有一个一个概念叫Canvas, 与此同时有一个概念叫做Paint, 这其实是OO的功劳,在应用开发过程中,笔者的经验是面向对象的最大好处之一,大部分时候可以具象化到这个世界对应的概念,例如上述绘画需要基本要素是画布和画笔, 画笔可以有很多支,但画布只有一个,所以,canvas.drawXXX而不是paint.drawXXX, 这个概念具象化到我们的世界可以这样描述用红色画笔在画布上画一个XX。
为什么OO很重要,笔者经验是大部分应用或者说软件其实是将现实的物品抽取人们所关注的信息(属性), 然后在计算机中抽象出来。软件其实做的事情是模拟, 模拟现实世界,因为使用软件仍旧是人。这点我们不展开,每个人都有自己的见解。
阶段性
不知不觉就叨叨了很多,这些类似于碎碎念,在编程过程中,或者阅读过程中,一点一点补齐对自己职业,对这个领域的理解,笔者也确实认为一千个人眼中有一千个哈姆雷特,编程是一个非常大的话题,不同的技术栈,甚至使用不同的语言(例如Haskell)的人对编程的理解都是不同的。笔者这里仅仅一个初入编程世界的新手,未来还有很多的路要走,对这些事物的认知也在不断变化,但这不妨碍我阶段性总结一下,当是给自己一个新的起点吧。
愿诸君多思考,多实践,多总结。
转载自:https://juejin.cn/post/7364614337371865127