30
Help us understand the problem. What are the problem?

posted at

updated at

Organization

Swift5.7 新機能まとめ

まえがき

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パターンです。

  1. Clock: 時計、時間の測定などの用途
    1. Continuous Clock: 実世界と同じように動き続ける時計
    2. Suspending Clock: 逆に動かない、通常のDateオブジェクトに近いイメージ?
  2. Instants: 瞬間を表現
  3. 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を実装するオブジェクトとして、ContinuousClockSuspendingClock が用意されています。

ContinuousClock はマシンがスリープ中であっても中断することなく、そういった状況下で精度良く操作したい場合に有効とのことです。一方、SuspendingClock ではスリープ中は中断するようです。(このあたりの使い分け方法はよくわからず、、詳しい方はコメントいただけると助かります。)

参考

swift/CHANGELOG.md at main · apple/swift

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
30
Help us understand the problem. What are the problem?