cockscomblog?

cockscomb on hatena blog

Development Containersのfeatureを作る

OSによって作られるメタデータファイル(.DS_StoreとかThumbs.dbとか)をgitignoreするとき、プロジェクトじゃなくてグローバルの設定にしたい。それで長年 ~/.config/git/ignore にファイルを置いていた。内容はgithub/gitignoreから取ってくる。giboを使っているなら、gibo dump macOS > ~/.config/git/ignore するだけだ。

Development Container

最近Development Containersを使ってみていて、おおよそ気に入っているのだけど、このグローバルなgitignoreの扱いに悩んだ。手元のファイルシステムからマウントされるので、.DS_StoreファイルがDevelopment Containerの中から見えてしまう。しかしグローバルなgitignoreは(あえてマウントしなければ)設定されていないから、gitの差分に出てきてしまう。

もちろんプロジェクトの .gitignore ファイルに書いたらいいのだけど、どうも気乗りしない。ということで、Development Containersのfeatureとして作ってみる。

Featureとは

Development Containersについて何年前かに使ったときは、このfeatureという概念がなかったように思う。コンテナに何か追加したければDockerfileを書くような感じだった。ところが最近では、Development Containerにfeatureを適用することで、必要な機能を追加する。例えばコンテナにNode.jsを入れたければ、.devcontainer/devcontainer.jsonfeaturesghcr.io/devcontainers/features/nodeを書き加える。

  "features": {
    "ghcr.io/devcontainers/features/node:1": {}
  },

このように、featureはOCI Imageとしてパッケージングされ、配布される。Node.jsのfeatureはfeatures/src/node at main · devcontainers/features · GitHubでその実態を見られる。

Featureを作る

自分でfeatureを作るのは、テンプレートリポジトリから始めるのがいちばん良さそうだ。GitHub Actionsもよく整備されている。サンプルとなるfeatureとして、colorhelloが入っている。これを真似していく。

まずテンプレートリポジトリから自分のリポジトリを作る。ひとつのリポジトリで複数のfeatureを提供するのが普通なようだ。このリポジトリ自体がDevelopment Containerで開発するようになっているので、VS Codeからコンテナで開く。

src/以下にディレクトリを作って、devcontainer-feature.jsoninstall.shを置く。サンプルではREADME.mdもあるが、これは後から自動生成されるので、自分で作る必要がない。

src
└── gitignoreglobal
    ├── README.md
    ├── devcontainer-feature.json
    └── install.sh

devcontainer-feature.jsonの仕様に合わせて書けばよい。optionsを定義しておくと、featureへの入力として文字列か真偽値を得られる。

install.shの方が本体で、ここにシェルスクリプトを書く。これはrootとして実行される。Development Containerとして実行する際は、例えばvscodeユーザーなどで実行されるので、その差に注意が要る。実際、gitignoreglobal featureではsystemのgit configを書き換えることにした。あまり上品ではないが、後から作られるユーザーのことを知る由もないので、諦めた。

#!/bin/sh
set -e

GITIGNORE_PATH="$(git config --system --get core.excludesfile || true)"
if [ -z "${GITIGNORE_PATH}" ]; then
  GITIGNORE_PATH=/etc/gitignore
  git config --system --add core.excludesfile $GITIGNORE_PATH
fi
echo "Using global gitignore file: ${GITIGNORE_PATH}"

mkdir -p "$(dirname "${GITIGNORE_PATH}")"
curl -sS "https://raw.githubusercontent.com/github/gitignore/main/${GITIGNORE}.gitignore" >> "${GITIGNORE_PATH}"

optionsで設定した入力値は環境変数として渡されるので、$GITIGNOREとしてこれを使っている。

curlgitdevcontainer-feature.jsondependsOnを設定していることで使えている。

    "dependsOn": {
        "ghcr.io/devcontainers/features/common-utils": {}
    }

テスト

テンプレートリポジトリから始めると、test/にテストが入っている。scenarios.jsonにテストシナリオを書いて、キー名と一致するkeyname.shに実際のテストを書く。test.shはデフォルトのテストということに決まっている。

test/gitignoreglobal
├── macos.sh
├── scenarios.json
└── test.sh

今回はscenarios.jsonで、macOS用のテストを定義する。

{
    "macos": {
        "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
        "features": {
            "gitignoreglobal": {
                "gitignore": "Global/macOS"
            }
        }
    }
}

macos.shは次のように書いた。source dev-container-features-test-libすると、checkreportResultsが使えるようになって、非常に便利。

#!/bin/bash

set -e

source dev-container-features-test-lib

# check <LABEL> <cmd> [args...]
mkdir -p /tmp/test_1
cd /tmp/test_1
git init

check "no difference at start" [ -z "$(git status --porcelain)" ]

touch .DS_Store
check "no difference after adding .DS_Store" [ -z "$(git status --porcelain)" ]

