Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

Swift Macrosを使ってみよう

こんにちは、お久しぶりです。 気づけば入社して一年が経過していました。サカパ・iOSチームの東です。 筋トレを始めたので体がどんどん大きくなっていきます💪
WWDC2023で発表されたSwift Macrosを触ってみたので記事にします。

Swift Macors

今回はCodingKeysのマクロを元に記事にしていきたいと思います。 普段馴染みのないSwiftSyntaxの知識も少し必要になります!

マクロパッケージの作り方

File -> New -> Package... -> Swift Macro

の流れです。NewからProjectを選択するとSwift Macroが選択できないので注意してください。

フォルダ構成

.
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
│   ├── Test
│   │   └── Test.swift
│   ├── TestClient
│   │   └── main.swift
│   └── TestMacros
│       ├── CodableKey.swift
│       └──  CustomCodable.swift
└── Tests
    └── TestTests
        └── TestTests.swift

Swift Macrosの基本(簡易)

Macrosの種類

自立型と付属型の2種類があります。

// 自立型
@freestanding(引数)

//付属型
@attached(引数)

それぞれ引数にマクロの種類が入ります。 引数によってマクロの動きが変わってきます。

freestanding

引数として受け取った値を使用する。関数のようにどこにでも記述可能

変数 意味
declaration 宣言マクロ。struct, var, func...
expression 式マクロ。グローバル関数とほぼ同じ意味
codeItem 宣言も式も生成できるマクロ。

attached

宣言が対象です。class, struct, var等

変数 意味
accessor プロパティ宣言に付与可能。アクセサスコープを出力できる
member 色々な型宣言に付与可能。スコープ内に別の宣言を出力可能
memberAttribute 色々な型宣言に付与可能。スコープ内の変数や関数にattributeを出力する
peer 色々な宣言に付与可能。付与した宣言と同じスコープに別の宣言を出力する
extension 型宣言に付与可能。extensionを出力します。

SwiftSyntax

swift-ast-explorer.com

こちらのサイトより、Swiftの構文解析を行うことがきでます。
これで、Memberへのアクセスや、継承されている名前の参照を簡単に行えるようになります。

CodingKeysのマクロを作成する

マクロの宣言

Test/Test.swift

@attached(peer)
public macro CodableKey(name: String) = #externalMacro(module: "TestMacros", type: "CodableKey")

@attached(member, names: arbitrary)
public macro CustomCodable() = #externalMacro(module: "TestMacros", type: "CustomCodable")

moduleに指定するのは、typeに指定した構造体が存在するフォルダです。
CodableKeyの場合、TestMacrosフォルダ内のCodableKeyを使用するという意味です。

マクロの実装

TestMacros/CodableKey.swift

import SwiftSyntax
import SwiftSyntaxMacros

public struct CodableKey: PeerMacro { // ①
    public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        []
    }
}

CodingKeysで書き換える対象を判別するだけのものなのでCodableKeyの実装はありません。

コード解説

PeerMacroを継承していますが。これはマクロ宣言時のattachedの引数によって変わります。
memberならMemberMacro、accessorならAccessorMacroになります。

TestMacros/CustomCodable.swift

import SwiftSyntax
import SwiftSyntaxMacros

public struct CustomCodable: MemberMacro {
    public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        let memberList = declaration.memberBlock.members // ①
        let types = declaration.inheritanceClause?.inheritedTypes // ②
        guard let types = types,
              !types.map({ $0.as(InheritedTypeSyntax.self)?.type.as(IdentifierTypeSyntax.self)?.name.text == "Codable" }).filter({ $0 == true }).isEmpty
        else {
            throw CustomError.message("Codableは必須です") // ③
        }
        // ④
        let cases = memberList.compactMap({ member -> String? in
            guard let propertyName = member.decl.as(VariableDeclSyntax.self)?.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { return nil }
            // ⑤
            if let customeKeyMacro = member.decl.as(VariableDeclSyntax.self)?.attributes.first(where: { element in
                element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description == "CodableKey"
            }) {
                // ⑥
                guard let customeKeyValue = customeKeyMacro.as(AttributeSyntax.self)!.arguments!.as(LabeledExprListSyntax.self)!.first?.expression.as(StringLiteralExprSyntax.self)?.segments.first?.as(StringSegmentSyntax.self)?.content.text
                else { return nil }
                return "case \(propertyName) = \"\(customeKeyValue)\""
            } else {
                return "case \(propertyName)"
            }
        })
        
        // ⑦
        let codingKeys: DeclSyntax = """
        enum CodingKeys: String, CodingKey {
            \(raw:  cases.joined(separator: "\n"))
        }
        """
        return [codingKeys]
    }
}

コード解説

