This article is only a record and summary of my reading of this book, please go to Objc China for purchase. Assault delete.

The introduction

Some characteristics of Swift functional programs:

  • Modularity: Rather than thinking of programs as a series of assignments and method calls, functional developers tend to emphasize that each program can be broken down repeatedly into smaller and smaller modular units, all of which can be assembled by functions to define a complete program. Of course, splitting a large program into smaller units is only possible if we can avoid sharing state between two separate components. This brings us to our next quality of concern.
  • Careful handling of mutable state: Functional programming is sometimes (half-jokingly) called “value-oriented programming.” Object-oriented programming focuses on the design of classes and objects, each of which has its own encapsulated state. Functional programming, however, emphasizes the importance of value-based programming, which protects us from mutable state or other side effects. By avoiding mutable state, functional programs are easier to compose than their imperative or object-oriented counterparts.
  • Types: Finally, a well-designed functional program should be careful when using types. More than anything else, carefully choosing the types of your data and functions will help you build your code. Swift has a powerful typing system that, when used properly, can make your code more secure and robust.

The translation of sequence

Avoiding program state and mutable objects is one of the most effective ways to reduce program complexity, which is the essence of functional programming. Functional programming emphasizes the result of execution, not the process of execution. The basic idea of functional programming is to build a series of small functions that are simple but have certain functions, and then assemble these functions to achieve complete logic and complex operations.

Functional idea

A function is first-class-values in Swift; in other words, a function can be passed as an argument to another function or as a return value from another function.

Case study: Battle Ship

typealias distance = Double

struct Position {
    var x: Double
    var y: Double
}

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 Position {
    func within(range: Distance) -> Bool {
        return sqrt(x * x + y * y) <= range
    }
}

struct Ship {
    var position: Position
    var firingRange: distance
    var unsafeRange: distance
}

extension Ship {
    func canSafelyEngage(ship 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)
    }
}
Copy the code

The wait function

Instead of defining an object or structure to represent a region, we use a function that determines whether a given point is within a region. This may seem strange if you’re not used to functional programming, but remember: functions are equivalent in Swift! We consciously chose Region as the name of this type, as opposed to CheckInRegion or RegionBlock, which imply that they represent a function type. At the heart of functional programming is the idea that functions are values, no different from structs, integers, or Booleans — the idea that using a different set of naming conventions for functions would violate that idea.

// first class function
typealias Region = (Position) - >Bool

// Define a circle centered at the origin
func circle(radius: Distance) -> Region {
    return { point in point.length <= radius }
}

// Define a circle with an arbitrary center
func circle2(radius: Distance, center: Position) -> Region {
    return { point in point.minus(center).length <= radius }
}

// Domain transform function
func shift(_ region: @escaping Region, by offset: Position) -> Region {
    return { point in region(point.minus(offset)) }
}

// represents a circle with center (5, 5) and radius 10
let shifted = shift(circle(radius: 10), by: Position(x: 5, y: 5))

// Represents all regions outside a region
func invert(_ region: @escaping Region) -> Region {
    return { point in! region(point) } }// The intersection of two regions
func intersect(_ region: @escaping Region, with other: @escaping Region)
-> Region {
    return { point in region(point) && other(point) }
}

// The union of two fields
func union(_ region: @escaping Region, with other: @escaping Region)
-> Region {
    return { point in region(point) || other(point) }
}

// In the first region but not in the second
func subtract(_ region: @escaping Region, from original: @escaping Region)
-> Region {
    return intersect(original, with: invert(region))
}

// New method
extension Ship {
    func canSafelyEngageV2(ship target: Ship, friendly: Ship) -> Bool {
        let rangeRegion = subtract(circle(radius: unsafeRange),
        from: circle(radius: firingRange))
        let firingRegion = shift(rangeRegion, by: position)
        let friendlyRegion = shift(circle(radius: unsafeRange),
        by: friendly.position)
        let resultRegion = subtract(friendlyRegion, from: firingRegion)
        return resultRegion(target.position)
    }
}
Copy the code

The way functions are evaluated and passed in Swift is not any different from that of integers or Bores. This allows us to write basic graphical components, such as circles, from which to build a series of functions. Each function can modify or merge regions to create new regions. Instead of writing complex functions to solve a specific problem, we can now assemble small functions to solve a wide variety of problems.

