由于本年工作中用得语言换成 Rust/OCaml/ReScript 啦,所以导致我现在写代码更倾向于写函数式风格的代码。
趁便试试 Swift 在函数式方面能达到啥好玩的程度。主要是我不会 Swift,仅仅为了好玩。

创立工程

随意创立个工程,小玩具就不计划跑在手机上了,由于我的设备是 ARM 芯片的,所以直接创立个 Mac 项目,记住勾上包含测验。

构建 MTKView 子类

现在来创立个 MTKView 的子类,其实我现在现已不接受这种所谓的面向目标,开发者用这种方法,就要写太多篇幅来描绘一个上下文结构跟函数就能完成的动作。

import MetalKit
class MetalView: MTKView {
    required init(coder: NSCoder) {
        super.init(coder: coder)
        device = MTLCreateSystemDefaultDevice()
        render()
    }
}
extension MetalView {
    func render() {
        // TODO: 详细完成
    }
}

咱们这儿给 MetalView extension 了一个 render 函数,里面是后续要写得详细完成。

普通的方法画一个三角形

先用常见的方法来画一个三角形

class MetalView: MTKView {
    required init(coder: NSCoder) {
        super.init(coder: coder)
        device = MTLCreateSystemDefaultDevice()
        render()
    }
}
extension MetalView {
    func render() {
        guard let device = device else { fatalError("Failed to find default device.") }
        let vertexData: [Float] = [
            -1.0, -1.0, 0.0, 1.0,
             1.0, -1.0, 0.0, 1.0,
             0.0,  1.0, 0.0, 1.0
        ]
        let dataSize = vertexData.count * MemoryLayout<Float>.size
        let vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
        let library = device.makeDefaultLibrary()
        let renderPassDesc = MTLRenderPassDescriptor()
        let renderPipelineDesc = MTLRenderPipelineDescriptor()
        if let currentDrawable = currentDrawable, let library = library {
            renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
            renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
            renderPassDesc.colorAttachments[0].loadAction = .clear
            renderPipelineDesc.vertexFunction = library.makeFunction(name: "vertexFn")
            renderPipelineDesc.fragmentFunction = library.makeFunction(name: "fragmentFn")
            renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
            let commandQueue = device.makeCommandQueue()
            guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") }
            let commandBuffer = commandQueue.makeCommandBuffer()
            guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") }
            let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
            guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
            if let renderPipelineState = try? device.makeRenderPipelineState(descriptor: renderPipelineDesc) {
                encoder.setRenderPipelineState(renderPipelineState)
                encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
                encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
                encoder.endEncoding()
                commandBuffer.present(currentDrawable)
                commandBuffer.commit()
            }
        }
    }
}

然后是咱们需求注册的 Shader 两个函数

#include <metal_stdlib>
using namespace metal;
struct Vertex {
    float4 position [[position]];
};
vertex Vertex vertexFn(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
    return vertices[vid];
}
fragment float4 fragmentFn(Vertex vert [[stage_in]]) {
    return float4(0.7, 1, 1, 1);
}

在运行之前需求把 StoryBoard 控制器上的 View 改成咱们写得这个 MTKView 的子类。

用 Metal 画一个三角形(Swift 函数式风格)

自界说操作符

函数式当然不是指可以界说操作符,可是没有这些操作符,感觉没有魂灵,所以先界说个管道符

代码完成

precedencegroup SingleForwardPipe {
    associativity: left
    higherThan: BitwiseShiftPrecedence
}
infix operator |> : SingleForwardPipe
func |> <T, U>(_ value: T, _ fn: ((T) -> U)) -> U {
    fn(value)
}

测验管道符

由于创立项意图时候,勾上了 include Tests,直接写点测验代码,履行测验。

final class using_metalTests: XCTestCase {
    // ...
    func testPipeOperator() throws {
        let add = { (a: Int) in
            return { (b: Int) in
                return a + b
            }
        }
        assert(10 |> add(11) == 21)
        let doSth = { 10 }
        assert(() |> doSth == 10)
    }
}