構造体のメンバをリストで取得しています。
中身はMemberBlockItemSyntaxという構造体です。

CustomCodableをつけた構造体に継承されているものを取得しています。

guard節でCodableが継承されているかチェックし、ビルド前にエラーを表示させています。
今回の例ではCodingKeysを使用するのでCodableを必須にしています。以下のように即座にエラーが表示されます。

①で取得したmembersをCodingKeysのメンバに変換しています。

ここで、先ほど作成したCodableKeyが関係してきます。
attributeに指定されているかどうかをチェックし、変換を行うかを判断しています。

CodableKeyの引数の値を取得します。
今回は引数が1つなので単純に.arguments!.as(LabeledExprListSyntax.self)!.firstとしていますが、引数が複数ある場合は、適切に処理をしなければなりません。(引数名を取得する等)

最終的に結果として返す部分です。
④では、構造体のメンバ分の数の文字列の配列が作成されるので、改行文字列で結合して返します。

Plugin

TestMacros/Plugin.swift

import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct TestPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        CustomCodable.self,
        CodableKey.self
    ]
}

こちらに型を登録しないと利用できないので注意が必要です。

マクロのテスト

マクロについても簡単にテストを書くことが可能です。 プロジェクトを作成した時点でTestsフォルダが作成されているかと思います。

testMacrosにMacroを登録する

TestTests/TestTests.swift

let testMacros: [String: Macro.Type] = [
    "CodableKey": CodableKey.self,
    "CustomCodable": CustomCodable.self
]

こちらで登録を行わないとテスト実行時にマクロの実行ができないので忘れずに行ってください。

テストを書く

マクロのテストを書く際は、assertMacroExpansionを使用します。
(文字列ベースなので誤字、脱字などに注意して下さい)
今回のテスト例

  • 正しくCodingKeysが展開されるか
  • Codableが継承されていない時にエラーが表示されるか

正しくCodingKeysが展開されるか

TestTests/TestTests.swift

    func testCodableKey() throws {
#if canImport(TestMacros)
        assertMacroExpansion(
            """
            @CustomCodable
            struct Test: Codable, Equatable {
                @CodableKey(name: "OtherName1")
                var propertyWithOtherName1: String
                var propertyWithSameName: String
                @CodableKey(name: "OtherName2")
                var propertyWithOtherName2: String
            }
            """,
            expandedSource: """
            struct Test: Codable, Equatable {
                var propertyWithOtherName1: String
                var propertyWithSameName: String
                var propertyWithOtherName2: String
            
                enum CodingKeys: String, CodingKey {
                    case propertyWithOtherName1 = "OtherName1"
                    case propertyWithSameName
                    case propertyWithOtherName2 = "OtherName2"
                }
            }
            """,
            macros: testMacros
        )
#else
        throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
    }

第一引数は実際のコードを書きます。
expandedSourceには、実際にマクロが展開された時に期待されるコードを書きます。
macorsにはtestMacrosにMacroを登録するで宣言したものを指定します。

Codableが継承されていない時にエラーが表示されるか

TestTests/TestTests.swift

    func testCodableKeyFiailureNotInheritedCodable() throws {
#if canImport(CustomMacroLibMacros)
        assertMacroExpansion(
            """
            @CustomCodable
            struct Test: Equatable {
                @CodableKey(name: "OtherName1")
                var propertyWithOtherName1: String
                var propertyWithSameName: String
                @CodableKey(name: "OtherName2")
                var propertyWithOtherName2: String
            }
            """,
            expandedSource: """
            struct Test {
                var propertyWithOtherName1: String
                var propertyWithSameName: String
                var propertyWithOtherName2: String
            }
            """,
            diagnostics: [
                DiagnosticSpec(message: "message(\"Codableは必須です\")", line: 1, column: 1)
            ],
            macros: testMacros
        )
#else
        throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
    }

エラーが表示されるかを確認するには、diagnostics引数を使用します。

まとめ

Swift Macrosはまだ発表されたばかりで、情報も少ないですが使ってみるとかなり興味深いものだと思いました。
上手に使えば、開発の効率を上げることができますが、使いすぎるとかえってコードの可読性が悪くなってしまうので、取り扱いには気をつけたいところです。

いつもの

弊社ではいっしょに働くアプリエンジニアを募集しています! ご興味のある方はぜひご応募いただけますと嬉しいです。

hrmos.co

PyCon APAC 2023 に参加してきました :)

AIR Design for Marketing 事業部 バックエンドエンジニアの大田です。最近ボルダリングをサボっているせいで体重が高め安定してしまっています。久しぶりに会った知人から「ちょっとふっくらしました?」というお言葉を頂戴しました、控えめな表現に優しさが垣間見えますね 😇