touch NOT_IGNORED
check "difference after adding NOT_IGNORED" [ -n "$(git status --porcelain)" ]

reportResults

実行するにはdevcontainer CLIを使う。devcontainer features test --features gitignoreglobalのようにすると、特定のfeatureのテストを実行できる。Docker in Dockerで、新しいコンテナの中で実行されるので、やろうと思えば複数のベースイメージに対してテストを実行させられる。

またGitHub Actionsでも実行されるようになっている。

デプロイ

デプロイもdevcontainer CLIでできるが、事前に設定されたGitHub Actionsでやるのが簡単だった。GitHub Packagesにデプロイできる。README.mddevcontainer-feature.jsonの内容から自動的に生成され、Pull Requestが作られるので、便利だ。

  "features": {
    "ghcr.io/cockscomb/devcontainer-features/gitignoreglobal:1": {
      "gitignore": "Global/macOS"
    }
  },

書いていて気がついたけど、macOSWindowsが混在している環境だったら複数指定したいと思う。options環境変数マッピングされる都合からか、文字列と真偽値しか受け付けないので、スペース区切りで"Global/macOS Global/Windows"のようにできるとよさそう。

ひとまずDevelopment Containersのfeatureを作ってみた。テンプレートリポジトリやCLIが整備されているおかげで、普通にやるとテストも書けるし、CI/CDも用意できる。OCI Imageとして配布されるのもハイテクな感じがする。全体的によくできたエコシステムと思う。

Apple Vision Pro故障録

お盆前、Apple Vision Proを着けると、左目の視野の下方に黄色い横線が見えた。図示すると次のようになる。

左目の視野下方に黄色い横線

もちろん現実に黄色い線があるのではなく、Apple Vision Proを介した視野にだけ黄色い線がある。嫌な汗が出てくるのを感じつつ、問題を切り分けていく。

  • 右目を閉じたり左目を閉じたりして、左目の視野にだけ表示されることを再確認
  • 再起動を試してみるが、改善しない
  • 環境」を使うと表示されない
  • 空間写真を撮ると、写真の下方にも写り込んだ

この時点で、ビデオパススルー用の左のカメラモジュールに発生した不具合である蓋然性が高い。ディスプレイの問題なら「環境」でも表示されるだろう。ということでGenius Barの予約を取った。

Genius Bar

土曜日にGenius Barへ行った。担当してくださるジーニアスの他に、「後学のため」ということで別なジーニアスも見学することになった。

症状を説明すると、すぐにビデオパススルーカメラの不具合だろうということになる。撮っておいた環境写真が役立った。その後、謎の操作によって診断プログラムモードが起動され、ワイヤレスで診断プログラムが実行される。

Apple Vision Proは店内では修理できないので、修理センターに送る旨を伝えられ、手続きをする。本体とバッテリーが目の前で専用の梱包資材に収納される。この時点で、基本的に無償であることも説明され、一安心。お盆を挟むこともあって、修理には1〜2週間かかるということだった。

ジーニアスの対応は終始丁寧で、ありがたかった。

電話での確認

3日後の火曜日、Appleのカスタマーサポートから電話が来る。エンジニアから質問があるということで、答えていく。

Apple Vision Proの修理についての経験が十分蓄積していないのかもしれないし、あるいは診断プログラムにカメラモジュールからの入力を検査する仕組みがないのかもしれない。人間の視覚に関する部分だから、人間側の方に問題がある場合もあるだろう。

改めて症状を説明し、撮っておいた写真を専用の仕組みでアップロードすると、状況をわかってもらえたようだった。

もちろんこの電話も丁寧だった。

修理

お盆なのか少し間が空いて、翌月曜日の午前に「製品の修理を開始いたします。」というメールが届く。そして夕方に「発送のご案内」メール。

ヤマト運輸の追跡サービスによると「ADSC支店」から「羽田クロノゲートベース」を経由している。これはApple Storeオンラインと同じ経路だが、修理センターも物流拠点と同じあたりにあるのだろう。

翌日、修理されたApple Vision Proが届く。Genius Barで見たのと同じ専用の梱包資材に入っていた。同封の書面によるとシリアル番号が変わっており、本体ごと交換になっていた。動作を確認したところ、当然ながら何の問題もなかった。

金曜日に再びAppleのカスタマーサポートから電話があり、様子を聞かれたので、問題ない旨を伝えた。

所感

まとめると、次のような経過を辿って修理が完了した。修理に出してから戻ってくるまで10日ほどだ。お盆を挟まなければもう少し早いかもしれない。この間Apple Vision Proを使用できなかったが、必需品というわけでもないし、仕方ない。ちょうど忙しいタイミングだったし、まあちょうどいい。

経過
0日目 不具合を発見
1日目 Genius Barで修理に出す
4日目 カスタマーサポートから電話
10日目 修理開始・発送
11日目 修理された製品の受け取り
14日目 カスタマーサポートから電話

