Location via proxy:   
[Report a bug]   [Manage cookies]                

cockscomblog?

cockscomb on hatena blog

Androidアプリ「Font List」を公開しました

Font List

この記事の終わりには、いろいろあってAndroidアプリを公開しました、というオチになるわけだけど、そこまで長いので、いったんアプリをお知らせします。

システムフォントを一覧するアプリです。無料。どうぞご利用ください。

Font List

前日譚

2024年11月10日、Google Play Supportからメールが届いた。件名はこうだ。

[ご対応のお願い] デベロッパー アカウント(cockscomb)に問題があります

筆者のGoogle Play Consoleデベロッパーアカウントが使用されていないため、利用者の安全性を維持するため、閉鎖の警告を受けた。

筆者がGoogle Play Consoleのデベロッパーアカウントを開設したのは、このメールからおよそ1年前、2023年11月10日のことだ。というのも、2023年11月13日以降に個人としてデベロッパーアカウントを登録すると、一定の基準をクリアしない限りアプリを公開できなくなる。それより前に登録しておけばこの制約を逃れられるというので、25ドル払ってデベロッパーアカウントを開設しておいたのだった。

ところがその後放置状態にあったため、「休眠アカウント」という扱いになり、閉鎖の危機を迎えた。一度閉鎖されるとそのデベロッパーアカウントは削除されてしまい、新たに登録すると、やはり前述の制約下に置かれることになる。

最初のメールの時点で解決の期限は59日後。1ヶ月後に2度目のメールがあり、そして元日に3度目のメールが届く。残り8日。

残り1週間を切ったところで、解決のために活動を始める。解決するには、まだ何もないデベロッパーアカウントなので、何かアプリを作成して公開するしかない。慌ててAndroid Studioを起動する。

開発動機

そもそも普段使っているのはiPhoneだから、Androidで作りたいアプリとかないのだけど、ないなりにしばらく考えたところ、以前欲しかったアプリのことを思い出した。

Android OSをアップデートした時か何かに、ブラウザで日本語フォントのウェイトの感じが変わったような気がした。もともと端末に搭載されている日本語フォントはウェイトが少なかったのだけど、見知らぬウェイトが表示された、とかだったように思う。それで端末に搭載されているフォントの様子を知りたい。ところが標準アプリにそういった機能はなく、Google Playでもいいアプリを見つけられなかった。

ということで端末に搭載されているフォントを一覧するだけのアプリが欲しい。ちょっとAPIを調べたところ、SystemFonts.getAvailableFonts()というのを見つけた。

これじゃんということで、Android Studioでプロジェクトを作る。さすがにAndroidの最近の開発事情をうっすらとは把握しているので、Jetpack Composeのテンプレートを選ぶ。

フォント名

UIとかは置いておいて、SystemFonts.getAvailableFonts()を呼び出して、エミュレータで様子を見てみる。Fontの集合がちゃんと返ってくる。それじゃあフォントの名前のリストでも表示するかと思ったが、まったくこれがどうにもならない。

このFontというクラスには、フォントの名前にあたる情報を取得するメソッドが生えていない。フォントファイルそのものにはアクセスできる。なるほど……。

ということで、フォントファイルをちゃんと読んで、フォントのファミリー名などを取得していくことにする。

手元のAndroid端末では、OpenType、TrueType、TrueType Collectionの3つの形式のフォントが使われているようであった。現実的にもこれくらいに対応しておれば十分だろうということで、これらをパースするコードを書いていく。3つというと多く感じられるが、メタデータだけ読むくらいなら3つはほとんど共通している。

フォントファイルはいろいろなテーブルが並んでいるような構造で、冒頭にそれぞれのテーブルへのオフセットが記録されている。TrueType Collectionの場合は、ひとつのファイルの中にそれが複数並んでいて、冒頭にそれぞれへのオフセットが記録されているような形だ。これを順にパースしていって、目的のテーブルを参照する。

フォント名はnameテーブルに入っているので、これを読み出す。

nameテーブルにはNameRecordが並んでおり、フォントファミリーの名前だけでなく、いろいろな文字列メタデータが格納されている。NameID1のレコードを探すと、フォントファミリーの名前がわかる。

