函数式swift - CSDN
精华内容
参与话题
  • Objc中国 - 函数式Swift (Swift4)(包含pdf、epub、mobi三种格式)
  • 最近在公司用swift做开发,也开始关注一些swift的语言风格,所以接下来的博客以swift语言为主。oc或者swift有什么问题可以一起交流。 一、委托模式 1、使用过程  协议最常见的用法莫过于进行代理传...

    Protocol(协议)专题

    demo链接–>https://github.com/PeipeiQ/MySwift
    我的个人博客->http://www.peipeiq.cn
    最近在公司用swift做开发,也开始关注一些swift的语言风格,所以接下来的博客以swift语言为主。oc或者swift有什么问题可以一起交流。

    一、委托模式

    1、使用过程

      协议最常见的用法莫过于进行代理传值,这就是委托模式。常用的应用场景有:controller中自定义了一个view,view中又添加了一个自定义view。在自定义的view中如果有些函数或者属性需要到controller中去调用,委托模式的做法就是规定一个协议,让controller去遵守一个协议并提供实现,那么在自定义view中就能使用协议中的方法。
      举个例子,现在想在一个controller中添加一个自定义view,可以实现点击view中按钮更改controller中label的值。简单的代码如下:
      自定义view

    //SelectTabbar.swift
    @objc protocol SelectTabbarDelegate {
        func changeLabel(_ str: String)
    }
    //SelectTabbar.swift
     class SelectTabbar: UIView {
        var keywords : [String]?
        var buttons : [UIButton]?
        weak public var delegate : SelectTabbarDelegate?
    
        init(frame: CGRect,keywords:[String]) {
            super.init(frame: frame)
            self.keywords = keywords
            renderView()
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    
        override func layoutSubviews() {
            super.layoutSubviews()
        }
    
        private func renderView(){
            buttons = keywords?.enumerated().map({ (index,key) ->UIButton in
                let buttonWidth = kScreenWidth/CGFloat((keywords?.count)!)
                let button = UIButton.init(frame: CGRect.init(x: CGFloat(index)*buttonWidth, y: 0, width: buttonWidth, height: 50))
                button.setTitle(key, for: .normal)
                button.setTitleColor(UIColor.blue, for: .normal)
                button.backgroundColor = UIColor.gray
                button.tag = index
                button.addTarget(self, action: #selector(tapButton(sender:)), for: .touchUpInside)
                addSubview(button)
                return button
            })
        }
    
        @objc func tapButton(sender: UIButton){
            delegate?.changeLabel(keywords![sender.tag])
        }
    
    }

      controller:

    class TestViewController: UIViewController,SelectTabbarDelegate {
    
        lazy var label : UILabel = {
            var label = UILabel(frame: CGRect.init(x: 50, y: 200, width: 100, height: 30))
            label.text = labelStr
            label.backgroundColor = UIColor.red
            return label
        }()
    
        private var labelStr : String? {
            didSet{
                label.text = labelStr
            }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .white
            view.addSubview(label)
            setupSelectTabbar()
        }
    
        func setupSelectTabbar(){
            let selectTabbar = SelectTabbar(frame: CGRect.init(x: 0, y: kNavigationHeightAndStatuBarHeight, width: kScreenWidth, height: 50),keywords:["aa","bb"])
            selectTabbar.delegate = self
            view.addSubview(selectTabbar)
        }
    
        func changeLabel(_ str: String) {
            labelStr = str
        }
    
    }

      这样就能比较清楚的表明自己的逻辑。否则,如果要在view操作controller的内容,则需要在外部操作controller的实例,这就造成一个问题,就是无法操作实例中的私有属性和私有方法(虽然iOS是一门动态语言,不存在绝对的私有,但是谁会去一直去使用runtime来进行操作呢)。
      

    2、注意点

      在 ARC 中,对于一般的 delegate,我们会在声明中将其指定为 weak,在这个 delegate 实际的对象被释放的时候,会被重置回 nil。这可以保证即使 delegate 已经不存在时,我们也不会由于访问到已被回收的内存而导致崩溃。ARC 的这个特性杜绝了 Cocoa 开发中一种非常常见的崩溃错误,说是救万千程序员于水火之中也毫不为过。
      在 Swift 中我们当然也会希望这么做。但是当我们尝试书写这样的代码的时候,编译器不会让我们通过:

      'weak' cannot be applied to non-class type

    原因:这是因为 Swift 的 protocol 是可以被除了 class 以外的其他类型遵守的,而对于像 struct 或是 enum 这样的类型,本身就不通过引用计数来管理内存,所以也不可能用 weak 这样的 ARC 的概念来进行修饰。
    两种解决方法:
    1、使用@objc
    2、声明类类型专属协议。通过添加 class 关键字来限制协议只能被类类型遵循,而结构体或枚举不能遵循该协议。class 关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前
    protocol SelectTabbarDelegate : class

    二、AOP编程思想的运用

    首先我们理解下AOP的含义。

    In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior to existing code (an advice) without modifying the code itself, instead separately specifying which code is modified via a “pointcut” specification, such as “log all function calls when the function’s name begins with ‘set’”. This allows behaviors that are not central to the business logic (such as logging) to be added to a program without cluttering the code, core to the functionality. AOP forms a basis for aspect-oriented software development.

    在swift简单来说,就是利用协议去切入某些代码中,将额外的功能单独出来而不产生耦合,可以将这些与主逻辑关系不大的代码统一放到一起。
    常用的场景:日志记录,性能统计,安全控制,事务处理,异常处理等等。
    接上面的例子,我们需要在打开TestViewController的时候统计一次,点击两个按钮的时候也进行统计,统计的内容由identifer进行区分。
    我们先建立一个Statistician.swift 来存放我们的统计逻辑。(模拟实现)
    申明一个StatisticianProtocal协议并提供他的默认实现。

    import Foundation
    enum LogIdentifer:String {
        case button1 = "button1"
        case button2 = "button2"
        case testViewController = "testViewController"
    }
    
    protocol StatisticianProtocal {
        func statisticianLog(fromClass:AnyObject, identifer:LogIdentifer)
        func statisticianUpload(fromClass:AnyObject, identifer:LogIdentifer)
        //用一个尾随闭包来扩展功能
        func statisticianExtension(fromClass:AnyObject, identifer:LogIdentifer, extra:()->())
    }
    
    extension StatisticianProtocal{
        func statisticianLog(fromClass:AnyObject, identifer:LogIdentifer) {
            print("statisticianLog--class:\(fromClass) from:\(identifer.rawValue)")
        }
    
        func statisticianUpload(fromClass:AnyObject, identifer:LogIdentifer) {
            print("statisticianUpload--class:\(fromClass) from:\(identifer.rawValue)")
        }
    
        func statisticianExtension(fromClass:AnyObject, identifer:LogIdentifer, extra:()->()){
            extra()
        }
    }
    
    class Statistician: NSObject {
    
    }

    接下来在任何需要统计的类里面,我们让这个类去遵守这个协议,然后在需要的地方调用协议中的方法即可。如果在某个特定的类中需要调用的方法略有不同,重写协议中的方法即可。

    class SelectTabbar: UIView,StatisticianProtocal {
        var keywords : [String]?
        var buttons : [UIButton]?
        weak public var delegate : SelectTabbarDelegate?
    
        init(frame: CGRect,keywords:[String]) {
            super.init(frame: frame)
            self.keywords = keywords
            renderView()
            //进行一次统计
            operateStatistician(identifer: .testViewController)
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    
        override func layoutSubviews() {
            super.layoutSubviews()
        }
    
        private func renderView(){
            buttons = keywords?.enumerated().map({ (index,key) ->UIButton in
                let buttonWidth = kScreenWidth/CGFloat((keywords?.count)!)
                let button = UIButton.init(frame: CGRect.init(x: CGFloat(index)*buttonWidth, y: 0, width: buttonWidth, height: 50))
                button.setTitle(key, for: .normal)
                button.setTitleColor(UIColor.blue, for: .normal)
                button.backgroundColor = UIColor.gray
                button.tag = index
                button.addTarget(self, action: #selector(tapButton(sender:)), for: .touchUpInside)
                addSubview(button)
                return button
            })
        }
    
        @objc func tapButton(sender: UIButton){
            //进行一次统计
            switch sender.tag {
              case 0:operateStatistician(identifer: .button1)
              default:operateStatistician(identifer: .button2)
            }
            delegate?.changeLabel(keywords![sender.tag])
        }
    
        func operateStatistician(identifer:LogIdentifer){
            statisticianLog(fromClass: self, identifer: identifer)
            statisticianUpload(fromClass: self, identifer: identifer)
            statisticianExtension(fromClass: self, identifer: identifer) {
                print("extra: in SelectTabbar class")
            }
        }
    
    }

    以上代码实现了三处统计的逻辑,而不用把统计的逻辑写入controller文件中,降低了功能上的耦合度。

    三、用来代替extension,增强代码可读性

      使用扩展,可以很方便的为一些继承它的子类增添一些函数。这就带来一个问题,就是所有的子类都拥有了这个方法,但是方法的本身可能不明确,或者是只是想让少数子类来使用这个方法。这时候可以使用协议来代替extension。

    //定义了一个Shakable协议,遵守这个协议的类即可使用里面的方法,并为该方法提供一个默认的实现
    //where Self:UIView表明了只有uiview的子类可以遵守这个协议
    protocol Shakable {
        func shakeView()
    }
    
    extension Shakable where Self:UIView{
        func shakeView(){
            print(Self.self)
        }
    }

    这时候可以让某个子类来遵守协议。例如刚才上面的例子。

    class SelectTabbar: UIView,Shakable

      如果不在类中重新实现这个方法,则可以实现默认的方法。这个意思表明,SelectTabbar类的子类是遵守Shakable协议的,间接等于SelectTabbar():Shakable?。这样我们就可以愉快的让SelectTabbar对象去使用这个方法。(Self关键字只能用在协议或者类中,表示当前类,可作为返回值使用)。
      一旦不想让某个子类使用shakeView()方法,很简单,只要把class SelectTabbar: UIView,Shakable中的Shakable协议干掉即可。
      其他实践:
      利用AOP去分离tableview的数据源和事件源的方法,可以单独处理里面的逻辑,使tableview的代理方法不显得那么冗余。
      

    总结

    关于协议,还有很多种用法。以上是目前比较常用的场景。日后开发中如果发现协议在其他地方中有更好的应该,将会更新本文。

    展开全文
  • 函数式 swift

    2020-05-18 16:27:13
    函数式Swift》是一本引领你进入Swift 函数式编程世界的优秀读物,它让更多的中国开发者有机会接触并了解Swift语言函数式的一面,是广大程序开发者不可多得的工具书。
  • 函数式Swift4-最新版

    2020-07-21 09:59:15
    最新版函数式swift编程,RMB买的,分享给需要学习的兄弟
  • 函数式Swift》是一本引领你进入Swift 函数式编程世界的优秀读物,它让更多的中国开发者有机会接触并了解Swift语言函数式的一面,是广大程序开发者不可多得的工具书。
  • 所谓函数式编程方法,是借助函数式思想对真实问题进行分析和简化,继而构建一系列简单、实用的函数,再“装配”成最终的程序,以解决问题的方法。 本章关键词 请带着以下关键词阅读本文: 一等值(一等函数) 模块...

    所谓函数式编程方法,是借助函数式思想对真实问题进行分析和简化,继而构建一系列简单、实用的函数,再“装配”成最终的程序,以解决问题的方法。

    本章关键词

    请带着以下关键词阅读本文:

    • 一等值(一等函数)
    • 模块化
    • 类型驱动

    案例:Battleship

    本章案例是一个关于战舰攻击范围计算的问题,描述如下:

    • 战舰能够攻击到射程范围内的敌船
    • 攻击时不能距离自身太近
    • 攻击时不能距离友船太近

    问题:计算某敌船是否在安全射程范围内。

    对于这个问题,我们换一种描述:

    • 输入:目标(Ship)
    • 处理:计算战舰到敌船的距离、敌船到友船的距离,判断敌船距离是否在射程内,且敌船到友船距离足够大
    • 输出:是否(Bool)

    看上去问题并不复杂,我们可以产出以下代码:

    typealias Distance = Double
    
    struct Position {
        var x: Double
        var y: Double
    }
    
    struct Ship {
        var position: Position
        var firingRange: Distance
        var unsafeRange: Distance
    }
    
    extension Ship {
        func canSafelyEngageShip(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
        }
    }复制代码

    可以看出,canSafelyEngageShip 方法分别计算了我们需要的两个距离:targetDistancefriendlyDistance,随后与战舰的射程 firingRange 和安全距离 unsafeRange 进行比较。

    功能看上去没有什么问题了,如果觉得 canSafelyEngageShip 方法过于繁琐,还可以添加一些辅助函数:

    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)
        }
    }
    
    extension Ship {
        func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
            let targetDistance = target.position.minus(p: position).length
            let friendlyDistance = friendly.position.minus(p: target.position).length
            return targetDistance <= firingRange
                && targetDistance > unsafeRange
                && (friendlyDistance > unsafeRange)
        }
    }复制代码

    到此,我们编写了一段比较直观且容易理解的代码,但由于我们使用了非常“过程式”的思维方式,所以扩展起来就不太容易了。比如,再添加一个友船,我们就需要再计算一个 friendlyDistance_2,这样下去,代码会变得很复杂、难理解。

    为了更好解决这个问题,我们先介绍一个概念:一等值(First-class Value),或者称为 一等函数(First-class Function)

    我们来看看维基上的解释:

    In computer science, a programming language is said to have first-class functions if it treats functions as first-class citizens. Specifically, this means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures.

    简单来说,就是函数与普通变量相比没有什么特殊之处,可以作为参数进行传递,也可以作为函数的返回值。在 Swift 中,函数是一等值。带着这个思维,我们尝试使用更加声明式的方式来思考这个问题。

    归根结底,就是定义一个函数来判断一个点是否在范围内,所以我们要的就是一个输入 Position,输出 Bool 的函数:

    func pointInRange(point: Position) -> Bool {
        ...
    }复制代码

    然后,我们就可以用这个能够判断一个点是否在区域内的函数来表示一个区域,为了更容易理解,我们给这个函数起个名字(因为函数是一等值,所以我们可以像变量一样为其设置别名):

    typealias Region = (Position) -> Bool复制代码

    我们将攻击范围理解为可见区域,超出攻击范围或处于不安全范围均视为不可见区域,那么可知:

    有效区域 = 可见区域 - 不可见区域

    如此,问题从距离运算演变成了区域运算。明确问题后,我们可以定义以下区域:

    // 圆心为原点,半径为 radius 的圆形区域
    func circle(radius: Distance) -> Region {
        return { point in point.length <= radius }
    }
    
    // 圆心为 center,半径为 radius 的圆形区域
    func circle2(radius: Distance, center: Position) -> Region {
        return { point in point.minus(p: center).length <= radius }
    }
    
    // 区域变换函数
    func shift(region: @escaping Region, offset: Position) -> Region {
        return { point in region(point.minus(p: offset)) }
    }复制代码

    前两个函数很容易理解,但第三个区域有些特别,它将一个输入的 region 通过 offset 变化后返回一个新的 region。为什么要有这样一个特殊“区域”呢?其实,这是函数式编程的一个核心概念,为了避免产生 circle2 这样会不断扩展然后变复杂的函数,通过一个函数来改变另一个函数的方式更加合理。例如,一个圆心为 (5,5) 半径为 10 的圆就可以用下面的方式来表示了:

    shift(region: circle(radius: 10), offset: Position(x: 5, y: 5))复制代码

    掌握了 shift 式的区域定义方法,我们可以继续定义以下“区域”:

    // 将原区域取反得到新区域
    func invert(region: @escaping Region) -> Region {
        return { point in !region(point) }
    }
    
    // 取两个区域的交集作为新区域
    func intersection(region1: @escaping Region, _ region2: @escaping Region) -> Region {
        return { point in region1(point) && region2(point) }
    }
    
    // 取两个区域的并集作为新区域
    func union(region1: @escaping Region, _ region2: @escaping Region) -> Region {
        return { point in region1(point) || region2(point) }
    }
    
    // 取在一个区域,且不在另一个区域,得到新区域
    func difference(region: @escaping Region, minus: @escaping Region) -> Region {
        return intersection(region1: region, invert(region: minus))
    }复制代码

    很轻松有木有!

    基于这个小型工具库,我们来改写案例中的代码,并与之前的代码进行对比:

    // After
    func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
        let rangeRegion = difference(region: circle(radius: firingRange),
         minus: circle(radius: unsafeRange))
        let firingRegion = shift(region: rangeRegion, offset: position)
        let friendlyRegion = shift(region: circle(radius: unsafeRange),
         offset: friendly.position)
        let resultRegion = difference(region: firingRegion, minus: friendlyRegion)
        return resultRegion(target.position)
    }
    
    // Before
    func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
        let targetDistance = target.position.minus(p: position).length
        let friendlyDistance = friendly.position.minus(p: target.position).length
        return targetDistance <= firingRange
          && targetDistance > unsafeRange
          && (friendlyDistance > unsafeRange)
    }复制代码

    借助以上函数式的思维方式,我们避开了具体问题中一系列的复杂数值计算,得到了易读、易维护、易迁移的代码。


    思考

    一等值(一等函数)

    一等值这个名词我们可能较少听到,但其概念却是渗透在我们日常开发过程中的。将函数与普通变量对齐,是很重要的一项语言特性,不仅是编码过程,在简化问题上也能为我们带来巨大的收益。

    例如本文案例,我们使用函数描述区域,然后使用区域运算代替距离运算,在区域运算中,又使用了诸如 shift 的函数式思想,进而将问题进行简化并最终解决。

    模块化

    在《函数式 Swift》的前言部分,有一段对模块化的描述:

    相比于把程序认为是一系列赋值和方法调用,函数式开发者更倾向于强调每个程序都能被反复分解为越来越小的模块单元,而所有这些模块可以通过函数装配起来,以定义一个完整的程序。

    模块化是一个听上去很酷,遇到真实问题后有时又会变得难以下手,本文案例中,原始问题看上去目标简单并且明确,只需要一定的数值计算就可以得到最终结果,但当我们借助函数式思维,将问题的解决转变为区域运算后,关注点就转变为区域的定义上,然后进一步分解为区域变换、交集、并集、差集等模块,最后,将这些模块“装配”起来,问题的解决也就顺理成章了。

    类型驱动

    这里的类型,对应着前文中我们定义的 Region,因为我们选用了 Region 这个函数式定义来描述案例中的基本问题单元,即判断一个点是否在区域内,从而使我们的问题转变为了区域运算。

    可见,我们是一种“类型驱动”的问题解决方式,或者说是编码方式,类型的选择决定了我们解决问题的方向,假如我们坚持使用 PositionDistance,那么解决问题的方向必然陷入此类数值运算中,显然,函数式的类型定义帮助我们简化并且更加优雅的解决了问题。


    参考资料

    1. Github: objcio/functional-swift
    2. First-class function

    本文属于《函数式 Swift》读书笔记系列,同步更新于 huizhao.win,欢迎关注!

    展开全文
  • 因此,我们会尽量把重点放在我们认为设计良好的 Swift 函数式程序应该具有的一些特质上: 模块化: 相较于把程序认为是一系列赋值和方法调用,函数式开发者更倾向于强调每个程序都能够被反复分解为越来越小的模块...
  • 这个案例是通过一个已经存在且面向对象的API,展示如何使用高阶函数将其以小巧且函数式的方式进行。 1.首先是定义一个闭包,并且重命名。该闭包接受一个CIImage并返回一个CIImage。 typealias Filter = (CIImage) ...

    ###案例研究:封装Core Image

    • 这个案例是通过一个已经存在且面向对象的API,展示如何使用高阶函数将其以小巧且函数式的方式进行。

    1.首先是定义一个闭包,并且重命名。该闭包接受一个CIImage并返回一个CIImage。

    typealias Filter = (CIImage) -> CIImage
    复制代码

    2.构建一个模糊滤镜

    //模糊滤镜
        func blur(radius: Double) -> Filter{
            return { image in
                let parameters:[String: Any] = [
                    kCIInputRadiusKey: radius,
                    kCIInputImageKey:image
                ]
                
                guard let filter = CIFilter(name: "CIGaussianBlur", withInputParameters: parameters) else{
                    fatalError("错误1")
                }
                
                guard let outputImage = filter.outputImage else{
                    fatalError("错误二")
                }
                
                return outputImage
            }
        }
    复制代码

    3.颜色叠层,颜色叠层在调用的时候会导致程序崩溃,这里应该有点儿问题,不过理解到里面的思想就行了。

    //固定颜色滤镜
        func generate(color: UIColor) -> Filter{
            return { _ in
                
                let parameters = [
                    kCIInputColorKey:CIColor(cgColor: color.cgColor)
                ]
                
                guard let filter = CIFilter(name: "CIConstantColorGenerator", withInputParameters: parameters) else{
                    fatalError(".....")
                }
                
                guard let outputImage = filter.outputImage else {
                    fatalError("''''''''")
                }
                
                return outputImage
            }
        }
    复制代码

    4.合成滤镜

    //合成滤镜
        func compositeSourceOver(overlay: CIImage) -> Filter{
            return { image in
                let parameters = [
                    kCIInputBackgroundImageKey:image,
                    kCIInputImageKey:overlay
                ]
                
                guard let filter = CIFilter(name: "CISourceOverCompositing", withInputParameters: parameters) else{
                    fatalError()
                }
                
                guard let outputImage = filter.outputImage else {
                    fatalError()
                }
                
                
                return outputImage
            }
        }
    复制代码

    5.再创建一个通过两个滤镜来创建颜色叠层滤镜。这里是用先前创建颜色生成滤镜generate(color:)来生成一个新叠层。然后以gennerate(color:)返回的CIImage作为compositeSourceOver(overlay:)参数调用该函数,返回Filter类型值。这里就很明显了任何滤镜之间可以相互组合,特别的灵活。

    //滤镜颜色叠成
        func overlay(color:UIColor) -> Filter{
            return { image in
                let overlay = self.generate(color: color)(image).cropping(to: image.extent)
                return self.compositeSourceOver(overlay: overlay)(image)
            }
        }
    复制代码

    6.所以我们可以创建一个将两个任意生成滤镜方法进行组合

    //简化函数
        func compose(filter filter1:@escaping Filter, with filter2:@escaping Filter) -> Filter{
            return {image in filter2(filter1(image))}
        }
    复制代码

    7.为了防代码更具有可读性,我们还可以再进行一步,为组合滤镜引入自定义运算符。

    infix operator >>>
    //自定义个运算符
    func >>> (filter1:@escaping Filter,filter2:@escaping Filter) -> Filter{
        return {image in filter2(filter1(image))}
    }
    复制代码

    ###柯里化 -将一个接受多个参数的函数变换为一系列只接受单个参数的函数,这个过程被称为柯里化。这个列子add2称为add1的柯里化版本。

    func add1(_ x: Int, _ y: Int) -> Int{
            return x + y
        }
        
    func add2(_ x: Int) -> ((Int) -> Int){
            return {y in x + y}
        }
    复制代码
    展开全文
  • 说明:本文及所属系列文章为图书《函数式 Swift》的读书笔记,旨在记录学习过程、分享学习心得。文中部分代码摘自原书开源代码库 Github: objcio/functional-swift,部分内容摘自原书。如需深入学习,请购买正版支持...

    说明:本文及所属系列文章为图书《函数式 Swift》的读书笔记,旨在记录学习过程、分享学习心得。文中部分代码摘自原书开源代码库 Github: objcio/functional-swift,部分内容摘自原书。如需深入学习,请购买正版支持原书。(受 @SwiftLanguage 微博启发,特此说明)


    标题中的三个数组操作函数我们并不陌生,本章将借助这些 Swift 标准库中的函数,再次探索函数式思想的应用。

    本章关键词

    请带着以下关键词阅读本文:

    • 函数式思想
    • 泛型

    案例:City Filter

    使用 City 结构体描述城市信息(名字、人口数量),并定义一个城市数组:

    struct City {
        let name: String
        let population: Int
    }
    
    let paris = City(name: "Paris", population: 2241) // 单位为“千”
    let madrid = City(name: "Madrid", population: 3165)
    let amsterdam = City(name: "Amsterdam", population: 827)
    let berlin = City(name: "Berlin", population: 3562)
    
    let cities = [paris, madrid, amsterdam, berlin]复制代码

    问题:输出 cities 数组中所有人口超过百万的城市信息,并将人口数量单位转换为“个”。

    开始解决问题之前,请大家先忘掉标题中 Map、Filter 和 Reduce 等函数,无论之前是否使用过,我们尝试从零开始逐步向函数式思想过渡。

    我们先使用一个简单的思路来解决这个问题,即,遍历输入的城市数组,然后依次判断每个城市的人口数量,超过一百万的城市输出其信息:

    func findCityMoreThanOneMillion(_ cities: [City]) -> String {
        var result = "City: Population\n"
        for city in cities {
            if city.population > 1000 {
                result = result + "\(city.name): \(city.population * 1000)\n"
            }
        }
        return result
    }
    
    let result = findCityMoreThanOneMillion(cities)
    print(result)
    // City: Population
    // Paris: 2241000
    // Madrid: 3165000
    // Berlin: 3562000复制代码

    对于一个具体问题来说,我们的解法并不算差,满足需求、代码也简单,但是它只能正常工作于这样局限的场景中,显然,这不符合函数式思想。

    我们从上述代码开始分析,findCityMoreThanOneMillion 函数主要完成了以下三个工作:

    1. 过滤:通过 city.population > 1000 过滤出人口超过百万的城市;
    2. 转换单位:通过 city.population * 1000 将单位转换为“个”;
    3. 拼接结果:使用 var result 将结果拼接起来,并最终返回。

    这三步自然的帮我们将原问题分解成了三个子问题,即:

    1. 数组元素过滤问题(Filter);
    2. 数组元素修改问题(Map);
    3. 数组遍历与结果拼接问题(Reduce)。

    为了解决原始问题,我们需要优先解决这三个子问题,很明显,它们对应了标题中的函数,下面一一讨论(为了匹配原书内容,我们从 Map 开始)。

    Map

    案例中,我们需要将 city.population 的单位转换为“个”,本质上就是将一个数值转换为另一个数值,下面编写一个函数来实现这个功能:

    func transformArray(xs: [Int]) -> [Int] {
        var result: [Int] = []
        for x in xs {
            result.append(x * 1000)
        }
        return result
    }复制代码

    使用该函数可以帮助我们将一个 [Int] 数组中的每个元素乘以 1000,这样就能满足我们从“千”到“个”的单位转换需求,然而,这个函数存在的问题也非常明显:

    1. 入参和返回值均固定为 [Int],扩展性差;
    2. 数值变换方式固定为 x * 1000,场景局限。

    试想,如果输入数组可能为 [Double][Int],需要将单位从“千”转换为“万”、“百万”或者“千万”,输出为 [Int][Double],就不得不去修改这个函数,或是添加更多相似的函数。

    如何解决呢?先来了解一个概念:泛型(Generics),Swift 官方文档对泛型的定义如下:

    Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.

    可见,泛型的目标就是编写灵活、可复用,并且支持任意类型的函数,避免重复性的代码。以官方代码为例:

    func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
        let temporaryA = a
        a = b
        b = temporaryA
    }
    
    var someInt = 3
    var anotherInt = 107
    swapTwoValues(&someInt, &anotherInt)
    // someInt is now 107, and anotherInt is now 3
    
    var someString = "hello"
    var anotherString = "world"
    swapTwoValues(&someString, &anotherString)
    // someString is now "world", and anotherString is now "hello"复制代码

    使用泛型定义的 swapTwoValues 函数能够接受任意类型的入参,无论是 Int 还是 String 均能正常工作。

    回到 transformArray 函数:

    引入泛型可以帮助我们解决第一个问题,即,入参和返回值均固定为 [Int],既然入参和返回值可以是不相关的两种数组类型,那么我们可以使用两个泛型来表示它们,例如 [E][T]

    此时,transformArray 函数的入参和返回值变成了 [E][T],那么函数内部所要完成的任务就是将 [E] 转换为 [T],而转换过程正好对应了第二个问题,即,数值变换方式固定位 x * 1000,解决它只需要调用方将这个“转换过程”传递给 transformArray 函数即可,也就是说,我们需要一个形如这样的函数作为入参:

    typealias Transform = (E) -> (T)
    // 由于没有定义 E、T 泛型,所以这里仅作示意复制代码

    然后,将 transformArray 函数改写如下:

    func transformArray<E, T>(xs: [E], transform: Transform) -> [T] {
        var result: [T] = []
        for x in xs {
            result.append(transform(x))
        }
        return result
    }复制代码

    这样就完成了一个类似 Map 的函数,将这个函数加入 Array 中,并将函数名改为 map,就可以使用 Array 对象来调用这个方法了:

    extension Array {
        func map<T>(transform: (Element) -> T) -> [T] {
            var result: [T] = []
            for x in self {
                result.append(transform(x))
            }
            return result
        }
    }
    // 其中 Element 使用 Array 中 Element 泛型定义复制代码

    我们知道,map 函数已经存在于 Swift 标准库中(基于 Sequence 协议实现),因此并不需要自己来实现,我们通过 Swift 源码来学习一下(路径:swift/stdlib/public/Sequence.swift):

    public protocol Sequence {
        ...
    
        func map<T>(
            _ transform: (Iterator.Element) throws -> T
        ) rethrows -> [T]
    }
    
    extension Sequence {
        ...
    
        public func map<T>(
            _ transform: (Iterator.Element) throws -> T
        ) rethrows -> [T] {
            let initialCapacity = underestimatedCount
            var result = ContiguousArray<T>()
            result.reserveCapacity(initialCapacity)
    
            var iterator = self.makeIterator()
    
            // Add elements up to the initial capacity without checking for regrowth.
            for _ in 0..<initialCapacity {
                result.append(try transform(iterator.next()!))
            }
            // Add remaining elements, if any.
            while let element = iterator.next() {
                result.append(try transform(element))
            }
            return Array(result)
        }
    }复制代码

    除了一些额外的处理,核心部分与我们的实现是相同的,使用方法如下:

    let arr = [10, 20, 30, 40]
    let arrMapped = arr.map { $0 % 3 }
    print(arrMapped)
    // [1, 2, 0, 1]复制代码

    Filter

    有了 Map 的经验,对于 Filter 的设计就方便多了,我们参考 transformArray 函数可以这样设计 Filter 函数:

    1. 入参和返回值均为 [T]
    2. 入参的 transform 修改为 isIncluded,类型为 (T) -> Bool,用于判断是否应该包含在返回值中。

    实现代码如下:

    func filterArray<T>(xs: [T], isIncluded: (T) -> Bool) -> [T] {
        var result: [T] = []
        for x in xs {
            if isIncluded(x) {
                result.append(x)
            }
        }
        return result
    }复制代码

    同样的,filter 函数也已经存在于 Swift 标准库中,源码如下(路径:swift/stdlib/public/Sequence.swift):

    public protocol Sequence {
        ...
    
        func filter(
            _ isIncluded: (Iterator.Element) throws -> Bool
        ) rethrows -> [Iterator.Element]
    }
    
    extension Sequence {
        ...
    
        public func filter(
            _ isIncluded: (Iterator.Element) throws -> Bool
        ) rethrows -> [Iterator.Element] {
    
            var result = ContiguousArray<Iterator.Element>()
            var iterator = self.makeIterator()
    
            while let element = iterator.next() {
                if try isIncluded(element) {
                    result.append(element)
                }
            }
    
            return Array(result)
        }
    }复制代码

    核心部分实现也是相同的,使用方法如下:

    let arr = [10, 20, 30, 40]
    let arrFiltered = arr.filter { $0 < 35 }
    print(arrFiltered)
    // [10, 20, 30]复制代码

    Reduce

    Reduce 与 Map 不同之处在于,Map 每次将集合中的元素抛给 transform 闭包,然后得到一个“变形”后的元素,而 Reduce 是将集合中的元素连同当前上下文中的变量一起抛给入参闭包(此处命名为 combine),以便于该闭包处理,然后返回处理后的结果,因此 combine 的定义类似:

    typealias Combine = (T, E) -> (T)
    // 由于没有定义 E、T 泛型,所以这里仅作示意复制代码

    因此 reduce 函数可以定义如下:

    func reduceArray<E, T>(xs: [E], initial: T, combine: Combine) -> T {
        var result: T = initial
        for x in xs {
            result = combine(result, x)
        }
        return result
    }复制代码

    Swift reduce 函数源码如下(路径:swift/stdlib/public/Sequence.swift):

    /// You rarely need to use iterators directly, because a `for`-`in` loop is the
    /// more idiomatic approach to traversing a sequence in Swift. Some
    /// algorithms, however, may call for direct iterator use.
    ///
    /// One example is the `reduce1(_:)` method. Similar to the `reduce(_:_:)`
    /// method defined in the standard library, which takes an initial value and a
    /// combining closure, `reduce1(_:)` uses the first element of the sequence as
    /// the initial value.
    ///
    /// Here's an implementation of the `reduce1(_:)` method. The sequence's
    /// iterator is used directly to retrieve the initial value before looping
    /// over the rest of the sequence.
    ///
    ///     extension Sequence {
    ///         func reduce1(
    ///             _ nextPartialResult: (Iterator.Element, Iterator.Element) -> Iterator.Element
    ///         ) -> Iterator.Element?
    ///         {
    ///             var i = makeIterator()
    ///             guard var accumulated = i.next() else {
    ///                 return nil
    ///             }
    ///
    ///             while let element = i.next() {
    ///                 accumulated = nextPartialResult(accumulated, element)
    ///             }
    ///             return accumulated
    ///         }
    ///     }复制代码

    reduce 函数与上面两个函数不太相同,Apple 将其实现以另一个 reduce1 函数放在了注释中,原因应该如注释所说,for-in loop 方式更加常用,但我们仍然可以正常使用 reduce 函数,方法如下:

    let arr = [10, 20, 30, 40]
    let arrReduced = arr.reduce(output) { result, x in
        return result + "\(x) "
    }
    print(arrReduced)
    // Arr contains 10 20 30 40复制代码

    函数式解决方案

    在准备好了 Map、Filter 和 Reduce 工具库之后,我们再来解决 City Filter 问题:

    let result =
        cities.filter { $0.population > 1000 }
            .map { $0.cityByScalingPopulation() }
            .reduce("City: Population") { result, c in
                return result + "\n" + "\(c.name): \(c.population)"
            }
    print(result)
    // City: Population
    // Paris: 2241000
    // Madrid: 3165000
    // Berlin: 3562000
    
    extension City {
        func cityByScalingPopulation() -> City {
            return City(name: name, population: population * 1000)
        }
    }复制代码

    借助 Map、Filter 和 Reduce 等方法,可以方便的使用链式语法对原数组进行处理,并得到最终结果。


    思考

    函数式思想

    当我们讨论函数式思想时,我们到底在说什么?

    简单说,函数式思想是通过构建一系列简单、实用的函数,再“装配”起来解决实际问题,对于这句话的理解,我想至少有三点:

    1. 目标转换:基于函数式思想解决问题时,目标不再“急功近利”直接解决具体问题,而是庖丁解牛,把具体问题分解成为小规模的,甚至是互不相干的子模块,攻克这些子模块才是更高优先级的工作;
    2. 函数设计:分解出的每个子模块,实际上也就对应了一个、或一组能够独立工作的函数,良好的函数设计不仅有助于我们解决当前问题,更能为我们构建一个优秀的工具库,去解决很多其他问题;
    3. 问题解决:有时具体问题的解决好像已经被我们遗忘了,在解决了子问题、构建了工具库后,简单“装配”就能轻松解决原始问题。借助函数式思想,我们也更容易发现问题之间的共同点,从而快速解决,换句话说,解决问题成为了函数式思想下的“副产品”

    泛型

    Swift 中对于泛型的应用非常广泛,使用泛型能够使我们事半功倍,一个函数可以“瞬间”支持几乎所有类型,更重要的是,因为 Swift 语言的“类型安全”特性,使得这一切都安全可靠。

    泛型之所以安全,是因为它仍然处于编译器的类型控制下,而 Swift 中的 Any 类型就不那么安全了,表面上看两者都能表示任意类型,但使用 Any 类型能够避开编译器的检查,从而可能造成错误,来看下面的例子:

    func exchange<T>(_ income: T) -> T {
        return "Money: \(income)" // error
    }
    
    func exchangeAny(_ income: Any) -> Any {
        return "Money: \(income)"
    }复制代码

    同样的函数体,使用泛型的 exchange 会提示错误:error: cannot convert return expression of type 'String' to return type 'T',而使用 AnyexchangeAny 则不提示任何错误。如果我们不清楚 exchangeAny 的返回值类型,而直接调用,则可能导致运行时错误,是非常危险的。因此,善用泛型能够让我们在“无须牺牲类型安全就能够在编译器的帮助下写出灵活的函数”。

    更多关于泛型的讨论请参阅原书,或官方文档。


    参考资料

    1. Github: objcio/functional-swift
    2. The Swift Programming Language: Generics
    3. The Swift Programming Language (Source Code)

    本文属于《函数式 Swift》读书笔记系列,同步更新于 huizhao.win,欢迎关注!

    展开全文
  • http://www.cocoachina.com/special/20161118/18132.html 转载于:https://www.cnblogs.com/-WML-/p/9177831.html
  • Swift 函数式

    2020-02-26 18:04:09
    这本书尝试让你学会以函数式的方式进行思考。我们认为 Swift 有着合适的语言特性来适配函 数式的编程方法。然而是什么使得程序具有函数式特性?又为何要一开始就学习关于函数式的 内容呢? 很难给出函数式的准确定义 ...
  • 函数式编程的核心理念就是 函数是值,它和结构体、整型或是布尔型没有什么区别 —— 对函数使用另外一套命名规则会 违背这一理念。知识点1 函数 sort() 求平方根 例 let temp = sort(4) temp 等于 2知识点2 求一...
  • ###swift为什么是用可选值? 在Object-C中我们可以将nil作为一个参数传递给函数,但是这有个弊端,一个是空指针的访问,二是Object-C区分字典查询(键不存在于字典)和成功返回nil的字典查询(键存在于字典,但关联...
  • Swift函数式程序的特性: 模块化:函数式编程更倾向于将程序反复分解为越来越小的模块单元,而这些块可以通过函数装配起来,以定义一个完整的程序。 对可变状态的谨慎处理:面向对象编程专注于类和对象的设计,每个...
  • 可选值(Optionals)是 Swift 引入的一项非常棒的特性,本文将基于原书中的案例以及函数式编程思想进行讨论。 概述 首先推荐大家阅读原书中的可选值章节,有很多使用细节以及与 Objective-C 的对比讨论,我认为如果...
  • 最近在阅读objc中国翻译的的<函数式swift>,读到"纯函数式数据结构"一章中关于字典树Trie的内容时,惊叹于书中对于这一结构设计的巧妙.读完之后,把自己的学习心得记录下来,一方面帮助自己巩固学习以及日后查找,另一...
  • 函数式Swift:闭包{ }

    2019-11-29 20:39:20
    揭秘@escaping, @non-escaping, @autoclosure 和 curry函数 闭包是可以独立传递的函数块,可以在代码中传递和使用。... 函数和闭包是Swift中的一流对象:您可以存储它们,将它们作为函数的参数传递,并像对待其...
  • 上一篇文章里,我们已经完成了 AST 的创建。接下来我们开始具体的来定义 Parser,并且会给出一个通用的单字符 Parser 的实现。...用一个函数来定义它,就是: Parser : String -&gt; AST 复制代码因为解...
  • QuickCheck 是一个用于随机测试的 Haskell 工具库,本文将基于原书中的案例以及函数式编程方法讨论如何构建 Swift 版本的 QuickCheck 库。 注:在学习本章内容以前,笔者没有学习过 Haskell,也没有使用过 ...
  • 前一章《函数式思想》中,我们学习了函数式编程思想,即,通过构建一系列简单、实用的函数,再“装配”起来解决实际问题。本章借助一个封装 Core Image 的案例进一步练习函数式思想的应用。 本章关键词 请带着以下...
1 2 3 4 5 ... 20
收藏数 8,813
精华内容 3,525
热门标签
关键字:

函数式swift