また製品保証の範囲内ということで無償だった。Appleのハードウェア保証は原則的に国を跨いで有効なので、アメリカで買ったような場合でも安心だ。

プロセス全体について、Appleの皆さんが極めて丁寧であったことは何度書いても強調し足りないほどである。純粋に親切だったのもあるだろうし、それに加えてApple Vision Proのハードウェア的な問題への対応が現状まだ稀な症例だったということも想像できる。

ということで、またApple Vision Proを活用できるようになりました。

WWDC24予言

今年も書いておく。

AI

今のAppleに最も期待されているトピックであり、Google I/OMicrosoft Buildなどで競合他社が最も力を入れているのがAI。当然WWDC24でも、「AI」という語が飛び交うことになる。

「AI」という語を使い始めたApple

Appleは2024年5月のスペシャルイベントでLogic Pro*1とFinal Cut Pro*2のアップデートを発表したが、そこでは機械学習を活用した機能を「AI機能」と宣伝するようになった。これは例えば、Apple Watchのダブルタップジェスチャーでは「機械学習アルゴリズム」と表現していた*3のと比べると、わかりやすい変化である。

とはいえWWDC24では、AIの中でもとりわけLLMを含む生成AIについて発表されるだろう。ここで、Appleのプライバシー重視の姿勢とどう折り合いをつけるかというのが一つの焦点になる。

オンデバイスクラウドの分担

現在の高性能なLLMを、iPhone/iPad/Macなどのオンデバイスでそのまま動作させるのは、Appleをもってしても不可能だろう。基本的には規模と性能は相関関係にある。もちろんフットプリントを小さくするテクニックはあるし、ハイエンドのMacである程度のものを動かすのは可能だろうが、ごく一部の環境でしか利用できないのでは仕方ない。必然的に高性能のLLMをクラウドで動作させ、オンデバイスでは小規模な言語モデルでも精度の出るタスクを行わせることになる。

例えば、端末内のデータを高性能になったSiriが読み取って応答する、というようなRAGを応用した機能を考える。このとき、端末内のデータのEmbeddingを計算するのはオンデバイスのマルチモーダルなモデルが行い、端末内のベクトル検索エンジンに保存する。高性能になったSiriはクラウド上で動作し、必要に応じて端末内のデータを検索する。検索で類似度の高いと判定されたデータは、端末内で要約され、その結果がクラウドで処理されてSiriの応答になる。

つまり端末内のモデルはEmbeddingの計算と要約タスクだけを行う。こうすると、クラウドに送信されるのは応答に必要な最小限のデータに絞られ、プライバシー上の問題が軽減される。Appleは例えiCloudでデータを同期していても、データをエンド・ツー・エンドで暗号化してクラウド上では復元できないように扱うことが多いため、データ自体をオンデバイスで扱うのが自然だ。

このくらいの仕組みでも、例えばSafariの履歴をSiriで参照するようなことができるだろう。Embeddingや要約は比較的小規模なモデルでも精度が保たれる。

SDKから見たAI

他にもAIに関連した新機能が出てくるものと思われるが、基本的にはオンデバイスで処理するものが多いのではないか。開発者向けのSDKという観点から見ると、オンデバイスで動作するAI機能は、SDKとして公開しやすい。一方でクラウド上で動作する大規模なAIは、現在のWeatherKitがそうであるように、別途何らかの課金体系が定められる可能性がある。

visionOS

もう一つ、WWDC24で重要なトピックになるのは、visionOSのアップデートだろう。この真新しいプラットフォームは、まだ実験的な位置付けというのが実情だ。

アメリカ国外での発売

現在はまだApple Vision Proが米国でしか販売されていないため、visionOSも国際化が行われていない。しかし今年後半には米国外での販売が開始されることが事前にアナウンスされているため、WWDC24ではvisionOSの国際化が発表されるだろう。それがvisionOS 2を待たなければならないのか、あるいはvisionOS 1.2なのか、まだはっきりとしない。

機能の拡充

visionOSは、iOS/iPadOSをベースとしているだろうことから、初期のリリースから一定の品質、機能を持っている。それでも、例えばアプリ一覧を並び替えることができないとか、未完成さを感じさせる部分もある。visionOS 2ではそういったギャップが埋められていくはずだ。

実用上は、ウインドウマネジメントの進歩があると便利だろうと思う。空間の中で自由にウインドウを置けるのは、今ある大きなメリットだが、実際にやってみると、もう少し何か制約がある方が便利だろうと想像される。例えば、ウインドウを物理的な壁にスナップできれば、現実世界との認知的な不協和が起きにくくなるだろうと思う。

空間WidgetKit

本質的に、visionOSの空間コンピューティングはあまり拡張現実的ではない。現実を拡張するというよりは、単に現実の空間内でコンピューティングしている状態だ。これはもう少し機能が拡充されてほしい。Apple Vision Proをわざわざ装着しているからこそ得られる現実の拡張が必要だと思う。例えばWidgetKitが追加されて、壁にウィジェットを貼り付けておけるとか、それくらいでもいい。Apple Vision Proを着けていれば壁が情報のダッシュボードになる、というのは拡張的だと思う。

