まえがき
Swift5.7から導入される新機能をいくつか抜粋し、触りの部分のみまとめました。
各機能の詳細については、プロポーザルのリンクをご参照ください。また内容に間違いがある場合や正確性に欠く記載がありましたらコメントいただけると幸いです。
まずは分かりやすい箇所から…
SE-0345 if let
shorthand for shadowing an existing optional variable
if letのUnwrapがより短くシンプルになりました
let foo: String? = "foo"
if let foo {
print("foo is unwrapped: \(foo)")
}
またif文以外にもguard文やwhile文でも同様に可能です。weak selfをguard文でunwrapする時などよりシンプルに書けそうです。
if let foo { ... }
if var foo { ... }
else if let foo { ... }
else if var foo { ... }
guard let foo else { ... }
guard var foo else { ... }
while let foo { ... }
while var foo { ... }
swift-evolution/0345-if-let-shorthand.md at main · apple/swift-evolution
SE-0326 Enable multi-statement closure parameter/result type inference
クロージャーの返り値の型推論が少し強くなりました
複数の文で構成されるクロージャーの返り値の型が推論されるようになりました。
下記にサンプルコードを記載しています。
func map<T>(fn: (Int) -> T) -> T {
return fn(37)
}
// これはSwift5.6以下もOK
let _ = map { Double($0) }
// でも複数の文が入ってくると、ちゃんと返り値の型とか入れないとダメだった
let _ = map { val -> Double in
print("Hehe")
return Double($0)
}
// 5.7からは省略可能に
let _ = map {
print("well done")
return Double($0)
}
個人的な感想ですが、シンプルに書けるようになるものの、そこまで恩恵は大きく無さそうな印象です(どちらかといえばコンパイラ側の実装の変更範囲大変そうだなぁという感想が勝つ…)
SE-0341 Opaque Parameter Declarations
Opaque Typeを関数の引数に指定できるようになりました
// NEW
func f(_ p: some P) {}
// is identical to
func f<T: P>(_ p: T) {}
「え、その手のサブタイピングって f(_ p: P)
て書けば良くない?」とパッと見思いました。しかし、例えばSequenceのようにassociated typeや、EquatableのようにSelfを使用するprotocolの場合においては例外で、ジェネリクスの制約として使う他ありませんでした。
そのためにわざわざジェネリクスを用意して書いてあげるのは冗長じゃない?という課題感から起案されたプロポーザルであり改善内容となっているようです。
SE-0328 Structural opaque result types
some
キーワードが関数の返り値により柔軟に適用できるようになりました
コード例を以下に示します。
func f() -> [some Hashable: some Codable] {
return [ 1: "One", 2: "Two" ]
}
func g() -> [some Equatable] {
// ※ Opaque Typeなので["foo", 2]はTypeError
return ["foo", "bar"]
}
// おそらく文法的には正しいがXcode Version 14.0 beta (14A5228q)だとLLDBサーバーがクラッシュする?
func h() -> () -> some Equatable {
return {
let str = "hoge"
return str
}
}
こんな感じで従来では -> some View
のように単純な返り値型でしかOpaque Typeを使用できなかったところが、ディクショナリ関数の返された関数の返り値(引数はエラー)など幅広い使用用途がサポートされるようになりました!
SE-0309 Unlock existentials for all protocols
Selfやassociated typeを持つprotocolを型として使うことが可能になりました
let foo: any Equatable = "foo"
let bar: any Equatable = "bar"
foo == bar // -> Error. Selfが同一の型である保証が無いため
if let foo = foo as? String,
let bar = bar as? String {
print(foo == bar) // False
}
当然以下のように定義することも可能です。
let foo: [any Equatable] = ["foo", 1] // OK
let bar: [some Equatable] = ["bar", 2] // ちなみにsomeの場合はNG, 同じ型の要素でないため
この機能によって型消去テクニックが不要になります。
SE-0346 Lightweight same-type requirements for primary associated types, SE-0353 Constrained Existential Types
protocolにジェネリクスを持たせることができるようになりました
SE-0309によってSelfやassociated typeを持つProtocolにおいても型として利用することが可能になりました。一方、そのassociated typeについて制約を課したい状況はあるはずです。
その際に以下のようにprotocolにジェネリクスを設定して型を渡すことでシンプルに解決できるようになっています。
protocol Sequence<Element> {
associatedtype Element
associatedtype Iterator : IteratorProtocol
where Element == Iterator.Element
...
}
protocol DictionaryProtocol<Key, Value> {
associatedtype Key : Hashable
associatedtype Value
...
}
このように渡せるようになることで、型消去を行う必要性や、where句をゴニョゴニョする必要がなくなりスッキリした記述になります。
// 従来の方法(1) whereを使用する方法
func foo<S: Sequence>() -> S where S.Element == Int {}
// 従来の方法(2) AnyHogeで型消去した後ジェネリクスに渡す方法
func foo() -> AnySequence<Int> {}
// NEW
func foo() -> some Sequence<Int>
正規表現周り
SE-0350 Regex Type and Overview, SE-0354 Regex Literals
正規表現がより扱いやすくパワフルになりました
例えば以下の文字列(statement
)をパターンマッチを用いて適切に情報を抜き出して、 Transaction
にデコードする処理を考えてみます。
struct Transaction {
enum Kind { case credit; case debit }
let kind: Kind
let date: Date
let accountName: String
let amount: Decimal
}
let statement = """
CREDIT 03/02/2022 Payroll $200.23
CREDIT 03/03/2022 Sanctioned Individual A $2,000,000.00
DEBIT 03/03/2022 Totally Legit Shell Corp $2,000,000.00
DEBIT 03/05/2022 Beanie Babies Forever $57.33
"""
従来では、NSRegularExpression
を用いて正規表現を組み立てて、Range
をゴニョゴニョして抜き出すような処理になります。
// 従来の方法
let pattern = #"(\w+)\s\s+(\S+)\s\s+((?:(?!\s\s).)*)\s\s+(.*)"#
let nsRegEx = try! NSRegularExpression(pattern: pattern)
let target = "CREDIT 03/02/2022 Payroll $200.23"
let range = NSRange(target.startIndex..<target.endIndex, in: target)
guard let result = nsRegEx.firstMatch(in: target, range: range),
let kindRange = Range(result.range(at: 1), in: target) else {
fatalError()
}
let kind = Transaction.Kind(target[kindRange]) // CREDIT
今回より新しく導入された Regex
を用いることでよりシンプルに同等の処理を実装することが可能になります。特に下記2.3.にあるリテラル型でラベルを用いることで、パターンマッチの結果が静的に決められるため型安全に扱うことが出来ます。
// 1.コンストラクタで正規表現オブジェクトを生成する方法
let pattern = #"(\w+)\s\s+(\S+)\s\s+((?:(?!\s\s).)*)\s\s+(.*)"#
let regex = try! Regex(pattern)
// -> Regex<AnyRegexOutput>
// 2.リテラルで生成する方法
let regex = /(\w+)\s\s+(\S+)\s\s+((?:(?!\s\s).)*)\s\s+(.*)/
// -> Regex<(Substring, Substring, Substring, Substring, Substring)>
// 3.リテラル(ラベル付き)で生成する方法
let regex = #/
(?<kind> \w+) \s\s+
(?<date> \S+) \s\s+
(?<account> (?: (?!\s\s) . )+) \s\s+
(?<amount> .*)
/#
// ->regex: Regex<(
// Substring,
// kind: Substring,
// date: Substring,
// account: Substring,
// amount: Substring
// )>
if let result = try? regex.wholeMatch(in: "CREDIT 03/02/2022 Payroll $200.23" {
print(result.0) // CREDIT 03/02/2022 Payroll $200.23
print(result.1) // CREDIT
print(result.kind) // CREDIT
print(result.2) // 03/02/2022
print(result.date) // 03/02/2022
print(result.3) // Payroll
print(result.account) // Payroll
}
SE-0351 Regex builder DSL
DSLでより可読性高く正規表現を組み立てられるようになりました
上記で紹介した Regex
により正規表現の扱いが楽になりそうですが、更に正規表現の作成をより宣言的で可読性高く行うためのDSLが RegexBuilder
パッケージから提供されるようになったようです。
例えば先程の statement
用の正規表現は以下のようにRegexBuilder
を用いると作成することが出来ます。
import RegexBuilder
enum TransactionKind: String {
case credit = "CREDIT"
case debit = "DEBIT"
}
struct Date {
var month, day, year: Int
init?(mmddyyyy: String) { ... }
}
struct Amount {
var valueTimes100: Int
init?(twoDecimalPlaces text: Substring) { ... }
}
let statementPattern = Regex {
// Parse the transaction kind.
TryCapture {
ChoiceOf {
"CREDIT"
"DEBIT"
}
} transform: {
TransactionKind(rawValue: String($0))
}
OneOrMore(.whitespace)
// Parse the date, e.g. "01012021".
TryCapture {
Repeat(.digit, count: 2)
Repeat(.digit, count: 2)
Repeat(.digit, count: 4)
} transform: { Date(mmddyyyy: $0) }
OneOrMore(.whitespace)
// Parse the transaction description, e.g. "ACH transfer".
Capture {
OneOrMore(.custom([
.characterClass(.word),
.characterClass(.whitespace)
]))
CharacterClass.word
} transform: { String($0) }
OneOrMore(.whitespace)
"$"
// Parse the amount, e.g. `$100.00`.
TryCapture {
OneOrMore(.digit)
"."
Repeat(.digit, count: 2)
} transform: { Amount(twoDecimalPlaces: $0) }
} // => Regex<(Substring, TransactionKind, Date, String, Amount)>
let statement = """
CREDIT 04062020 PayPal transfer $4.99
CREDIT 04032020 Payroll $69.73
DEBIT 04022020 ACH transfer $38.25
DEBIT 03242020 IRS tax payment $52249.98
"""
for match in statement.matches(of: statementPattern) {
let (line, kind, date, description, amount) = match.output
...
}
このように宣言的で且つ可読性高く正規表現を組み立てられる他、transformで変換処理を渡すことで1つのRegex
オブジェクトで、文字列からオブジェクトに変換するための情報を集約することも可能になります。
詳細なAPIはhttps://github.com/apple/swift-evolution/blob/main/proposals/0351-regex-builder.mdを参照ください。
SE-0357 Regex-powered string processing algorithms
導入されたRegexで、より文字列操作APIがパワフルになりました
Collectionに対して以下のメソッドが追加されています。
関数 | 説明 |
---|---|
func contains(_ regex: R) -> Bool where R : RegexComponent | 引数に渡したRegexComponentとマッチするかどうか |
str.contains(/foo/) // Bool | |
func starts(with regex: R) -> Bool where R : RegexComponent | RegexComponentで始まっているか |
func firstRange(of regex: R) -> Range? where R : RegexComponent | 与えられたRegexComponentと初めてマッチした範囲を返す |
func ranges(of regex: R) -> [Range] where R : RegexComponent | 与えられたRegexComponentとマッチした範囲を返す |
func split(by separator: R) -> [Self.SubSequence] where R : RegexComponent | separatorにマッチする部分を取り除いて分割する |
func firstMatch(of r: R) -> Regex.Match? where R : RegexComponent | 与えられたRegexComponentと初めてマッチした箇所を返す |
func matches(of r: R) -> [Regex.Match] where R : RegexComponent | 与えられたRegexComponentとマッチした箇所全てを配列で返す |
以下String, Substring | |
func wholeMatch(of r: R) -> Regex.Match? where R : RegexComponent | 全体としてマッチする箇所を返す。 |
func prefixMatch(of r: R) -> Regex.Match? where R : RegexComponent | 先頭からマッチする箇所を返す。 |
またSwitch文においても以下のような使用方法も出来るようです。
switch "abcde" {
case /a.*f/: // never taken
case /abc/: // never taken
case /ab.*e/: return "success"
default: // never taken
}
switch "2022-04-22" {
case decimalParser: // never taken
case OneOrMore {
CharacterClass.whitespace
}: // never taken
case #/\d{2}/\d{2}/\d{4}/# // never taken
case dateParser: return "success"
default: // never taken
}
SE-0329 Clock, Instant, and Duration
時間や期間を表すオブジェクトが追加されました
主に追加された概念は以下の3パターンです。
- Clock: 時計、時間の測定などの用途
- Continuous Clock: 実世界と同じように動き続ける時計
- Suspending Clock: 逆に動かない、通常のDateオブジェクトに近いイメージ?
- Instants: 瞬間を表現
- Durations: 期間
Clock
「今」という概念 now
や、特定時間のスリープ sleep()
、クロージャの処理時間の計測measure()
を提供する概念で、以下のprotocolで定義されています。
public protocol Clock: Sendable {
associatedtype Duration: DurationProtocol
associatedtype Instant: InstantProtocol where Instant.Duration == Duration
var now: Instant { get }
func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws
var minResolution: Instant.Duration { get }
}
extension Clock {
func measure(_ work: () async throws -> Void) reasync rethrows -> Instant.Duration
}
このClockを実装するオブジェクトとして、ContinuousClock
と SuspendingClock
が用意されています。
ContinuousClock
はマシンがスリープ中であっても中断することなく、そういった状況下で精度良く操作したい場合に有効とのことです。一方、SuspendingClock
ではスリープ中は中断するようです。(このあたりの使い分け方法はよくわからず、、詳しい方はコメントいただけると助かります。)