ちまちまnameテーブルを読んでいくことで、フォントの一覧ができてくる。

SystemFonts.getAvailableFonts()

ここで改めてSystemFonts.getAvailableFonts()の返り値Set<Font>について見ていく。ここで返ってくるFontはフォントファイルひとつひとつに対応するのかと思っていたが、実際には違った。

まずはTrueType Collectionで、フォントファイルひとつに複数のフォントが格納されている。これが個別のエントリとして返る。TrueType Collection内部のどれかということについては、Font.getTtcIndex()で示される。

フォントファイル ttcIndex axes 備考
/system/fonts/NotoSerifCJK-Regular.ttc 0 N/A Noto Serif CJK JP
/system/fonts/NotoSerifCJK-Regular.ttc 1 N/A Noto Serif CJK KR
/system/fonts/NotoSerifCJK-Regular.ttc 2 N/A Noto Serif CJK SC
/system/fonts/NotoSerifCJK-Regular.ttc 3 N/A Noto Serif CJK TC

Noto Serif CJKは、CJKの各言語のフォントをまとめたTrueType Collectionになっている

もうひとつ、Variable Fontがある。フォントファイルは単一のVariable Fontで、その可変なパラメータ違いが個別のエントリとして返る。パラメータはFont.getAxes()で取得できる。

フォントファイル ttcIndex axes 備考
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 26 Thin
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 39 ExtraLight
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 58 Light
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 90 Regular
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 108 Medium
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 128 SemiBold
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 151 Bold
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 169 ExtraBold
/system/fonts/NotoSansKhmer-VF.ttf 0 wdth: 100, wght: 190 Black

Noto Sans Khmerは、ウェイトの異なるVariable Font

Variable Font

アプリではフォントファミリー名に加えて、フォントのウェイトなどを表示したい。「Regular」や「Bold」、あるいは「Italic」のようなラベルになる。これは通常、フォントのサブファミリーとしてnameテーブルから取得できる。しかしVariable Fontの場合はそうはいかない。

Variable Fontにとってのウェイトは、wghtというaxisの値で表現される。値はフォントにもよるが、100から900までの任意の整数で表せる。これをどうラベルにするといいのか。

ここでVariable Fontのfvarテーブルを使う。fvarテーブルには、Variable Fontのパラメータの軸(axis)の定義と、インスタンスと呼ばれる典型的な設定が格納されている。例えばwghtが100、200、300、400……、の各インスタンスとそれに対応する名前のIDが記録されている。名前はnameテーブルの該当のIDを取ってきて、それぞれ「Thin」「ExtraLight」「Light」「Regular」……、と表示できる。

これを逆引きして、axisの値からインスタンスを探すと、いい具合にウェイトの名前などを表示できた。

UI

データが取れたということでJetpack ComposeでUIを作る。単純にリストを作るだけなら簡単。特にこだわりもないので、シンプルに済ませた。

Material 3というやつがみんな好きなんだろうと思うので、なるべくそこからはみ出さないようにする。といってもTop App Barをそれらしくするくらい。同僚の記事を参考にした。

ナビゲーション時のトランジションがちょっと違う感じがするけど、いろいろ調べても容易にはMaterial 3のトランジションを再現できないので妥協した。

審査

2日間の開発でアプリがなんとなくできた。まだいくらでもやったらよさそうなことが思いつくが、そうしている間にデベロッパーアカウントが閉鎖されると本末転倒。ということで、Google Play Consoleでリリースを作成し、アプリをアップロードした。アプリ名もこだわりないので、フォントの一覧なら「Font List」だろうと決めた。アイコンもなんかそれらしく15分で作成。諸々のメタデータを埋めて、審査に提出。最近はGoogle Playもしっかり審査してくれる。

この時点で、Google Play Consoleのデベロッパーアカウントの閉鎖の危機は去った。

数営業日で審査が終わり、アプリを無事公開。

いかがでしたか

システムフォントが知りたい皆さまに大変おすすめの製品となっております。

もうちょっとよくできる気がするので、暇なときに改善するかもしれない。

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 です。