空間コンピューティング対応アプリ

空間コンピューティングを活かしたアプリが増えるような仕組みがあってもよい。例えばAppleKeynoteは、visionOS専用のリハーサルモードを持っていて、まるで壇上にいるような体験ができる。これは空間コンピューティングを活かした好例だ。しかしKeynoteの他のPagesやNumbersは、まだiPad版が動作するだけだ。Logic ProやFinal Cut Proのようなツールは、空間を活かしたUIを想像しやすいと思う。SwiftUIに空間を活用したUIを作りやすくするような改善が行われるとおもしろいが、想像し難いところでもある。

空間MapKit

同じ理由で、MapKitは空間を活かすのに向いている。そもそも地図が表す現実の世界は立体的だし、MapKitも特に米国内のいくつかの都市では非常にリアルな立体地図で描写できる。

端末間での連携

また端末間の連携についても機能の向上を期待したい。すでにMacの画面のミラーリング機能を備えているが、これをiPadにも拡張したり、あるいはMacの画面であっても単なるミラーリングではなく、Apple Vision Proでは3Dコンテンツを表示できるような、そういった拡張があると嬉しい。あるいは、テキストの入力を連携したiPhoneから行えるとか、そのような機能があってもいい。こういった端末間の連携はAppleの得意とするところでもあるはずだ。

開発ツール

Xcode

XcodeにもAI機能が搭載されることはあまり疑う余地がない。Xcode自体に、AppleがファインチューニングしたXcode AIが搭載される可能性もある。あるいは、Xcodeの拡張の仕組みが改善され、GitHub Copilotが自然に動作するようになるかもしれない。

XcodeについてはSwift Playgroundに導入されている新しいプロジェクト形式の方にも期待がある。

Swift

Swiftは今年秋のSwift 6で大きく変化する。Concurrencyに関連して、データ競合を避けるための言語上の機能がデフォルトで有効になり、データ競合の発生し得るパターンがコンパイラで検出されるようになる。もちろんコンパイラ自体の解析も新たな言語仕様でより親切になり、よりセマンティックに行われるようになる。Swift 6には他にも、ジェネリクスなどに関しての大きめのアップデートもあって、いい年になりそうだ。

SwiftUI

Swift Concurrencyといえば、SwiftUIのConcurrencyをサポートする部分はもう少し便利になってほしい。ReactにSuspenseがあるように、非同期に得られるデータに依存したViewが書きやすくなる仕掛けがあると便利そうだ。

iOS/iPadOS

iOSとパーソナライズ

最近のiOSでは、パーソナライズに関する機能が増えていっている。ロック画面の変化はわかりやすい例だ。WWDC24でもパーソナライズに関連した機能が拡充される可能性は十分にある。iOSがパーソナライズ機能を充実させていることの裏を返すと、要するにiOSが純粋に成熟しているということなのだろう。

iPadOSとプロフェッショナル

iPadOSについても、ライトウェイトなタブレットのOSとしては限りなく成熟している。プロフェッショナル向けのアプリの不足に対しては、Apple自身もLogic ProやFinal Cut Proを開発したし、AdobePhotoshopIllustratorをリリース済み、DaVinci ResolveもiPad向けにリリースされている。もちろん現実的には多くのプロフェッショナルがMacを手放せないにせよ、一定の成果が出ているということにはなる。

iPadOSでのソフトウェア開発

開発者としてはiPadで本格的なソフトウェア開発ができる未来を待ち遠しく思うが、そこはMacを使えばいいということのようにも思う。とはいえ例えばVirtualization.frameworkをiPadOSでも動作させるような方法で、iPadでソフトウェア開発する未来がひらけてほしいとは思う。macOSのVirtualization.frameworkはかなり進歩していて、macOS/Linuxを動作させられる。あるいはVirtualization.frameworkではなく、Linuxカーネルにあるようなコンテナ技術をDarwinにも搭載し、「Darwin Container」のようにしてもいい。サンドボックスと一定の自由を両立させることは不可能ではない。

サイドローディング

サイドローディングについては、2024年3月にEUの規制に合わせて大きく状況が動いた。とはいえあくまでもEUに限定された状況であることには変わりない。このシチュエーション自体は今後も続くのだろうと思われ、法制度で求められれば地域を限定して制限が緩和されるということになるのかと思う。

これはApp Storeのビジネス的側面を考えると、そういうものとしか言いようがない。Apple営利企業だ。といっても、例えばWebKit以外のブラウザエンジンについてはEUに限定しなくてもよいのではないかと思う。WebKitのシェアが落ちて、ブラウザエンジンの寡占化が進むことに対する懸念はあるが。


ということです。

Rust 1.75.0のasync fn in traits

