由于本年工作中用得语言换成 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
的子类。
自界说操作符
函数式当然不是指可以界说操作符,可是没有这些操作符,感觉没有魂灵,所以先界说个管道符
代码完成
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))
}
}
}
最终履行出这种作用