Case study: Encapsulating Core Image

A filter type

import CoreImage

typealias Filter = (CIImage) - >CIImage

/ / fuzzy
func blur(radius: Double) -> Filter {
    return { image in
        let parameters: [String: Any] = [
            kCIInputRadiusKey: radius,
            kCIInputImageKey: image
        ]
        guard let filter = CIFilter(name: "CIGaussianBlur", parameters: parameters) else { fatalError()}guard let outputImage = filter.outputImage else { fatalError()}return outputImage
    }
}

// Color overlay
func generate(color: UIColor) -> Filter {
    return { _ in
        let parameters = [kCIInputColorKey: CIColor(cgColor: color.cgColor)]
        guard let filter = CIFilter(name: "CIConstantColorGenerator",  parameters: parameters) else { fatalError()}guard let outputImage = filter.outputImage else { fatalError()}return outputImage
    }
}

// Create a composite filter
func compositeSourceOver(overlay: CIImage) -> Filter {
    return { image in
        let parameters = [
            kCIInputBackgroundImageKey: image,
            kCIInputImageKey: overlay
        ]
        guard let filter = CIFilter(name: "CISourceOverCompositing", parameters: parameters) else { fatalError()}guard let outputImage = filter.outputImage else { fatalError()}return outputImage.cropped(to: image.extent)
    }
}

// Color overlay filter
func overlay(color: UIColor) -> Filter {
    return { image in
        let overlay = generate(color: color)(image).cropped(to: image.extent)
        return compositeSourceOver(overlay: overlay)(image)
    }
}

// Blur the image and overlay it with a red layer
let url = URL(string: "http://via.placeholder.com/500x500")!
let image = CIImage(contentsOf: url)!

let radius = 5.0
let color = UIColor.red.withAlphaComponent(0.2)
let blurredImage = blur(radius: radius)(image)
let overlaidImage = overlay(color: color)(blurredImage)

// Combine the above two expressions
let result = overlay(color: color)(blur(radius: radius)(image))
Copy the code

Make code more readable:

// Lost readability, better to build a function that combines filters into one
func compose(filter filter1: @escaping Filter, with filter2: @escaping Filter) -> Filter {
    return { image in filter2(filter1(image)) }
}

let blurAndOverlay = compose(filter: blur(radius: radius), with: overlay(color: color))
let result1 = blurAndOverlay(image)

// go further
infix operator >>>

func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
    return { image in filter2(filter1(image)) }
}

let blurAndOverlay2 = blur(radius: radius) >>> overlay(color: color)
let result2 = blurAndOverlay2(image)
Copy the code

Because the operator >>> defaults to left-associative, like Unix pipes, filters are applied to the image in left-to-right order.

The combined filter operator we define is an example of a compound function. In mathematics, the composition of the functions f and g is sometimes written as f · g, indicating that the defined new function maps the input x to f(g(x)). In addition to order, this is exactly what our >>> operator does: it passes an image parameter to the two filters that the operator operates on.

Theoretical background: Corrification

There are two equivalent ways to define a function that can take two (or more) arguments. The first style should be more familiar to most programmers:

func add1(_ x: Int, _ y: Int) -> Int {
    return x + y
}
Copy the code

The add1 function takes two integer arguments and returns their sum. In Swift, however, we can define this function in another version:

func add3(_ x: Int)- > (Int) - >Int {
    return { y in x + y }
}
Copy the code

Add1 differs from add2 in how it is called:

add1(1.2) / / 3
add2(1) (2) / / 3
Copy the code

In the first method, we pass both arguments to add1; The second method passes the first argument, 1, to the function and then applies the returned function to the second argument, 2. The two versions are completely equivalent: we can define add1 in terms of add2, and vice versa.

The examples of add1 and add2 show how a function that takes many arguments can be transformed into a series of functions that take only one argument, a process called Currying, named after the logician Haskell Curry; We’ll call add2 the Currified version of add1.

So why is currization interesting? As we’ve seen in this book so far, there are situations where you might want to pass functions as arguments to other functions. If we have an uncurrified function like add1, we must call the function with both of its arguments. However, for a curryized function like ADD2, we have two choices: we can call it with one or two arguments.