Rustでツールを書こうとして、コンポーネントを差し替えられるようにtraitとして定義した。GUIプログラミングの習い性で、IOが発生するメソッドは非同期にしたいから、asyncキーワードをつける。ここでは、何か文字列を読み込む予定のLoader traitを定義する。

use std::error::Error;

trait Loader {
    async fn load(&self) -> Result<String, Box<dyn Error>>;
}

このコードを書き始めた時点で、このコードは有効ではなかった。Rustではtraitにasyncメソッドを持たせられなかったのだ。そこで、async-trait crateの出番となる。

use std::error::Error;

use async_trait::async_trait;

#[async_trait]
trait Loader {
    async fn load(&self) -> Result<String, Box<dyn Error>>;
}

これを使ってコードを書くと、こういう感じになった。

(Rust Playground)

Rust 1.75.0のasync fn in traits

2023年12月28日に、Rust 1.75.0がリリースされた。

このバージョンから、traitのメソッドをasyncにできるようになった。impl TraitTraitを実装したなんらかの型を表していて、traitのasync fn-> impl Futureの糖衣構文のような扱いになっている。

これを利用すると、async-trait crateを使わなくても同じように書けるはずなので、書き換えてみる。単に#[async_trait]を除去すると、次のようなエラーにぶつかる。

   |
24 |     loader: Box<dyn Loader>,
   |                 ^^^^^^^^^^ `Loader` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>

うわーとなって、「Announcing `async fn` and return-position `impl Trait` in traits | Rust Blog」の記事をよく読み直してみると、制限にぶつかっていることがわかる。特に今回は Dynamic dispatch のところに注目する。

Traits that use -> impl Trait and async fn are not object-safe, which means they lack support for dynamic dispatch. We plan to provide utilities that enable dynamic dispatch in an upcoming version of the trait-variant crate.

動的ディスパッチというのは要するに仮想関数みたいなやつと同じで、コンパイル時に静的に型が決まらないようなケースはまだサポートされていないようだ。ここでさっきのコードを見直すと、確かに、Box<dyn Loader>などとやっている。いずれ公式のtrait-variant crateを使うと、なんらかいい感じにしてくれるようだが、現時点ではまだそういう機能がない。

ということでいったんasync-trait crateに戻ってもよいのだけど、そもそも今回は動的ディスパッチをやめても、いまのところ差し支えない。ChatGPTに「Rustでtraitオブジェクトに対するdynamic dispatchを避けるにはどうしたらよいですか」と聞いてみると、ジェネリクスを使えばいいとわかる(それはそう)。

use std::error::Error;

trait Loader {
    async fn load(&self) -> Result<String, Box<dyn Error>>;
}

struct Processor<L: Loader> {
    loader: L,
}

impl<L: Loader> Processor<L> {
    fn new(loader: L) -> Self {
        return Self { loader };
    }

    async fn process(&self) -> Result<(), Box<dyn Error>> {
        unimplemented!()
    }
}

これで無事にコンパイルできる。

(Rust Playground)

なるほどでした。

2023年の「散財 of the Year」

買ってよかった2023ということで2023年の「散財 of the Year」を発表します。

Best おもちゃ

今年のBestおもちゃはガンプラガンプラは小学生か中学生くらいの頃にいくつか作ったが、それ以来、20年以上ぶりにプラモデルを作った。

小学生になった息子と作ったら楽しかろうと思って、子供でも簡単そうなものと、大人でも楽しめそうなものを少しずつ買った。息子だけでなく4歳の娘もすっかり虜になり、3人でHGのガンプラを作ったりもしている。

自分でも、工具をいくらか買い揃えて、RGを2つとHGを2つ作った。ニッパーでパーツを切り離し、ヤスリでゲート跡を目立たなくして、スミ入れでディテールを足していく。この地味な工程を延々繰り返しながら、最近の出来事を思い返したりする。そういうことをしていると、何やらセラピーのように感じられる。

20年前と比べて今時のガンプラはかなり出来がよく、満足感も高い。細かな作業をしながらセルフリフレクションしたい皆さんにおすすめです。

Best コンピュータ

Bestコンピュータは、MacBook Pro (14-インチ, M3 Pro, Nov 2023)。M3は来年だと思っていたので、驚きつつ購入。スペースブラックがかっこいい。

2023年における計算機の性能を決めるパラメータは、製造プロセスへの依存度が高い。製造プロセスの微細化が進めば、回路規模を大きくしてもダイサイズを維持でき、消費電力も減らせる。ということでTSMCのN3目当てにM1 ProをM3 Proにした。実際、シングルコア性能が向上したためか、GUIがさらに滑らかになったのを感じられる。

Macの性能がいいと、作業へのためらいが減って、少し気楽になるのがよい。

Best ゲームハードウェア

Bestゲームハードウェアは、PlayStation VR 2と少し迷ったが、PlayStation Portalに決めた。

