Swift:函数式编程学习之Thinking Functionally

前言

函数式编程(functional programming)诞生已经有五十多年的历史了,因为当时硬件的局限,这种编程范式一直没有成为主流。随着现在多核心CPU的普及,这种编程范式又慢慢回到我们的视线。在我看完中国首届Swift开发者大会的视频后,被这种充满魔性的编程方式所折服。我鼓足勇气开始接触这不可思议的编程范式。这系列的文章算是我阅读《functional programming in swift》时的读书笔记吧。本文以及以后这一些列文章涉及的所有代码包括习题之类的都会在Gighub对应章节之下。

Thinking Functionally

借用《functional programming in swift》中第二章的标题,开始我的摸索之路。

函数在Swift中是作为一等公民(first-class values)存在,也就是Swift中的函数可以当做参数传递给其他的函数;函数也能够当做其他函数的返回值。简而言之,Swift中的函数和Swift中IntStringBool是完全一样的!《functional programming in swift》Thinking Functionally章节便是通过一个战舰的例子来解释这一特征。

一个简单的例子

例子的背景是这样的:假设我们要设计一个海上战争的游戏,其中最主要的对象便是我们操作的战舰,既然是战争游戏,我们的战舰得具备射击其他战舰的功能。所以我们问题就可以归结于:给定一个其他战舰的位置,我们要判断我们所操作的战舰能否打中对方,也就是说需要判断其他战舰的位置是否在我们操作战舰的射击范围内。下面是示意图


我们的战舰位置和射击的范围

首先我们需要定义两个类型以便后面的操作

typealias Distance = Double //这里其实并不是必须的,是为了提高代码可读性 struct Position { var x: Double var y: Double }

既然我们的目标是要检查其他战舰是否在我们的射击范围内,所以不难想到我们需要定义一个方法去检查一点是否在是射击范围内,就像下面这个方法。

extension Position { func inRange(range: Distance) -> Bool { return sqrt(x * x + y * y) <= range } } Position(x: 1,y: 2).inRange(10)

敌人位置在(1,2),我们的射击范围是10的圈。看起来好像不错,但是这个方法仅仅只能用于我们的战舰在原点(0,0)的时候。所以我们需要重新设计方法,让他能够适应各种情况。下面是战舰不在原点时候的示意图。


我们的战舰位置和射击的范围

为了实现目标,引入了一个Ship结构,它拥有position属性、firingRange以及unsafeRange。

struct Ship { var position: Position // 表示这个船的位置 var firingRange: Distance // 表示这个船的最大攻击距离 var unsafeRange: Distance //表示最小的安全距离(暂且不用管) }

对象有了,然后我们对Ship进行扩展,让它能够判断其他船是否在其射击范围内。

extension Ship { func canEngageShip(target: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) return targetDistance <= firingRange } }

现在我们能够很方便的判断其他船是否在我们的攻击范围内了(无论我们船的位置在哪)

let otherShip1 = Ship(position: Position(x: 10.1,y: 0),firingRange: 10,unsafeRange: 5) let myShip1 = Ship(position: Position(x: 0,unsafeRange: 5) myShip1.canEngageShip(otherShip1) //return false let myShip2 = Ship(position: Position(x: 5,unsafeRange: 5) myShip2.canEngageShip(otherShip1) //return true

现在又有新的要求,我们的船不能攻击离自己太近的船(可以理解为炮弹在自己周围爆炸可能为伤害到自己),所以这样我们的安全射击的范围又变小了。下面是这种情况的示意图


白色区域是危险区域,浅粉色区域才是安全射击区域

根据要求,我们又要修改我们的代码

extension Ship { func canSafelyEngageShip(target: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) return targetDistance <= firingRange && targetDistance > unsafeRange } } //测试代码就不贴了

既然是战争游戏,我们拥有的船可能不止一艘,所以我们也不能攻击在距离友方船只过近的敌方船只(避免误伤)。示例图如下


右方船只距离地方船只较远,能够安全射击

右方船只距离地方船只较近,不能安全射击

所以,我们又要修改我们的代码,需要增加一个友方战舰信息的参数。

extension Ship { func canSafelyEngageShip1(target: Ship,friendly: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) let friendlyDx = friendly.position.x - target.position.x let friendlyDy = friendly.position.y - target.position.y let friendlyDistance = sqrt(friendlyDx * friendlyDx + friendlyDy * friendlyDy) return targetDistance <= firingRange && targetDistance > unsafeRange && (friendlyDistance > unsafeRange) } }

随着代码的演变,已经变得越来越难以维护,且可读性也是不高的。这个方法里面主要展示的是一系列复杂的计算。我们可以简单的整理一下我们的代码,使其看起来变得清晰。仔细观察上面的代码,会发现,其中绝大部分的计算是与Shipposition属性相关的,所以我们不妨把这些几何计算“交给”Position来处理。于是,便有了如下扩展。

extension Position { func minus(p: Position) -> Position { return Position(x: x - p.x,y: y - p.y) } var length: Double { return sqrt(x * x + y * y) } }