The functions defined to create filters in this chapter have all been Currified — they all accept an additional image parameter. By defining filters in a Currified style, we can easily combine them using the >>> operators. If we build filters with uncurrified versions of these functions, we can still write the same filters, but the types of filters will be slightly different depending on the parameters they accept. As a result, it would be much harder to define a unified combinatorial operator for these different types of filters.

discuss

We believe that the API designed in this chapter also has some advantages:

  • Security – With the API we built, it is almost impossible to have run-time errors caused by undefined keys or cast failures.
  • Modularity – Filters are easily combined using the >>> operators. This allows you to break down complex filters into smaller, simpler, reusable components. Also, a composite filter is exactly the same type as the components that make it up, so you can use them interchangeably.
  • Clear and easy to understand – even if you’ve never used Core Image, you should be able to assemble simple filters using the functions we’ve defined. You don’t need to care at all about how certain keys like kCIInputImageKey or kCIInputRadiusKey are initialized. You can pretty much figure out how to use the API just by looking at the type, and you don’t even need more documentation.

Map, Filter, and Reduce

Functions that take other functions as arguments are sometimes called higher-order functions.

Generic introduction

The most interesting thing about this code is its type signature. Understanding this type signature helps you understand genericCompute as a family of functions. Each selection of the type parameter T determines a new function. This function takes an array of integers and a function of type (Int) -> T as arguments, and returns an array of type [T].

extension Array {
    func map<T>(_ transform: (Element) -> T) - > [T] {
        var result: [T] = []
        for x in self {
            result.append(transform(x))
        }
        return result
    }
}
Copy the code

The Element type we use in the function’s transform argument is derived from the generic definition of Element in Swift’s Array.

Filter

extension Array {
    func filter(_ includeElement: (Element) -> Bool) - > [Element] {
        var result: [Element] = []
        for x in self where includeElement(x) {
            result.append(x)
        }
        return result
    }
}
Copy the code

Reduce

extension Array {
    func reduce<T>(_ initial: T, combine: (T, Element) -> T) - >T {
        var result = initial
        for x in self {
            result = combine(result, x)
        }
        return result
    }
}
Copy the code

In fact, we can even redefine maps and filters using Reduce:

extension Array {
    func mapUsingReduce<T>(_ transform: (Element) -> T) - > [T] {
        return reduce([]) { result, x in
            return result + [transform(x)]
        }
    }
    func filterUsingReduce(_ includeElement: (Element) -> Bool) - > [Element] {
        return reduce([]) { result, x in
            return includeElement(x) ? result + [x] : result
        }
    }
}
Copy the code

It is important to note that while defining everything through reduce is an interesting exercise, it is often not a good idea in practice. The reason is that, unsurprisingly, your code will end up copying a lot of the generated array at run time, in other words, it will repeatedly allocate memory, free memory, and copy a lot of the contents of memory. For example, it is obviously more efficient to write a map with a mutable result array. In theory, the compiler could optimize this code to be as fast as the mutable result array version, but Swift (currently) doesn’t do that.

Generic and Any types

In addition to generics, Swift supports the Any type, which can represent Any type of value. On the surface, this looks very similar to generics. Both the Any type and the generic type can be used to define functions that take two different types of arguments. However, it is important to understand the difference between the two: generics can be used to define flexible functions, and type checking remains the responsibility of the compiler; The Any type circumvents Swift’s type system (and should therefore be avoided whenever possible).

Let’s consider the simplest example: imagine a function that does nothing but return its arguments. We might write it like this:

func noOp<T>(_ x: T) -> T {
    return x
}

func noOpAny(_ x: Any) -> Any {
    return x
}
Copy the code

Both noOp and noOpAny will take arbitrary arguments. The key difference is what we know about the return value. In the definition of noOp, it is clear that the return value is exactly the same as the input value. In noOpAny’s case, the return value is of any type — even a different type than the original input value. We can give an incorrect definition of noOpAny as follows:

func noOpAnyWrong(_ x: Any) -> Any {
    return 0
}
Copy the code

Use the Any type to avoid Swift’s type system. However, trying to set the return value of a noOp function defined using generics to 0 will result in a type error. In addition, any function that calls noOpAny does not know what type the return value will be converted to. The result can be all sorts of runtime errors.

Using generics allows you to write flexible functions with the compiler’s help without sacrificing type safety; If you use Any, you’re really on your own.