今年は大作ゲームを何本かクリアしていて、ほどほどにコンソールゲームで遊んでいる。PlayStation 5もNintendo Switchもそれぞれちゃんと遊んでいて、もちろんグラフィックスはPlayStation 5に分があるのだが、Nintendo Switchの気やすさの方が好ましく感じられる場面の方が多かった。というところに、Project QことPlayStation Portalが発表。

発売時に手に入れてちょこちょこ遊んでいるが、テレビの前に座らなくていい開放感で、ゲームの体験が大きく変化した。もちろんグラフィックスのよさは(HDRじゃないとか細かいことはあるけれど)折り紙つき。専用ハードウェアでないとできない体験でもあった。

Best 散財

さて、栄えあるBest散財は、3DプリンタBambu Lab P1Sだ。これを買ってから、CADで簡単なモデリングができるようになって、ちょっとしたものを出力している。使用頻度は高くないが、いつでも使える3Dプリンタがあるという事実それ自体に満足している。

モデル共有サイトで自作のモデルを公開してみているが、これまでに500回ダウンロードされ、出力した人がコメントを残してくれている。そういうのもかなり嬉しい。


2023年は円安、物価高で、生活のコストも上がった感があるが、振り返ってみればかなりエンジョイしている。2024年はApple Vision Proあたりでしっかり散財したい。


▶ 【PR】はてなブログ 「AIタイトルアシスト」リリース記念 特別お題キャンペーン
お題と新機能「AIタイトルアシスト」についてはこちら!
by はてなブログ

買ってよかった2023

SwiftにおけるTyped throwsの現在

現在Swift Evolutionで議論されているSE-0413 Typed throwsについて、Swiftの歴史を辿りながら紹介します。

この記事ははてなエンジニア Advent Calendar 2023の9日目の記事です。昨日は id:kouki_daniPadだけでアプリを作ってみるでした。ファスティング中の id:kouki_dan を関モバに誘ったのは私です。お誕生日おめでとうございました。

Swiftのエラーハンドリング

Swiftのエラーハンドリングでは、2015年6月のSwift 2.0のリリース以来、エラーに型がつかない。Errorプロトコルに準拠したなんらかの型が投げられるということだけ決まっていて、それが実際にどうであるかを確認するのは(あるいは確認しないのは)、呼び出し側に任されている。do文のcatch句にはパターンが書けるので、必要に応じてハンドリングできる。

do {
    let xmlDoc = try parse(myXMLData)
} catch let e as XMLParsingError {
    print("Parsing error: \(e.kind) [\(e.line):\(e.column)]")
} catch {
    print("Other error: \(error)")
}

実際にどういった型のエラーが起きるのかは、ドキュメンテーションでしか宣言できない。エラーのハンドリングが網羅的かどうかを機械的に検査することもできない。

Typed throwsに関する初期の議論

このことは度々議論の的となった。2015年12月にはすでに、当時のswift-evolutionメーリングリストで議論されている。Swiftを生み出したクリス・ラトナーは、typed throwsは良いが、Swift 3の resilience モデルまでは問題がある、と返信している。

動的にリンクされるライブラリがエラーをthrowする際に、ライブラリ側が変化してthrowするエラーが変わっても、呼び出し元からはそれを知ることができないから、なんらかの仕組みがないと型の安全性が壊れる、ということだ。

ちなみに当時 resilience モデルと言っていたものは、Swift 3では実現されない。Swift 5.0でのABIの安定化後に、Library Evolutionとして、2019年9月にリリースされたSwift 5.1から利用できるようになった。

エラーの型をパラメータに持つ型

Result

2018年11月にResultを標準ライブラリへ追加するプロポーザルがSwift Evolutionで起案され、1ヶ月後に承認される。そして2019年3月のSwift 5.0でリリースされた。

@frozen public enum Result<Success, Failure> where Failure : Error {
    case success(Success)
    case failure(Failure)
}

これはSwiftのエラーシステムが提供するthrowstrycatchとは全く違う方法でエラーハンドリングを行わせるもので、言語としての一貫性という意味では怪しいところがある。ただし当時の背景からすればこれは妥当で、まだSwift Concurrencyがなく、非同期処理はコールバックで表現されていたため、このようなものが求められていた。実際にサードパーティのResult型が広く使われてもいた。

C++の開発者であるビャーネ・ストラウストラップは「プログラミング言語C++ 第4版」の中で、標準ライブラリの役割のひとつに「ライブラリ間通信を実現するコンポーネントの集合」を挙げている。ライブラリ間でのやり取りに必要な汎用のコンテナ型を提供するのは、標準ライブラリの重要な役割である。したがってResultが標準ライブラリに追加されることには必然性があった。

そしてこのResult<Success, Failure>の型パラメータには、Errorプロトコルに制約されたFailureがある。他のプログラミング言語におけるEither型を考えればこれも妥当であるが、既存のエラーハンドリングモデルとはギャップがある。

