在学习 Haskell 之前,作者一直使用主流语言,如 Java、C 和 C++——现在他仍然喜欢它们。那么,一个命令式开发人员如何转变成了一个 Haskell 开发者?他将在本文中将对此做出解释——尤其是对那些在函数式编程方面经验较少的开发人员。
首先,我将通过对一些主题的讨论比较函数式编程和面向对象编程,因为它是最流行的范式。在第一个代码示例中,我将简要介绍 Haskell 的语法,因为我将在本文中使用它。
控制流描述你如何告诉程序做什么——形成算法。基本控制元素有以下三种:
顺序——顺序执行代码
重复——重复执行代码
选择——根据条件将代码划分成分支
顺序是语句逐行执行
重复是循环,如 for 或 while 语句,或递归
选择是 if … else 或 switch 语句
下面这个简单的例子使用 Java 实现文本居中显示。该文本是作为一个字符串数组传入的。每一行是这个数组的一个元素:
void alignCenter(String[] text)
{
int maxLength = 0;
for (String line : text) {
if (line.length() > maxLength) {
maxLength = line.length();
}
}
for (int i = 0; i < text.length; ++i)
{
int spaceCount = (maxLength - text[i].length()) / 2;
StringBuilder builder = new StringBuilder();
for (int j = 0; j < spaceCount; ++j)
{
builder.append(' ');
}
builder.append(text[i]);
text[i] = builder.toString();
}
}
顺序是链式调用
重复是递归
选择是模式匹配,或 case … of 或 if … else 表达式
下面是同一个例子的 Haskell 实现,展示模式匹配和递归的用法:
alignCenter :: [String] -> [String]
alignCenter xs = alignCenter' maxLength xs
where maxLength = maximum (map length xs)
alignCenter' :: Int -> [String] -> [String]
alignCenter' _ [] = []
alignCenter' n (x:xs) = (replicate spaceCount ' ' ++ x) : alignCenter' n xs
where spaceCount = div (n - length x) 2
下面是一个没有使用递归的简化版本,使用了 map 和 lambda 函数:
alignCenter :: [String] -> [String]
alignCenter xs = map (\x -> replicate (div (n - length x) 2) ' ' ++ x) xs
where n = maximum (map length xs)
函数的第一行是签名。签名 alignCenter :: [String] -> [String] 告诉我们这是一个名为 alignCenter 的函数,其输入是一个字符串列表,输出是一个新字符串列表(从左往右读)。
第一个函数确定字符串列表中最长的行,并调用第二个函数。我们通过一个简单的表达式 maximum (map length xs) 终止第一个循环。那么它是如何工作的?让我们看下涉及到的所有函数的签名。
length :: [a] -> Int
map :: (a -> b) -> [a] -> [b]
maximum :: [a] -> a
length 函数的输入是一个任意类型的列表,输出是一个 Int 值。类型签名中的所有小写类型都是类型变量,类似于 Java 中 List
map 函数接收两个参数,第一个是 a -> b 类型的函数,第二个是 [a],返回值是 [b]。 那么,“它接收一个函数作为参数”是什么意思呢?是的,这是真的。你可以将函数作为参数传递,不过不能是函数指针(如 C 语言中),也不能是方法引用(如 Java 语言中),要是作为第一类值的真正函数。以函数为参数或返回新函数作为结果的函数称为高阶函数。那么,这个函数是干什么用的呢?它将 [a] 的每个元素传递给 a -> b 函数,后者将 a 转换为 b,并把它们汇集到一个新列表 [b] 中。
现在让我们解析下类型变量 map length xs,其中,xs 是 [String] 类型。
map :: (String -> Int) -> [String] -> [Int]
你需要知道 String 是 [Char] 类型的同义词,表示字符列表。这就是为什么它兼容 length 函数。表达式 map length [“Hello”, “World!”] 会被解析成 [5, 6]。我们感兴趣的是列表中最长字符串的长度,因此,我们将结果列表传给 maximum,它会返回列表中长度最大的元素,即 6。
我们看下第二个函数:
alignCenter' :: Int -> [String] -> [String]
你可能已经注意到函数名末尾的’。没有什么特别的,它只是 Haskell 中一个有效的标识符字符,因为它在数学中是一个常用符号,表示与先前标识符相关的名称。该函数是递归的,我们遍历文本的每一行,进行转换,并将转换后的行放在所有剩余元素的递归调用之前。
alignCenter’ _[] =[] 这行代码是递归基本型。它的意思是:如果第二个参数是空列表,那么返回一个空列表,因为没有什么可做。在这种情况下,我们对第一个参数的值不感兴趣,所以我们不需要为它命名而只需要以 _ 表示。
以下几行代码就完成了整个工作:
alignCenter' n (x:xs) = (replicate spaceCount ' ' ++ x) : alignCenter' n xs
where spaceCount = div (n - length x) 2
我们将第一个参数绑定到 n,将第二个参数(一个列表)与模式 (x:xs) 进行匹配,这意味着:将列表的第一个元素绑定到 x,其余所有元素绑定到 xs。我们会根据需要复制空格,将它们与当前元素 x 串在一起,并在所有剩余的元素 xs 递归调用的结果列表前加上:。就这些。
在递归操作(reduction step)之前声明递归的结束条件(base case)非常重要,因为编译器自顶向下运行,并采用它找到的第一个匹配模式。
与相同代码的 OOP 版本相比,我们使用模式匹配和抽象函数节省了大量代码。好了,现在你可能会抱怨:“嗯,你只是把整个代码隐藏在库函数里了,比如 replicate、map 和 maximum”——我告诉你:“是的,当然!因为我不需要成千上万次地重复编写同样的 for 循环!”老实说,Java 代码可以使用 leftPad 之类的东西来复制空格,但它是一个非常具体的函数,专门用于填充字符串,没有其他用途。
在函数式编程中,你能够以一种简单的方式抽象常见的循环用例来执行映射、过滤、折叠和展开等任务。在 OOP 中,如果没有大量的样板代码(如后台 接口 和内置语法糖),你将无法实现这样优雅的解决方案。
这些概念描述了构建应用程序的基本思想。代码、数据及其交互在各自的范式中是如何表示的?
面向对象编程引入了接口、类、继承和对象的概念。对象包含数据字段和方法代码,这些方法通过操作字段来更改对象状态。
函数式编程的核心是函数。与 OOP 中的方法相比,你能用它做的事情更多:
把函数传递给其他函数
将新函数作为函数的求值结果返回
将两个函数组合成一个新函数
使用函数的一部分构建一个新函数
函数求值的输出只取决于它的输入。这意味着不存在可以影响函数结果的隐藏变量。这大大提高了可测试性。
数据由代数数据类型 表示。在函数式编程中,你不需要像类那样将数据和代码放在容器中。你将构建一组数据类型和一组单独的函数,这些函数对这些类型进行操作。数据类型不知道它们被哪些函数使用,因为它们对函数一无所知,而且每个函数都不知道还有其他函数也对相同的数据类型进行操作。
下面是 Haskell 中数据类型的一些例子,只是让你感受下它们是如何定义的:
data Bool = True | False
data Customer = Customer Int String
data Customer' = Customer' {
customerId :: Int,
customerName :: String
}
总是有一个数据类型名称和一个以|分隔的构造函数名称列表,其中包含可选参数。第一个示例很简单。第二个示例有一个与类型同名的构造函数和两个参数。最后一个示例与前面的示例相同,但是使用了命名参数,这称为记录语法。
Haskell 中的数据是不可变的,这意味着你不能更改 Customer 的姓名,而是需要用新姓名创建一个客户。
假设,你有一个现实世界的问题需要解决。第一步做什么?试着把问题分解成更小的问题,然后再进一步细分下去。然后,描述你的问题,这意味着将你的问题放入你选择的编程语言的俚语中。
在 OOP 的情况下,你需要发现类及其字段和方法,找到相似性,将它们放入抽象类中,并最终通过派生这些抽象类来构建可以供使用的具体类。
FP 则是从函数开始。一个函数处理一个非常小的问题,它操作非常小的类型。在理想情况下,类型完全包含函数所需的信息,不多不少。这可以保证类型和函数几乎不需要更改,即使你完全重构了应用程序的其余部分,除非你的问题发生了变化。事实证明,你还会将你的逻辑类型或业务实体分解为小的技术类型,从而实现无痛且安全的重构。
耦合描述组件之间依赖关系以及一个组件的变化对其他组件的影响。
彼此通信的对象是紧耦合的。限制耦合的一种方法是应用诸如 依赖倒置 之类的原则,即你应该通过抽象(如接口)而不是实现(如类)进行通信。
为我们希望其交换信息的实现定义接口。为了避免出现很大的通用接口,一个接口应该只包含几个高内聚方法——这称为 接口隔离 。从长远来看,如果做得不对,你很可能会遇到虚拟接口实现,比如抛出 UnsupportedOperationException 异常或在空方法体中返回虚拟值。
当涉及到接口实现时,你经常添加抽象类来实现接口的某些部分,未受影响的接口方法仍由具体实现来实现——这就是继承的原理,这是 OOP 中最紧密的耦合。
面向对象和继承的思想是为了使编程更接近现实世界。我们都知道这样的例子:“对于 Car 和 Truck 这两个派生类,有两个基类 Vehicle 和 Ship。可是,Amphibian 怎么处理?”它有两个基类的特征——所以你需要 多重继承 ,但因为钻石问题,这是一个坏主意。为了解决这些问题,开发人员引入了 组合优于继承 的原则,这意味着你应该用可替换的组件组合对象。显然,组合优于继承有点违背 OOP 的原始关键概念之一——继承。
如你所见,一切都关乎正确的类和接口结构——为了设计出一个好的软件设计,还有很多 原则、 反原则 和 模式 需要你关注。
最后但同样重要的是,下面这个简单的例子展示了如何使用依赖倒置原则实现排序算法与比较逻辑的松耦合,该例子使用接口作为抽象:
interface Comparator<T>
{
int compare(T o1, T o2);
}
class ArcaneComparator<T> implements Comparator<T>
{
public int compare(T o1, T o2)
{
// 在这里插入晦涩难懂的比较实现
}
}
class Arrays
{
static <T> void sort(T[] a, Comparator<? super T> c)
{
// 使用比较器 c,
// 不需要了解具体实现
}
}
FP 是组合组件而不是耦合组件。FP 中的松耦合函数是指通过识别相似性来抽象函数,提取细节,构建高阶函数,并用细节参数化它们。
让我们来看看下面的情况:
sortById :: [Customer] -> [Customer]
sortByName :: [Customer] -> [Customer]
有两个函数做同样的事情——它们按照某些标准进行排序。那么,为什么我们不把相似点放到一个新的函数中来防止重复呢?
data Ordering = LT | EQ | GT
...
sort :: (Customer -> Customer -> Ordering) -> [Customer] -> [Customer]
compareId :: Customer -> Customer -> Ordering
compareName :: Customer -> Customer -> Ordering
或使用一个类型同义词:
type Compare = Customer -> Customer -> Ordering
sort :: Compare -> [Customer] -> [Customer]
compareId :: Compare
compareName :: Compare
sort 的第一个参数是 Customer -> Customer -> ordering 类型的函数,这意味着它接收两个客户,对于小于、等于或大于的情况,分别返回 LT、EQ 或 GT。这有什么不同呢?我们分解出了上述用于对列表进行排序的标准。我们现在可以写成 sort compareId 而不是 sortById。如果你还想叫它 sortById,也很容易做到:
sortById :: [Customer] -> [Customer]
sortById customers = sort compareId customers
或者:
sortById :: [Customer] -> [Customer]
sortById = sort compareId
如果你是最近才接触函数式编程,那么第二个版本在你看来可能有点不够清晰,所以我建议你好好看看第一个版本。如果你对第二种方法感兴趣,你可以进一步阅读,这称为 Eta 变换 。
sort 函数仍然依赖于 Customer 类型,这已经不重要了,因为这些细节被分解了。只有 compare 函数对类型的细节感兴趣。所以我们可以用一个类型变量替换它:
sort :: (a -> a -> Ordering) -> [a] -> [a]
我们可以用任何一种方式表达相同的功能。在 OOP 中,我们使用了一些语言特性,比如接口以及实现该接口的类。在 FP 中,我们有函数。类型 a -> a ->Ordering 表示接口,与该类型匹配的每个函数都可能是该接口的实现。
在我个人看来,我觉得函数式编程比面向对象编程干净得多。
在编写相同的功能时,你可以:
更抽象
编写更少代码
使用更少的样板特性
而且:
更可维护
更稳定
更有趣
非常感性您耐心地读完这篇文章!
原文链接:https://morgenthum.dev/articles/why-prefer-fp
如果你真的做出了一些东西,在面对那些令人眼花缭乱的理论知识,或是和你相似甚至比你做的更糟糕的人时大可不必谦虚。在一天结束之时,正是那些在战壕中的开发者——构建、测试和开发了代码的人,真正做了事情。
这些事情可以帮助新手在他们漫长的旅程中学习编程。我知道我还有更多东西需要学习,并将继续学习如何永远地学习。最重要的事情说三遍,请继续,不要放弃,不要放弃,不要放弃。
Javascript代码异步执行的场景,比如ajax的调用、定时器的使用等,在这样的场景下也经常会出现这样那样匪夷所思的bug或者糟糕的代码片段,那么处理好你的Javascript异步代码成为了异步编程至关重要的前提
以买苹果为例说明程序员如何解决问题。程序员需要对问题进行透彻的分析,理清其涉及的所有细节,预测可能发生的所有意外与非意外的情况,列出解决方案的所有步骤,以及对解决方案进行尽量全面的测试。而这些正是我认为编程难的地方。
Google Blockly 是一款基于Web的、开源的、可视化程序编辑器。你可以通过拖拽块的形式快速构建程序,而这些所拖拽的每个块就是组成程序的基本单元。可视化编程完成
成为伟大的程序员,需要付出许多编程之外的努力。我们的大脑是有限的,每天要应付的问题复杂到足以让人精神崩溃。当工作不顺利时,多少都会有些冒名顶替症候群的感觉。
推荐8款最好用的前端开发工具供美工或者前端开发人员使用,当然若你是NB的全栈工程师也可以下载使用。Web前端开发最常见的编程软件有以下几种: 在前端开发中,有一个非常好用的工具,Visual Studio Code,简称VS code
学编程现在看起来挺简单,因为网上有丰富的各种资源。然而当你实际去学的时候就发现,还是很难!对我来说也一样。但从某天起,我决定认认真真学编程一年。后来又过了一年,又过了一年又一年……我好像有点感悟。
命名最好遵循驼峰法和下划线法,并且要清楚的表达变量的意思。相对于驼峰法而言,我更喜欢下划线法。下划线法可以更清楚的看出这个变量表示的意思。比如aBigGreenBanana和一个a_big_green_banana。
每隔几个月就会出现一篇文章表明:CSS并不是真正的编程语言。以编程语言的标准来说,CSS过于困难。使用这门语言会很有创造性:事实确实如此,CSS不同于传统的编程,且具有缺陷,同任何标准化编程语言相比
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!