现在随意写个测验通过嘞。

Functional Programming

现在需求把上面的逻辑分割成小函数,事实上,由于 Cocoa 的基础是建立在面向目标上的,咱们还是无法彻底摆脱面向目标,现在先小范围应用它。

生成 MTLBuffer

先理一下逻辑,代码开端是创立顶点数据,生成 buffer

fileprivate let makeBuffer = { (device: MTLDevice) in
    let vertexData: [Float] = [
        -1.0, -1.0, 0.0, 1.0,
         1.0, -1.0, 0.0, 1.0,
         0.0,  1.0, 0.0, 1.0
    ]
    let dataSize = vertexData.count * MemoryLayout<Float>.size
    return device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
}

创立 MTLLibrary

接着是创立 MTLLibrary 来注册两个 shader 方法,还创立了一个 MTLRenderPipelineDescriptor 目标用于创立 MTLRenderPipelineState,可是创立的 MTLLibrary 目标是一个 Optional 的,所以其实得有两步,总归先提取它再说吧

fileprivate let makeLib = { (device: MTLDevice) in device.makeDefaultLibrary() }

抽象 map 函数

根据咱们有限的函数式编程经验,像 Optional 这种目标大概率有一个 map 函数,所以咱们自家完成一个,同时还要写成柯里化的(主张自动柯里语法糖化入常),由于这儿有逃逸闭包,所以要加上 @escaping

func map<T, U>(_ transform: @escaping (T) throws -> U) rethrows -> (T?) -> U? {
    return { (o: T?) in
        return try? o.map(transform)
    }
}

处理 MTLRenderPipelineState

这儿终究意图便是 new 了一个 MTLRenderPipelineState,顺带处理把程序的一些上下文给渲染管线描绘器(MTLRenderPipelineDescriptor),比如咱们用到的着色器(Shader)函数,像素格式。 最终一行直接 try! 不处理过错啦,反正出问题直接会抛出来的

fileprivate let makeState = { (device: MTLDevice) in
    return { (lib: MTLLibrary) in
        let renderPipelineDesc = MTLRenderPipelineDescriptor()
        renderPipelineDesc.vertexFunction = lib.makeFunction(name: "vertexFn")
        renderPipelineDesc.fragmentFunction = lib.makeFunction(name: "fragmentFn")
        renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
        return (try! device.makeRenderPipelineState(descriptor: renderPipelineDesc))
    }
}

暂时收尾

现已不想再抽取函数啦,其实还能更细粒度地处理,由于函数式有个纯函数跟副作用的概念,像 Haskell 里是可以用 Monad 来处理副作用的情况,这个主题留给后续吧。先把 render 改造一下

fileprivate let render = { (device: MTLDevice, currentDrawable: CAMetalDrawable?) in
    return { state in
        let renderPassDesc = MTLRenderPassDescriptor()
        if let currentDrawable = currentDrawable {
            renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
            renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
            renderPassDesc.colorAttachments[0].loadAction = .clear
            let commandQueue = device.makeCommandQueue()
            guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") }
            let commandBuffer = commandQueue.makeCommandBuffer()
            guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") }
            let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
            guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
            encoder.setRenderPipelineState(state)
            encoder.setVertexBuffer(device |> makeBuffer, offset: 0, index: 0)
            encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
            encoder.endEncoding()
            commandBuffer.present(currentDrawable)
            commandBuffer.commit()
        }
    }
}

然后再调用,于是就变成下面这副鸟姿态

class MetalView: MTKView {
    required init(coder: NSCoder) {
        super.init(coder: coder)
        device = MTLCreateSystemDefaultDevice()
        device |> map {
            makeLib($0)
            |> map(makeState($0))
            |> map(render($0, self.currentDrawable))
        }
    }
}

最终履行出这种作用

用 Metal 画一个三角形(Swift 函数式风格)