Resultgetメソッドやイニシャライザによって、Swiftのエラーハンドリングシステムと相互運用できるようになっている。このときエラーの型はany Errorになる。

@frozen public enum Result<Success, Failure> where Failure : Error {
    @inlinable public func get() throws -> Success
}

extension Result where Failure == any Error {
    public init(catching body: () throws -> Success)
}

Swift ConcurrencyのTask

2021年9月リリースのSwift 5.5で、Swift Concurrencyとしてasync/awaitやActorなどが導入された。ここで導入されたTaskにもResultと同様にFailure型パラメータがある。

@frozen public struct Task<Success, Failure> : Sendable where Success : Sendable, Failure : Error {
}

これもResultに近い。

Primary Associated TypesとAsyncSequence

2022年9月にリリースされたSwift 5.7で、Primary Associated Typeという機能が追加された。標準ライブラリの多くのプロトコルにも設定されたため、この機能でsome Sequence<String>のように書ける。ところが、AsyncSequenceプロトコルにはPrimary Associated Typeが設定されなかった。

AsyncSequence and AsyncIteratorProtocol logically ought to have Element as their primary associated type. However, we have ongoing evolution discussions about adding a precise error type to these. If those discussions bear fruit, then it's possible we may want to also mark the potential new Error associated type as primary. To prevent source compatibility complications, adding primary associated types to these two protocols is deferred to a future proposal. — Primary Associated Types in the Standard Library

Swift Evolutionでは、エラーの型に関する議論が続いているから、とされた。このことでany AsyncSequence<String, any Error>とは書けない。

Typed throws

そして2023年8月に、Status check: Typed throwsが投稿される。9月にはSwift Language Steering GroupのDoug GregorがPitchを投稿し、11月、ついに正式なプロポーザルSE-0413 Typed throwsができた。

実際に試す

ここで実際に動作を試してみる。

最新のdo throws(ErrorType)の構文を試したいので、Swift Forumに投稿された最新のツールチェーンをダウンロードし、~/Library/Developer/Toolchains/に展開する。

Xcodeの場合

Xcodeなら「Xcode > Toolchains」からこれを選択。

あるいは「Manage Toolchains…」でもいい。

Terminalの場合

シェルではツールチェーンのBundle IDを調べてTOOLCHAINS環境変数に設定する。

$ ls ~/Library/Developer/Toolchains/ 
swift-PR-70182-969.xctoolchain

$ /usr/libexec/PlistBuddy -c "Print CFBundleIdentifier:" ~/Library/Developer/Toolchains/swift-PR-70182-969.xctoolchain/Info.plist
org.swift.pr.70182.969

$ export TOOLCHAINS=org.swift.pr.70182.969

$ swift --version                         
Apple Swift version 5.11-dev (LLVM e131e99f323910c, Swift 4d62b1f4e64aa28)
Target: arm64-apple-macosx14.0

実験的フラグの設定

また実験的フラグTypedThrowsを有効にする必要がある。Swift Packageなら、.enableExperimentalFeature("TypedThrows")とするのが簡単だ。

// swift-tools-version: 5.9

import PackageDescription

let package = Package(
    name: "TypedThrows",
    targets: [
        .executableTarget(
            name: "TypedThrows",
            swiftSettings: [
                .enableExperimentalFeature("TypedThrows"),
            ]
        ),
    ]
)

Typed throwsを見ていく

エラーの型を指定するにはthrowsの代わりにthrows(ErrorType)を書く。

enum CatError: Error {
    case sleeps
    case sitsAtATree
}

func callCat() throws(CatError) -> Cat {
    if Int.random(in: 0..<24) < 20 {
        throw .sleeps
    }
    return Cat(name: "Neko")
}

もし宣言したのと違う型のエラーをthrowしようとすれば、そこでコンパイルエラーになる。

func callCatBadly() throws(CatError) -> Cat {
    throw SimpleError(message: "sleeping") // error: Thrown expression type 'SimpleError' cannot be converted to error type 'CatError'
}

catch句では型推論される。

do {
    _ = try callCat()
} catch {
    print(error) // このerrorはCatError
}

ただし、doの中で複数の型のエラーが起きる場合は、any Errorに落ちる。

do throws(ErrorType)で明示的に発生していいエラーを限定できる。もしもそれ以外のエラーが発生するようであれば、そこでコンパイルエラーになる。

do throws(SimpleError) {
    _ = try callCat() // error: Thrown expression type 'CatError' cannot be converted to error type 'SimpleError'
} catch {
    print(error)
}

throws(any Error)throwsと同じ意味で、throws(Never)throwsじゃないのと同じ意味になる。

またエラー型を型パラメータにすることで、rethrowsを置き換えられる。

ということで、全体の規則は難しくない。

Typed throwsを使うべき場面は限定されている

