clojure定义函数
![](https://i-blog.csdnimg.cn/blog_migrate/d4cd164f02443303dcebf67e24931433.png)
经过长时间的学习和使用面向对象的编程,我退后了一步来思考系统的复杂性。
“Complexity is anything that makes software hard to understand or to modify.
“ —约翰·奥特豪特
经过研究,我发现了函数式编程概念,例如不变性和纯函数。 这些概念是构建无副作用功能的巨大优势,因此,维护系统更容易-还有其他好处 。
在本文中,我将通过许多代码示例向您详细介绍函数式编程和一些重要概念。
本文使用Clojure作为编程语言示例来解释函数式编程。 如果您对LISP类型的语言不满意,我也会在JavaScript中发布同一篇文章。 看看: Javascript中的函数式编程原理
什么是函数式编程?
函数式编程是一种编程范式-一种构建计算机程序的结构和元素的风格-将计算视为对数学函数的评估,并且避免了状态和可变数据的更改- 维基百科
纯功能
![](https://i-blog.csdnimg.cn/blog_migrate/40894c7e0d44e60c0b531f18f50c1d4f.png)
当我们想了解函数式编程时,我们学习的第一个基本概念是纯函数 。 但这到底是什么意思? 是什么使函数纯净?
那么我们如何知道一个函数是否pure
呢? 这是一个非常严格的纯度定义:
- 如果给定相同的参数,它将返回相同的结果(也称为
deterministic
) - 它不会引起任何明显的副作用
如果给定相同的参数,它将返回相同的结果
假设我们要实现一个计算圆的面积的函数。 一个不纯函数将接收radius
作为参数,然后计算radius * radius * PI
。 在Clojure中,运算符排在最前面,因此radius * radius * PI
变为(* radius radius PI)
:
为什么这是不纯功能? 仅仅是因为它使用了一个没有作为参数传递给函数的全局对象。
现在想象一些数学家认为PI
值实际上是42
并且会更改全局对象的值。
我们的不纯函数现在将导致10 * 10 * 42
= 4200
。 对于相同的参数( radius = 10
),我们得到不同的结果。 让我们修复它!
TA-DA🎉! 现在,我们将始终将PI
值作为参数传递给函数。 所以现在我们只是访问传递给函数的参数。 没有external object
。
- 对于参数
radius = 10
&PI = 3.14
,我们将始终具有相同的结果:314.0
- 对于参数
radius = 10
&PI = 42
,我们将始终具有相同的结果:4200
读取文件
如果我们的函数读取外部文件,则它不是纯粹的函数-文件的内容可以更改。
随机数生成
任何依赖随机数生成器的函数都不能是纯函数。
它不会引起任何明显的副作用
可观察到的副作用的示例包括修改全局对象或通过引用传递的参数。
现在我们要实现一个函数,以接收一个整数值并返回增加了1的值。
我们有对counter
。 我们的不纯函数会收到该值,然后将值增加1的值重新分配给计数器。
观察 :在函数式编程中不鼓励可变性。
我们正在修改全局对象。 但是,我们将如何使其pure
呢? 只需返回增加1的值即可。就这么简单。
看到我们的纯函数increase-counter
返回2,但是counter
值仍然相同。 该函数返回递增的值,而不更改变量的值。
如果我们遵循这两个简单的规则,就会更容易理解我们的程序。 现在,每个功能都是孤立的,无法影响我们系统的其他部分。
纯函数是稳定,一致和可预测的。 给定相同的参数,纯函数将始终返回相同的结果。 我们不需要考虑相同参数产生不同结果的情况-因为它永远不会发生。
纯功能的好处
该代码绝对更容易测试。 我们不需要嘲笑任何东西。 因此,我们可以对具有不同上下文的纯函数进行单元测试:
- 给定参数
A
→期望函数返回值B
- 给定参数
C
→期望函数返回值D
一个简单的示例是一个函数,该函数接收一个数字集合,并期望它递增该集合的每个元素。
我们接收到numbers
集合,将map
与inc
函数配合使用以递增每个数字,并返回一个新的递增数字列表。
对于input
[1 2 3 4 5]
,预期output
为[2 3 4 5 6]
。
不变性
随时间不变或无法更改。
![](https://i-blog.csdnimg.cn/blog_migrate/f6db29fd62431903e02ebb0dbbe9b183.png)
当数据不可变时,其状态无法更改 创建之后。 如果要更改不可变对象,则不能。 而是使用新值创建一个新对象。
在Javascript中,我们通常使用for
循环。 接下来的for
语句具有一些可变变量。
对于每次迭代,我们都会更改i
和sumOfValue
状态 。 但是,我们如何处理迭代中的可变性? 递归! 回到Clojure!
因此,这里有sum
函数,用于接收数值向量。 该recur
跳回loop
,直到我们得到了向量空( 我们的递归 base case
)。 对于每个“迭代”,我们会将其值添加到total
累加器中。
通过递归,我们保留变量 一成不变的。
观察 :是的! 我们可以使用reduce
来实现此功能。 我们将在“ Higher Order Functions
主题中看到这一点。
建立对象的最终状态也很常见。 假设我们有一个字符串,并且我们想将此字符串转换为url slug
。
在Ruby的OOP中,我们将创建一个类,例如UrlSlugify
。 这节课会有一个slugify!
将字符串输入转换为url slug
。
美丽! 它实现了! 在这里,我们必须进行命令式编程,确切地说出每个slugify
处理过程中要执行的操作-首先是小写字母,然后删除无用的空格,最后用连字符替换其余的空格。
但是我们在这个过程中正在改变输入状态。
我们可以通过执行功能组合或功能链接来处理此突变。 换句话说,函数的结果将用作下一个函数的输入,而无需修改原始输入字符串。
这里我们有:
-
trim
:删除字符串两端的空格 -
lower-case
:将字符串转换为所有小写 -
replace
:用给定字符串中的替换替换匹配的所有实例
我们结合了所有三个功能,可以"slugify"
字符串。
说到组合函数 ,我们可以使用comp
函数来组合所有三个函数。 让我们来看看:
参照透明
![](https://i-blog.csdnimg.cn/blog_migrate/95f572bd199fa1c3cbd674882cf99185.png)
让我们实现一个square function
:
给定相同的输入,此(纯)函数将始终具有相同的输出。
传递“ 2”作为square function
的参数将始终返回4。因此,现在我们可以将(square 2)
替换为4。就是这样! 我们的功能是referentially transparent
。
基本上,如果一个函数对于相同的输入始终产生相同的结果,则它是参照透明的。
纯函数+不可变数据=参考透明
有了这个概念,我们可以做的一件很酷的事情就是记住该功能。 想象一下我们具有以下功能:
(+ 5 8)
等于13
。 此功能将始终导致13
。 因此,我们可以这样做:
这个表达式将始终为16
。 我们可以将整个表达式替换为数值常量并进行记忆 。
作为一流实体
![](https://i-blog.csdnimg.cn/blog_migrate/26ac73e88651b6c65bb7184b1788c1f7.png)
函数作为一等实体的想法是将函数也视为值并用作数据。
在Clojure中,通常使用defn
定义函数,但这只是(def foo (fn ...))
语法糖。 fn
返回函数本身。 defn
返回一个指向函数对象的var
。
作为一流实体的功能可以:
- 从常量和变量中引用它
- 将其作为参数传递给其他函数
- 作为其他函数的结果返回
这个想法是将函数视为值,并像数据一样传递函数。 这样,我们可以组合不同的功能来创建具有新行为的新功能。
想象一下,我们有一个将两个值相加然后将值加倍的函数。 像这样:
现在,一个将值相减并返回双精度值的函数:
这些功能具有相似的逻辑,但区别在于运算符功能。 如果我们可以将函数视为值并将其作为参数传递,则可以构建一个接收操作符函数并在函数内部使用的函数。 让我们来构建它!
做完了! 现在我们有一个f
参数,并用它来处理a
和b
。 我们传递了+
和-
函数与double-operator
函数组合在一起,并创建了新行为。
高阶函数
当我们谈论高阶函数时,我们指的是以下函数之一:
- 将一个或多个函数作为参数,或
- 返回一个函数作为其结果
我们上面实现的double-operator
函数是一个高阶函数,因为它接受一个operator函数作为参数并使用它。
您可能已经听说过filter
, map
和reduce
。 让我们来看看这些。
过滤
给定一个集合,我们想按属性过滤。 筛选器函数期望使用true
或false
值来确定是否应将元素包含在结果集合中。 基本上,如果回调表达式为true
,则过滤器函数会将元素包括在结果集合中。 否则,它将不会。
一个简单的例子是当我们有一个整数集合并且我们只需要偶数时。
势在必行
使用Javascript的一种必要方法是:
- 创建一个空向量
evenNumbers
- 遍历
numbers
向量 - 将偶数推到
evenNumbers
向量
我们可以使用filter
高阶函数来接收even?
函数,并返回偶数列表:
我在“ 黑客评级FP路径”上解决的一个有趣的问题是“ 过滤器阵列”问题 。 问题的思想是过滤给定的整数数组,并仅输出小于指定值X
那些值。
解决此问题的强制性Javascript解决方案如下:
我们确切地说出函数需要做的事情–遍历集合,将集合当前项与x
进行比较,如果该元素通过条件,则将其推送到resultArray
。
声明式方法
但是,我们需要一种更具声明性的方式来解决此问题,并且还需要使用filter
高阶函数。
声明式Clojure解决方案如下所示:
首先,这种语法似乎有些奇怪,但是很容易理解。
#(> x %)
只是一个匿名函数,它接收x
并将其与集合中的每个元素进行比较。 %
表示匿名函数的参数,在本例中为filter
内部的当前元素。
我们也可以使用地图来做到这一点。 想象一下,我们有一幅name
和age
的地图。 并且我们只希望过滤特定年龄段的人员,在此示例中,年龄超过21岁的人员。
代码摘要:
- 我们有一个人的名单(
name
和age
)。 - 我们有匿名函数
#(< 21 (:age %))
。 还记得%
代表集合中的当前元素吗? 好吧,集合的元素是人脉地图。 如果我们这样做(:age {:name "TK" :age 26})
,它将返回年龄值,在这种情况下为26
。 - 我们基于此匿名函数过滤所有人员。
地图
map的想法是转换集合。
map
方法通过将函数应用于其所有元素并根据返回的值构建新集合来转换集合。
让我们得到上面的同一people
集合。 我们现在不想按“年龄超过”进行过滤。 我们只想要一个字符串列表,例如TK is 26 years old
。 因此,最后一个字符串可能是:name is :age years old
,其中:name
和:age
是people
集合中每个元素的属性。
以命令式Javascript方式,它将是:
以声明性的Clojure方式,它将是:
整个想法是将给定的集合转换为新的集合。
另一个有趣的Hacker Rank问题是更新列表问题 。 我们只想用它们的绝对值更新给定集合的值。
例如,输入[1 2 3 -4 5]
需要输出为[1 2 3 4 5]
。 -4
的绝对值为4
。
一个简单的解决方案是就每个集合值进行就地更新。
我们使用Math.abs
函数将值转换为其绝对值,并进行就地更新。
这不是实现此解决方案的功能方法。
首先,我们了解了不变性。 我们知道不变性对于使我们的功能更加一致和可预测非常重要。 这个想法是建立一个具有所有绝对值的新集合。
第二,为什么不使用map
来“转换”所有数据?
我的第一个想法是建立一个to-absolute
函数以仅处理一个值。
如果它是负数,我们希望将其转换为正值(绝对值)。 否则,我们不需要对其进行转换。
现在我们知道如何对一个值进行absolute
运算,我们可以使用此函数作为参数传递给map
函数。 您还记得higher order function
可以接收函数并将其用作参数吗? 是的,地图可以做到!
哇。 如此美丽! 😍
减少
reduce的想法是接收一个函数和一个集合,并返回通过组合项目创建的值。
人们谈论的一个常见示例是获取订单的总额。 假设您在购物网站上。 您已将Product 1
, Product 2
, Product 3
和Product 4
到购物车(订单)。 现在,我们要计算购物车的总金额。
以必要的方式,我们将迭代订单清单,并将每个产品的数量加总为总数量。
使用reduce
,我们可以构建一个函数来处理amount sum
并将其作为参数传递给reduce
函数。
在这里,我们有shopping-cart
,接收当前total-amount
的功能sum-amount
和将它们sum
的current-product
对象。
使用sum-amount
并从0
开始使用get-total-amount
功能来reduce
shopping-cart
。
获得总量的另一种方法是map
和reduce
。 那是什么意思 我们可以使用map
将shopping-cart
转换为amount
值的集合,然后仅将reduce
函数与+
函数一起使用。
get-amount
接收产品对象并仅返回amount
值。 所以我们这里是[10 30 20 60]
。 然后reduce
将所有项目相加在一起。 美丽!
我们看了每个高阶函数的工作方式。 我想向您展示一个示例,说明如何在一个简单示例中组合所有这三个函数。
谈论shopping cart
,想象一下我们的订单中有以下产品清单:
我们想要购物车中所有书籍的总数。 就那么简单。 算法?
- 按书籍类型过滤
- 使用地图将购物车转化为金额的集合
- 通过将所有项目与reduce相加来合并
做完了! 🎉
资源资源
我整理了一些阅读和学习的资源。 我正在分享我发现非常有趣的内容。 有关更多资源,请访问我的Functional Programming Github存储库 。
简介
纯功能
不变的数据
- 用于功能编程的不可变DS
- 为什么共有的可变状态是万恶之源
- Clojure中的结构共享:第1部分
- Clojure中的结构共享:第二部分
- Clojure中的结构共享:第3部分
- Clojure中的结构共享:最后一部分
高阶函数
声明式编程
而已!
大家好,我希望您在阅读这篇文章时玩得开心,也希望您在这里学到了很多东西! 这是我分享自己所学知识的尝试。
来跟我学习。 我正在这个“ 学习功能编程”存储库中共享资源和代码。
希望您在这里看到了对您有用的东西。 下次见! :)
TK。
翻译自: https://hackernoon.com/clojure-functional-programming-38cc6a9298f5
clojure定义函数