使用这些扩展的方法,我们的代码会变得简洁,且更具可读性。

 func canSafelyEngageShip2(target: Ship,friendly: Ship) -> Bool { let targetDistance = target.position.minus(position).length // 敌方船只距离我们的船只的距离 let friendlyDistance = friendly.position.minus(target.position).length // 敌方船只距离右方的船只的距离 return targetDistance <= firingRange && targetDistance > unsafeRange && (friendlyDistance > unsafeRange) }

这一段代码已经很好了,但我们可以进一步进行改变。

First-Class Functions

在上面的例子中,我们在Position中引入了两个方法从而使我们的代码变得简洁。我们不妨尝试使用同样的方式。

我们最原始最根本的问题是什么?最根本的问题无非是给定一个点,我们确定是否在某一个区域范围内。所以这个问题最原始的函数模型就像下面这个样子。

func pointInRange(point: Position) -> Bool { // Implement method here }

这个函数类型在接下类例子中十分重要,所以我们给他取一个别名:

typealias Region = Position -> Bool

这个“新”的类型,便是一个函数类型,它接受一个Position类型的参数并返回一个Bool类型的值。我们可以这样理解,Region这个类型功能就是把Position转化成Bool,你可能有疑问为什么需要这个转化?这其实就是这个例子问题的本质,我们就是想判断一个船的位置(代表参数Position)是否在一个区域里面(返回值:Bool)。那如何转化呢?那就得看Region具体的“值”了。

We consciously chose the name Region for this type,rather than something like CheckInRegion or RegionBlock. These names suggest that they denote a function type,yet the key philosophy underlying functional programming is that functions are values,no different from structs,integers,or booleans

接下来我们将会设计一系列的函数来演示。

第一个示例,我们定义一个圆心在原点的圆形。

func circle(radius: Distance) -> Region { return { point in point.length <= radius } } /*测试*/ let circleT = circle(10) circleT(Position(x: 1,y: 1)) //return true circleT(Position(x: 10,y: 10))//return false

通过let circleT = circle(10)我们获得了一个一个半径为10圆心在原点的一个圆circleT,它的类型是Region,它的本质是{ point in point.length <= radius }这样的一个闭包。当你传入一个Position参数时,列如上面的Position(x: 1,y: 1),此时闭包中point便有了值,接着进行point.length <= radius这个运算,所以整个调用方式circleT(Position(x: 1,y: 1))就比较具有可读性。

如果你对于circle这个函数(方法)的实现,理解有些困难,那么肯定是因为你对Swift中闭包(closure)这一概念的不熟悉。这是一个很较简单的闭包,其中涉及到唯一一个可能的难点便是闭包的值捕获。但如果你问我哪里返回了一个Bool类型的值,那么你可以自刎了。。。。

对于circle函数,利用闭包的知识可以进一步简化

func circle(radius: Distance) -> Region { return { $0.length <= radius } }

让我们回归正轨,正如前面时所说,我们的圆不可能永远在圆心,所以前面定义的circle方法在很多情况下就不适用。你可能立马想到,那简单,我们再定义一个函数,只需要添加一个位置参数不就行了么?然后你可能就写出下面的代码。

func circle2(radius: Distance,center: Position) -> Region { return { point in point.minus(center).length <= radius } }

这样看上去确实是一个不错的解决方式,但是如果你不仅只有圆这一个形状呢(比如:矩形、三角形。。。)?那么是不是意味着你得重复定义这样一个带有Position参数的函数?其实并不要,我们可以定义一个转换中心点的函数来解决这所有的转化问题。

func shift(region: Region,offset: Position) -> Region { return { point in region(point.minus(offset)) } } // 这样调用就能获得一个半径是10,圆心在(5,5)的圆形区域 let circleS = shift(circle(10),offset: Position(x: 5,y: 5))

This is one of the core concepts of functional programming: rather than creating increasingly complicated functions such as circle2

我们尽量不要去创建一个复杂的函数(方法)来解决我们所遇到的问题,而是我们需要把这个问题分解成其他的子问题,这些子问题都实现了一个属于自己的功能。然后我们通过对这些功能组合,来达成我们目标,或者解决问题。

既然是要通过函数的组合来实现不同的目的,我们不妨定义更多区域转换的函数。

func invert(region: Region) -> Region { return { point in !region(point) } } let invertT = invert(circle(10)) invertT(Position(x: 1,y: 1)) // return false invertT(Position(x: 10,y: 10)) // return true

这是一个取相反区域的操作,我们传入的是一个circle(10),经过invert取反后,只有在circle(10)之外的点才算是范围内。用个图可能比较好理解。


图中绿色的区域代表的是circle(10)所表示的Region,如果不经过invert函数,p1点在其范围内,p2不在。如果是经过invert生成的invertT区域,那所代表的区域便是除了绿色区域之外的所有区域。现在应该明白取反的意义了吧?

类似于这样的函数我们还可以定义很多。

//region1和region2相交的区域(交集) func intersection(region1: Region,region2: Region) -> Region{ return { point in region1(point) && region2(point)} } //region1加上region2的区域(并集) func union(region1: Region,region2: Region) -> Region { return { point in region1(point) || region2(point)} } // 在region中但不在minusRegion中的区域 func difference(region: Region,minusRegion: Region) -> Region { return intersection(region,region2: invert(minusRegion)) }

如果你喜欢,你可以利用数学中集合的交、并、补运算创造无数个方法。

上面几个简单的例子告诉我们,在Swift中函数作为参数传递给其他函数使用是和其他基本类型是完全一样的。上面几个例子每一个返回值都是一个不同的区域(region),我们可以通过组合这些函数来完成我们最初的目标。

回到之前战舰的例子,我们现在可以重构之前的方法

 func canSafelyEngageShip(target: Ship,friendly: Ship) -> Bool { let rangeRegion = difference(circle(firingRange),minusRegion: circle(unsafeRange)) let firingRegion = shift(rangeRegion,offset: position) let friendlyRegion = shift(circle(unsafeRange),offset: friendly.position) let resultRegion = difference(firingRegion,minusRegion: friendlyRegion) return resultRegion(target.position) }

因为测试用例代码篇幅偏长,就不贴了。你可以从Gighub中获得本篇文章的所有示例代码以及测试用例。

在这个方法里面,我们定义两个Region:firingRegionfriendlyRegion,然后通过difference函数求出在firingRegion之内并且不在friendlyRegion范围之内的一个新的Region:resultRegion。这个resultRegion便是我们战舰能够安全射击的所有区域!

简单描述一下这些函数是如何通过组合来达到我们的目标的:

  • 确认我们能够的射击范围: let rangeRegion = difference(circle(firingRange),minusRegion: circle(unsafeRange))我们先有一个大的圆形射击区域circle(firingRange),这个区域大小是由射击范围firingRange属性决定的,但我们不能射击距离自己太近的目标,也就是说不能射击距离自己位置少于unsafeRange的目标。因此这个范围得"挖去"circle(unsafeRange)这样的一个区域。所以使用difference方法。rangeRegion就表示我们能够射击的区域,但是从我们的船不一定是在原点,所以这整个射击区域是随着Ship对象的Position属性移动的。因此调用shift函数确认真实的射击区域。


    绿色能够的射击范围
  • 避免射击友军:当然我们不能攻击距离友方船只太近的范围。于是,射击范围又要“挖去”一块,这挖去的一块便是let friendlyRegion = shift(circle(unsafeRange),offset: friendly.position)这个friendlyRegion,于是很自然的再次调动difference函数“挖掉”这一块。得到最后能够安全射击的范围resultRegion

image
  • 真正的射击范围:经过上面两步我们已经得到能够安全射击的一个范围resultRegion(列如上图的绿色区域),所以现在只需要确认目标位置是否在安全射击范围resultRegion内就好。显然直接调用resultRegion(target.position)即可

我们使用Region这一个类型,通过把问题进行分解,定义一系列的辅助函数,并将其组合起来从而实现我们的功能需求。相比于第一个版本的canSafelyEngageShip1(target: Ship,friendly: Ship) -> Bool,代码更具可读性。

小结

《functional programming in swift》第二章内容并未说多少关于函数式编程本身,而是着重强调在Swift中作为一等公民(first-class value)存在的函数,在当做参数传递的过程中是和其他基本类型(Int、String等)完全等价的。而且让我感觉到似乎对问题的分解然后组合是非常关键的一个步骤。

练习

读完这篇文章,你还是可能啥也不知道,为了让读者或者说自己真正的学有所得,这系列文章我都会尽量想或者搜索一些与之对应的习题,来巩固理解。习题我都会尽自己最大努力和水平写出参考答案放在Gighub对应章节之下,但水平有限,如果您有更好的答案请您一定要联系我wxl19950606@163.com

本章的练习本人暂且未想出一个不同的用例,但是我可以使用书本本章遗留一个问题。当时我写完还是挺有收获和成就感的。

本篇文章最后呈现的方法canSafelyEngageShip实现相比之前canSafelyEngageShip2方法好像复杂了一些。所以《functional programming in swift》书中提到了另外一种实现方式。

应该定义一个这样的结构,来取代之前的Region类型

struct Region { let lookup: Position -> Bool }

然后我们可以为其扩展一些类似本文中出现的invertintersection之类的函数,来重新实现我们的canSafelyEngageShip方法。最后实现的方式《functional programming in swift》书中书说是类似于这样

rangeRegion.shift(ownPosition).difference(friendlyRegion)

是不是已经十分接近自然语言了?同学们朝着这个目标行动吧!

后续

我是函数式编程的爱好者,但在这方面也是一个全新的新手,大神们都说函数式编程的学习的曲线是非常复杂的,学习的代价也是非常大的。所以这是一门高深的学问,既然FP如此之难,笔者文章中难免会出现一些错误,还请大家多多指教。本文以及以后这一些列文章涉及的所有代码包括习题之类的都会在Gighub对应章节之下。

您可以通过邮件wxl19950606@163.com与我取得联系,谢谢您的支持^_^!

参考文献



文/疯狂的剁椒鱼头(简书作者) 原文链接:http://www.jianshu.com/p/88adcf2173dd 著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。