プロポーザルに、Typed throwsを使っていいケースが紹介されている。普通はエラーを網羅的に場合分けしないので、any Errorである方がむしろいい。型があってもいい場面は次の通り。

  • モジュールやパッケージ内に閉じていて、常にエラー処理したい場合は、純粋に実装の詳細であり、もっともらしい
  • ジェネリックなコードで自分自身がエラーを発生させず、利用者が発生させたエラーをそのまま伝える場合
  • 制限された環境下で動作するか、あるいはメモリを割り当てできない場合で、かつ自分自身でしかエラーを作らないとき

1つ目のケースは、つまり外部との境界に表れないなら問題ないということだ。あとからエラーの種類が増えてもモジュール内に閉じているので、特に問題が起きない。

2つ目のケースは、rethrowsと同等の条件だ。これも型は外から与えられるので、実質的にモジュールに閉じる。

3つ目のケースは少し特殊で、組み込み環境のようなものが想定されている。

要するに、モジュールの境界ではまず型をつけない方がいい、ということが書かれている。Typed throwsが利用されすぎることが懸念されている。

Typed throwsの今後

現在のプロポーザルについて、おおよそ全体には好意的に受け止められている。このまま受理されれば、遅くとも来年秋のSwift 5.11頃にリリースされるのではないか。(互換性のためにSwift 6になると少し動作が変化する予定とされている。)

Typed throwsによってResultTaskなどとインピーダンスが揃い、使いやすくなる面が多いだろう。ただしAsyncSequenceにPrimary Associated Typesを設定するのはFuture directionsに示されている通り、for..inの調整も含めて別のプロポーザルを待つ必要がある。

またかねてから議論されていた、throws(FileSystemError | NetworkError)のように複数のエラー型を扱えるようにする話はいったん見送られ、Alternatives consideredに記載された。実質的に匿名enum(直和型と呼ばれることも)を追加することになるため、このプロポーザルのスコープから外されている。


ということで、関西モバイルアプリ研究会A #1で話したTyped throwsでした。

明日は id:papix です。

3DプリンタでMagSafe充電器スタンドをつくる

この秋にリリースされるiOS 17では、充電中のiPhoneに情報を一目でわかるように表示する「スタンバイ」機能が搭載されるそうだ。iPhone 14 Proの常時表示ディスプレイと組み合わせると便利そうだ。

これを活用するには充電中のiPhoneを一定の角度に保つ充電スタンドが必要になる。MagSafe充電器 ワイヤレスなら、充電器ごとに設定を記憶してくれるようだから、MagSafe充電器タイプが望ましい。市場には、MagSafe充電器が一体になったスタンドや、単体のMagSafe充電器と組み合わせて使う製品がある。今回は3Dプリンタを買ったことだから、メイカー精神を発揮してみる。

試作1号

MagSafe充電器の大きさをノギスで測ると、直径は55.9 mm、厚みは5.5 mmある。そこから例によって、Fusion 360モデリングする。MagSafe充電器が少し高い位置に一定の角度で固定されていれば用が足りるので、70度に傾けた枠に愚直に支柱をつける。iPhoneの分だけ重心が手前にくるから、台座も手前に伸ばしてある。

これを3Dプリンタで出力する。ツリー状のサポートをつけている。

一見するとうまくできているが、オーバーハング(せり出し)部分で精度が悪化し、荒れてしまった。内側もその影響を受けて、MagSafe充電器に干渉し、うまく収まらない。パーツを分割して出力するとよいのだろうか。

使用感も確かめてみると、最低限スタンドとしての役には立つ。iPhoneが浮かんで見えるのは格好がよい。ただ、支柱部分の剛性が足りていないのか、iPhoneの重みで僅かなたわみが生まれ、振動してしまう。iPhoneを操作するたびに振動するので、使い心地はよくない。

試作2号

全体的な剛性を確保するために、支柱で支える構造をやめた。ケーブルを裏側にまわすついでに、ひろく穴を空けてMagSafe充電器の充電中の熱が後ろへ逃げるようにしている。

3Dプリンタでの出力時には、MagSafe充電器まわりの精度が出やすいように、倒した状態にしている。

出力してみると、精度は十分で、剛性もある。使用感にも問題がない。

素材が軽いので、iPhoneを近づけると磁力でスタンドの方が動いてしまう。錘をつけると改善されるかもしれないが、困るわけではないので、このままでもよいだろうか。

感想

CADの操作に慣れて、少し複雑な形でもモデリングできるようになった。ただしCADでどんなに自由にかたちを作っても、それでうまくいくとは限らない。素材の特性や加工する装置にあわせて設計しなければ、まさに机上の空論だ。

今回は最低限度、MagSafe充電器スタンドとして使えるものができた。ヒンジで角度を調整できるとか、改良したい箇所もあるが、そもそもヒンジをどう作ったらいいのか、見当がつかない。いくらでも学ぶことがある。

3Dプリンタで何かを作るプロセスは、頭の中にあるものを現実で試し、失敗や成功の経験をするもので、刺激的でおもしろい。これは相当におもしろい。