タイトルの通り、 PyCon APAC 2023に参加してきましたので、簡単ですがレポート的な記事を書こうと思います 🐍

Day 1

Keynote

京都大学の喜多一教授による「なぜ大学教授がPythonの教科書を書いたのか 」という内容でした。 初学者が躓きやすいポイントなどについて話されていて、それに対してどのように対応していくのかをわかりやすく説明されていました。

やはりいちばんの壁はエラーメッセージで、正常系が動いているうちは前に進めるけれど、エラーが発生してしまうとメッセージを読みたくなくてドロップ・アウトしてしまうことがよくあるとのことでした。 なのであえてエラーを発生させることをして、それを解決することを体験してもらうことでエラーへの恐怖をなくすようなやり方をしているということで納得感のあるお話でした。

Kyoto University Research Information Repository: プログラミング演習 Python 2023

講義で使用するテキストと専用に開発したフォントを公開されているということや、配布しているコードにはテストコードが含まれているというところも印象的でした。

Introduction to Structural Pattern Matching

slides.takanory.net

Python 3.10 からの新機能である構造的パターンマッチングの紹介で、何が嬉しいのかを見ていく内容でした。こういう新機能はなかなか使い始めるタイミングが無いので、こういった場で紹介されているとありがたいですね。

Lunch

白身魚のお弁当。大変美味しかったです。

Python はどのようにデータベースと繋がるのか

Python から PostgreSQL に Socket を使ってどの様にアクセスできるのかをやってみたという内容でした。

意外に(?)シンプルでわかりやすいプロトコルなのが印象的でした。

型チェックを強化するPython 3.11の新機能

pyconapac2023-pep681-slide.ryu22e.dev

Pydantic などのいわゆる「データクラスっぽい」ライブラリに対して型チェックを強化するための標準仕様(Data Class Transforms (PEP681))についてでした。みんな大好き型のお話ですね。

この手のライブラリは IDE 側でも対応していないと補完などが効かなかったりするので、進化に期待ですね。

Dev Containers時代のPython開発環境のあり方

vscode で使える Dev Containers のお話ですね。最近は JetBrains IDE でも使えるらしいので、ちょっと導入してみたいと思っています1

Day 2

Python で一歩踏み出すバイナリの世界

events/pycon.apac.20231026 at main · rhoboro/events · GitHub

UTF-8 の仕様のお話などされていました。「16進数はまずは 0,7,8,F だけ覚えておけば良い」2というのはなるほどと思いましたね。 最近業務で実装したID生成のことを思い出したりしました :)

Lunch

回鍋肉弁当。こちらも美味でした :)

20階の Unconference 会場でお弁当を頂いていたのですが、PyCon PH の方とお話させていただきました。 私の英語スキルは単語を羅列できる程度なのですが、フィリピンの学校でマイクロソフトの支援(?)でC#が教えられていることなど興味深いお話ができました。 (私が仕事でC#をやっていたことがあるという話から発展しました)

PyCon APAC ブースでお菓子とステッカーを配っていると教えていただき、頂戴してきました :)

お菓子美味しかったです!

Comparison of Packaging Tools in 2023

pipenv, Poetry, PDM, pip-tools, Hatch, pip(, Rye) の比較をされています。 まとめとしてはライブラリ開発者は Hatch 、アプリケーション開発者は PDM が良いのではということでした。3

PDM は pyproject.toml に記述する依存関係の標準4に対応していて依存性解決も速いということなので、早速 Poetry を使っているプロジェクトを PDM に切り替えてみて試しています。 使い勝手は Poetry と変わりなく、特に問題なく動いているようなのでこのまま正式に切り替えようかなと思っているところです :)

まとめ

久しぶりのオフラインでのカンファレンス参加で正直疲れてしまった部分もありますが、やはり同じ開発者がリアルで話すという場のありがたさを感じました。 すぐに業務に取り入れることができるトピックもあり、刺激をもらえる2日間でした :)


  1. 少し試してみたのですが、 JetBrains IDE では現状エラーメッセージなどが表から見えなくてすんなり使えるとは言えない感じに見えます。残念。
  2. 0 = 0000, 7 = 0111, 8 = 1000, F = 1111
  3. Rye はまだ安定していないし個人開発なので、様子見が良いのではということでした。
  4. PEP725のはず...

超久しぶりにインフラ構築したときにやったこと 〜SSM でポートフォワーディング編〜

サービス開発パートナー事業部 Android エンジニアの松下です。最近はストリートファイター 6 を遊んでいます。とても難しい。

弊社ではバックエンド領域も扱う案件も少ないながらに存在していますが、事業部としても個人的にも結構ご無沙汰になっていました。
これを機にインフラ構築を行ったときの備忘録を残したいと思います。 AWS です。

続きを読む