Community update
Swift가 비록 앱 만드는 데 주로 쓰이지만 비전은 고수준 스크립트부터 로우레벨까지 커버하는 것이다. 그래서 올해 몇가지 큰 변화가 물밑에서 진행되었다.
Package
확장성있고 안전한 빌드 툴을 제공하기 위한 방법
사용 예시: 문서 만들기, 소스코드 포매팅, 테스트 리포트 만들기
기존에는 shell script를 쓰던 영역을 swift로 대체 가능
코드 예시
커맨드라인 플러그인
@main struct MyPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) throws {
let process = try Process.run(doccExec, arguments: doccArgs)
process.waitUntilExit()
}
}
빌드 툴 플러그인
import PackagePlugin
@main struct MyCoolPlugin: BuildToolPlugin {
func createBuildCommands(context: TargetBuildContext) throws -> [Command] {
let generatedSources = context.pluginWorkDirectory.appending("GeneratedSources")
return [
.buildCommand(
displayName: "Running MyTool",
executable: try context.tool(named: "mycooltool").path,
arguments: ["create"],
outputFilesDirectory: generatedSources)
]
}
}
플러그인은 Package안의 Plugin 폴더 안에 들어있게 된다.
다른 패키지에 같은 이름의 모듈이 있을 때를 대비해 다른 이름을 제공할 수 있다.
let package = Package(
name: "MyStunningApp",
dependencies: [
.package(url: "https://.../swift-metrics.git"),
.package(url: "https://.../swift-log.git")
],
products: [
.executable(name: "MyStunningApp", targets: ["MyStunningApp"])
],
targets: [
.executableTarget(
name: "MyStunningApp",
dependencies: [
.product(name: "Logging",
package: "swift-log"),
.product(name: "Metrics",
package: "swift-metrics",
moduleAliases: ["Logging": "MetricsLogging"]),
])])
쓰는 쪽에서는 선택해서 사용 가능
// MyStunningApp
import Logging // from swift-log
import MetricsLogging // from swift-metrics
let swiftLogger = Logging.Logger()
let metricsLogger = MetricsLogging.Logger()
성능 개선
Swift-Driver를 Xcode build system에서 바로 사용할 수 있게 됨
타입 체킹 시간 감소 - 제네릭 쪽 핵심 로직을 재작성
기존에는 연관된 프로토콜이 많을수록 시간 및 공간 복잡도가 지수 스케일로 증가
예시로 든 코드
public protocol NonEmptyProtocol: Collection
where Element == C.Element,
Index == C.Index {
associatedtype C: Collection
}
public protocol MultiPoint {
associatedtype C: CoordinateSystem
typealias P = Self.C.P
associatedtype X: NonEmptyProtocol
where X.C: NonEmptyProtocol,
X.Element == Self.P
}
public protocol CoordinateSystem {
associatedtype P: Point where Self.P.C == Self
associatedtype S: Size where Self.S.C == Self
associatedtype L: Line where Self.L.C == Self
associatedtype B: BoundingBox where Self.B.C == Self
}
public protocol Line: MultiPoint {}
public protocol Size {
associatedtype C: CoordinateSystem where Self.C.S == Self
}
public protocol BoundingBox {
associatedtype C: CoordinateSystem
typealias P = Self.C.P
typealias S = Self.C.S
}
public protocol Point {
associatedtype C: CoordinateSystem where Self.C.P == Self
}
런타임 개선
Concurrency updates
swift는 자체적인 메모리 안정성 검사를 이미 하고 있었다.
var numbers = [3, 2, 1]
numbers.removeAll(where: { number in
number == numbers.count // error: Overlapping accesses to "numbers, but modification requires exclusive access;
})
멀티 스레드에서도 동일하게 검사하게 되었다. → 이런게 필요하면 actor를 써야 한다.
var numbers = [3, 2, 1]
Task { numbers.append(0) } // Mutation of captured var 'numbers' in concurrently-executing code
numbers.removeLast()
이는 Swift 6로의 발전 과정 중 하나다.(Memory Safety to Thread Safety)
actor가 현재 시스템이 아닌 다른 시스템에 존재하는 것.
분산 시스템을 훨씬 간편하게 만든다.
distributed actor Player {
var ai: PlayerBotAI?
var gameState: GameState
distributed func makeMove() -> GameMove {
return ai.decideNextMove(given: &gameState)
}
}
// 호출부분
func endOfRound(players: [Player]) async throws {
// Have each of the players make their move
for player in players {
let move = try await player.makeMove()
}
}
Expressive Swift
언어는 도구지만, 언어를 가지고 만들려는 것은 언어의 영향을 많이 받게 된다. → 망치를 들면 모든 게 못으로 보인다.
if let shorthand
if let workingDirectoryMailmapURL {
mailmapLines = try String(contentsOf: workingDirectoryMailmapURL).split(separator: "\\n")
}
guard let workingDirectoryMailmapURL else { return }
mailmapLines = try String(contentsOf: workingDirectoryMailmapURL).split(separator: "\\n")
swift의 기능이 작은 변화에도 제대로 동작하지 않는 경우들이 있었다.
포인터 간의 자동변환을 절대로 허용하지 않는다.(typed → typed, typed→ raw, raw → typed 모두)
그래서 C코드를 Swift에서 쓰는게 까다롭다.
// Mismatches that are harmless in C…
int mailmap_get_size(mailmap_t *map);
void mailmap_truncate(mailmap_t *map, unsigned *sizeInOut);
void remove_duplicates(mailmap_t *map) {
int size = mailmap_get_size(map);
size -= move_duplicates_to_end(map);
mailmap_truncate(map, &size);
}
// …cause problems in Swift.
func removeDuplicates(from map: UnsafeMutablePointer<mailmap_t>) {
var size = mailmap_get_size(map)
size -= moveDuplicatesToEnd(map)
mailmap_truncate(map, &size) // error!: UnsafeMutablePointer<Int32> -> UnsafeMutablePointer<UInt32)
}
원래는 이렇게 써야 한다.
func removeDuplicates(from map: UnsafeMutablePointer<mailmap_t>) {
var size = mailmap_get_size(map)
size -= moveDuplicatesToEnd(map)
withUnsafeMutablePointer(to: &size) { signedSizePtr in
signedSizePtr.withMemoryRebound(to: UInt32.self, capacity: 1) { unsignedSizePtr in
mailmap_truncate(map, unsignedSizePtr)
}
}
}
그래서 C에서 import된 코드에 대해서는 별도 룰을 적용해서, C에서 가능한 포인터 변환에 대해서는 허용하도록 바꿨다.
String Processing
Generic code clarity
예시 유즈 케이스
/// Used in the commit list UI
struct HashedMailmap {
var replacementNames: [String: String] = [:]
}
/// Used in the mailmap editor UI
struct OrderedMailmap {
var entries: [MailmapEntry] = []
}
protocol Mailmap {
mutating func addEntry(_ entry: MailmapEntry)
}
extension HashedMailmap: Mailmap { … }
extension OrderedMailmap: Mailmap { … }
지금까지는 서로 다른 두가지 방법을 비슷한 형태로 쓰고 있었다.
Generic 형식: 특정 프로토콜을 채택하고 있는 타입
Existential형식: 내용물이 특정 프로토콜을 채택하고 있는 박스
func addEntries1<Map: Mailmap>(_ entries: Array<MailmapEntry>, to mailmap: inout Map) {
for entry in entries {
mailmap.addEntry(entry)
}
}
func addEntries2(_ entries: Array<MailmapEntry>, to mailmap: inout Mailmap) {
for entry in entries {
mailmap.addEntry(entry)
}
}
그래서 existential 한 형태로 사용할 때는 any 키워드를 앞에 붙이는 것을 요구한다.
func addEntries1<Map: Mailmap>(_ entries: Array<MailmapEntry>, to mailmap: inout Map) {
for entry in entries {
mailmap.addEntry(entry)
}
}
func addEntries2(_ entries: Array<MailmapEntry>, to mailmap: inout any Mailmap) {
for entry in entries {
mailmap.addEntry(entry)
}
}
편의를 위해서, any 타입으로 된 것을 실제 해당 프로토콜을 요구하는 경우로 넘길 때, 자동으로 언박싱을 해주게 된다.
extension Mailmap {
mutating func mergeEntries<Other: Mailmap>(from other: Other) { … }
}
func mergeMailmaps(_ a: any Mailmap, _ b: any Mailmap) -> any Mailmap {
var copy = a
copy.mergeEntries(from: b) // OK
return a
}
이 any 키워드는 기존에 타입으로 쓸 수 없었던 associatedtype을 가지는 경우나, Self를 가지던 경우에도 쓸 수 있다.
protocol Mailmap: Equatable {
mutating func addEntry(_ entry: MailmapEntry)
}
func addEntries2(_ entries: Array<MailmapEntry>, to mailmap: inout any Mailmap) {
for entry in entries {
mailmap.addEntry(entry)
}
}
이제 Collection같은 것도 마치 구체 타입인 것처럼 쓸 수 있다.
protocol Mailmap: Equatable {
mutating func addEntry(_ entry: MailmapEntry)
}
func addEntries2(_ entries: any Collection<MailmapEntry>, to mailmap: inout any Mailmap) {
for entry in entries {
mailmap.addEntry(entry)
}
}
이를 위해서 primary associatedtype이라는 것이 도입되었다.
대부분의 associatedtype은 프로토콜의 implementation detail에 해당한다.
하지만 특정 associatedtype은 알고 싶을 때가 있다.
이렇게 프로토콜을 쓸 때 유저가 알고 싶어하는 associatedtype은 제네릭처럼 프로토콜 이름 옆에 써준다.
protocol Collection<Element>: Sequence {
associatedtype Index: Comparable
associatedtype Iterator: IteratorProtocol<Element>
associatedtype SubSequence: Collection<Element>
where SubSequence.Index == Index,
SubSequence.SubSequence == SubSequence
associatedtype Element
}