cockscomblog?

cockscomb on hatena blog

WEB+DB PRESS Vol.125の特集「GraphQL完全ガイド」を執筆しました

f:id:cockscomb:20211017224108p:plain 今週、10月23日(土)に発売されるWEB+DB PRESS Vol.125に掲載される、特集記事「GraphQL完全ガイド」を執筆しました。よろしくおねがいします。

桃栗三年、GraphQL 6年

原稿を書く過程で、知っているはずのことでも改めて調べなおしたりする。特に歴史みたいなのが好きで、GraphQLは2015年6月に発表されて、2018年に安定版になって、みたいなのをずっと調べてしまう。GraphQLってなんかすごい最近っぽく感じていたけど、発表されてからもう6年経つらしい。

ちなみにjQuery 1.0は2006年8月にリリースされていて、Reactは2013年5月に公開されたらしい。6年というのはだいたいそれくらい。

6年で、GraphQLはよく普及した。Facebookはもちろん、GitHubTwitterNetflixも、GraphQLを使っている。GitHubの新しいプロジェクトでも、普通にGraphQL APIを使っている

GraphQL、使っていますか

筆者は2018年からGraphQLを使っている。仕事で隙あらばGraphQLを導入した。サーバー側もクライアント側も何度も作った。そこで得た知見をこのブログに書いたりもした。

なんでもそうだけど、慣れてしまえばなんでもないが、最初は取っ付きにくい。GraphQLもそういうところがある。ということで、なるべく網羅的に説明するようにした。GraphQLをまだ使ったことがなかったり、本格的に取り組んだ経験がない場合に、特に役に立つようなつもりで書いた。

お買い求めください

見本誌が届いたので知っているのだけど、どの記事もおもしろい。例え僅かな可能性としてGraphQLに興味がない場合でも、WEB+DB PRESS Vol.125を買わない理由にはならない。

筆者はよくGihyo Digital Publishingを使う。リフローのできるEPubと、プロが丁寧にレイアウトしたPDFのどちらも入手できて、便利だ。

Amazonで買っても書店で買ってもいいと思う。全部便利だと思う。

雑誌の原稿を書くのは、前回iOS 14に関する特集を同僚らと書いて以来の二度目だが、そうそう慣れるものでもなく、必死だった。編集の皆さまにもお世話になり、また同僚や家族の助力も得て、ようやくというところ。

ということで、何卒よろしくお願いいたします。

こちらからは以上です。

GitHubの新しいプロジェクトを使ってみている

新しいものが好きなので、GitHubの新しいプロジェクトで仕事をしている。まだベータ版だが、少し前に会社のアカウントで有効になったので、所属するチームで大喜びで使い始めた。

もともとしばらくAsanaを使っていて、Asanaはタスクに依存関係がつけられたりして気に入っている。とはいえ仕事の大部分はGitHub上のリポジトリで行うのだから、GitHub上で完結するなら試してみたいわけである。

スプリント

ということでとりあえず仕掛かりのIssueなどをプロジェクトに入れていたのだけど、なんかまだしっくりきていなかった。ドキュメントにはベストプラクティスとかもあるけど、そんなにおもしろいことは書いていない。Single source of truth志向なのはいいと思う。

しっくりこなかったことの一つは、スプリントの表現がうまくいかない感じがしたからだった。Single source of truthということでリポジトリマイルストーンを使う感じかと思ったが、しかし扱っているリポジトリは残念ながら複数あって、それぞれにマイルストーンを設定してまわりたくはない。

と思っていたら今日になっていいのが出た。

このiteration field typeというのが最高。これで何もかも上手くいった。

GitHubの新しいプロジェクトは、今まさにバリバリ開発している感じでおもしろい。

自動化

プロジェクトにIssueとかPull Requestを追加するところは、けっこう面倒に感じる。ということで自動化を試す。

Automating projects (beta) - GitHub Docs

複数のチームに関連する特別なリポジトリで、特定のラベルがついたIssueを、自分のチームのプロジェクトに追加する、というのをやってみた。次のようなGitHub Actionsを設定した。

name: Projects
on:
  issues:
    types:
      - labeled
jobs:
  projects:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/github-script@v5
        with:
          github-token: ${{ secrets.GITHUB_TOKEN_FOR_PROJECTS }}
          script: |
            const script = require('./.github/workflows/lib/projects.js');
            await script({ github, context, core }, {
              '特定のラベル': 1,
            });

「特定のラベル」がつけられたIssueを「#1」のプロジェクトに追加する、というマッピングが書いてある。シークレットからPersonal Access Tokenをもらって使っている。このトークンの権限は、プロジェクトを操作するために org:write が必要であるほか、Issueの情報を取得するために repo:read も必要だった。

.github/workflows/lib/projects.js は次の通り。

module.exports = async ({ github, context, core }, projectMapping) => {
  const headers = {
    'GraphQL-Features': 'projects_next_graphql',
  };

  const projectNumber = projectMapping[context.payload.label.name];
  if (projectNumber === undefined) {
    return;
  }

  const project = await github.graphql(
    `query($organization: String!, $projectNumber: Int!) {
      organization(login: $organization) {
        projectNext(number: $projectNumber) {
          id
        }
      }
    }`,
    {
      headers,
      organization: context.repo.owner,
      projectNumber,
    }
  );
  const projectId = project.organization.projectNext.id;

  const item = await github.graphql(
    `mutation($projectId: ID!, $contentId: ID!) {
      addProjectNextItem(input: { projectId: $projectId, contentId: $contentId }) {
        projectNextItem {
          id
        }
      }
    }`,
    {
      headers,
      projectId,
      contentId: context.payload.issue.node_id,
    }
  );
  core.exportVariable('itemId', item.addProjectNextItem.projectNextItem.id);
};

ドキュメントでは gh コマンドを使っているが、値を引き回すのが面倒なので、actions/github-script@v5 を使ってJavaScriptで書いた。スクリプトを別なファイルに置いているのはエディタで書きやすいから。でも別なファイルに分けると actions/checkout@v2 が必要になって、もったいない気もしている……。

actions/github-script@v5 を使うとOctokitが使えるのだけど、ヘッダの扱いがよくわからず、Octokitのコードを読む羽目になった。

ラベルが外されたらプロジェクトから取り除くのもやりたかったのだけど、プロジェクト内のアイテムから該当のものを探すのが簡単でなかったので、いったん諦めた。

ということで

いろいろタイミングが合ったので、GitHubの新しいプロジェクトを使ってみている。まだ開発中という感じだけど、その分だけ将来に期待できていいと思う。

Swift ConcurrencyのwithTaskCancellationHandlerとSendable

Swift 5.5がリリースされた。おめでとうございます。

Swift 5.5の目玉はもちろんSwift Concurrencyだ。言語機能として並行処理がサポートされた。async/awaitの構文だけでなく、Structured Concurrencyとしての整理や、actorの導入など、野心的な取り組みと言える。

Swift Concurrency

Swift Concurrencyに直接関係するSwift Evolutionの提案はこれだけある。

これだけの提案が行われ、実装されたことに圧倒される。

The Swift Programming Language」にもConcurrencyの章が追加されている。こちらではわかりやすく説明されているので、概要を掴みやすい。

そろそろ本題。

タスクのキャンセル

Structured Concurrencyでは、親のタスクがキャンセルされると子のタスクもキャンセルされる。キャンセルされたかどうかはTask.isCancelledTask.checkCancellation()とかで確認できる。

withTaskCancellationHandler

タスクがキャンセルされたときに何か実行するにはwithTaskCancellationHandlerを使う。

func withTaskCancellationHandler<T>(operation: () async throws -> T, onCancel handler: @Sendable () -> Void) async rethrows -> T

onCancel@Sendableであることは重要なので、覚えておいてください。

コールバックを引数に取る関数からasync関数を作るwithUnsafeThrowingContinuationwithTaskCancellationHandlerを組み合わせる例が、SE-0300 Continuations for interfacing async tasks with synchronous codeのAdditional examplesにある。

import Foundation

func download(url: URL) async throws -> Data? {
    var urlSessionTask: URLSessionTask?

    return try await withTaskCancellationHandler {
        try await withUnsafeThrowingContinuation { continuation in
            urlSessionTask = URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            }
            urlSessionTask?.resume()
        }
    } onCancel: {
        urlSessionTask?.cancel()
    }
}

最新のAPIに合わせて少し書き換えているが、これは次のエラーでコンパイルできない。

Reference to captured var 'urlSessionTask' in concurrently-executing code

var urlSessionTask: URLSessionTask?varであるため、エラーが起きている。これはdiag::concurrent_access_of_local_captureというメッセージだった。

diag::concurrent_access_of_local_capture

エラーを出しているのはここ。

ちょっとコードを読むと、「flow-sensitive concurrent captures」なる機能についても言及されているが、今はフラグで無効になっている。

これはキャプチャした後に変更されないとわかっていればOKとするもので、今回のケースではいずれにせよコンパイルが通らないのが正しいように見える。

varじゃなくてletにして、参照型を間に挟めばコンパイルが通る。

import Foundation

class Wrapper<Wrapped> {
    var value: Wrapped
    init(_ value: Wrapped) { self.value = value }
}

func download(url: URL) async throws -> Data? {
    let urlSessionTask: Wrapper<URLSessionTask?> = Wrapper(nil)

    return try await withTaskCancellationHandler {
        try await withUnsafeThrowingContinuation { continuation in
            urlSessionTask.value = URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            }
            urlSessionTask.value?.resume()
        }
    } onCancel: {
        urlSessionTask.value?.cancel()
    }
}

一見よいように思うかもしれないが、var変数のキャプチャはNGでこれはOKなのって不思議じゃないですか?ということで、Other Swift Flags-Xfrontend -warn-concurrencyを設定する。

f:id:cockscomb:20210926102445p:plain
Other Swift Flags

-warn-concurrency

-warn-concurrencyってなんなんだという話だけど、これは互換性と関係している。Swift 5系では破壊的な変更を避けつつ、並行処理でデータ競合を避けるための道具を提供する。そしてSwift 6ではチェックを厳密にすることで、デフォルトでデータの競合について安全になる。-warn-concurrencyオプションをつけると、Swift 6で問題になる箇所を予め知るためのもの、ということらしい。

さて、-warn-concurrencyオプションをつけると、該当の箇所に次の警告が現れる。

Cannot use let 'urlSessionTask' with a non-sendable type 'Wrapper<URLSessionTask?>' from concurrently-executed code

onCancel@Sendableクロージャだが、そこからSendableではない値を参照していることで、警告が出た。class Wrapper<Wrapped>@unchecked Sendableにすれば警告は消えるが、Wrapperがちゃんとスレッドセーフなわけではないので、コンパイラを誤魔化しているだけの効果しかない。

Sendable

Swift 6でチェックされる項目の一つはSendableについてである。

Sendableの提案は受理されているが、現時点では実装済みになっていない。Swift 6でデフォルトでチェックされるようになるのを待っているのだと思うが、Swiftの標準ライブラリでは既に使われているし、自分たちで使うこともできる。

Sendableというのはactor間でやり取りできる型を表すマーカープロトコルである。マーカープロトコルはコンパイラに対する目印。

Sendableにできるかどうかは、例えば次のようになっている。

  • structのような値型は、Copy on Writeなので、actor間でやり取りしても競合が起きない。変更可能なプロパティがSendableであればSendableにできる
  • classは参照型なので、変更可能なプロパティを持っていないときだけSendableにできる。ただしfinalじゃないといけない
  • actorは参照型だけど内部状態をデータ競合から保護しているのでSendable

classだけど、内部的にデータ競合を防ぐ仕組み(ロックとか)を持っていることもある。そういうときは@unchecked Sendableに指定できる。これはコンパイラにチェックされないSendableである。

そしてクロージャ@Sendableアノテーションすると、クロージャもSendableになる。SendableなクロージャがキャプチャできるのはSendableだけである。

Swift Atomics

ここでとりあえずapple/swift-atomicsManagedAtomicLazyReferenceを使ってみる。要するにデータの競合が排除されていればいいので、今回の用途には大袈裟な感じもするが、使ってしまう。

import Foundation
import Atomics

func download(url: URL) async throws -> Data? {
    let urlSessionTask = ManagedAtomicLazyReference<URLSessionTask>()

    return try await withTaskCancellationHandler {
        try await withUnsafeThrowingContinuation { continuation in
            let task = urlSessionTask.storeIfNilThenLoad(URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            })
            task.resume()
        }
    } onCancel: {
        urlSessionTask.load()?.cancel()
    }
}

ManagedAtomicLazyReferenceは、遅延して初期化される参照型をメモリ管理しつつ保持してくれる。

Swift Atomicsの1.0.2時点ではManagedAtomicLazyReferenceはSendableではないが、次のバージョンではSendableになるはずだ。

次のようにしてこれを先取りする。

extension ManagedAtomicLazyReference: @unchecked Sendable where Instance: Sendable {}
extension URLSessionTask: @unchecked Sendable {}

勢いよくURLSessionTaskもSendableにしてしまっているが、スレッドセーフなはずなので大丈夫だと思う。

ここまでやって、ついに-warn-concurrencyでも警告が出なくなったはずだ。

実際にはresumeする前にもキャンセルのチェックをしないと、タイミングによってうまくキャンセルされないはずなので、チェックする。ここまでのコードをまとめると次のようになるはず。

import Foundation
import Atomics

extension ManagedAtomicLazyReference: @unchecked Sendable where Instance: Sendable {}
extension URLSessionTask: @unchecked Sendable {}

func download(url: URL) async throws -> Data? {
    let urlSessionTask = ManagedAtomicLazyReference<URLSessionTask>()

    return try await withTaskCancellationHandler {
        return try await withUnsafeThrowingContinuation { continuation in
            let task = urlSessionTask.storeIfNilThenLoad(URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            })
            if Task.isCancelled {
                continuation.resume(throwing: CancellationError())
                return
            }
            task.resume()
        }
    } onCancel: {
        urlSessionTask.load()?.cancel()
    }
}

いかがでしたか?

withTaskCancellationHandlerを題材にしてSwift Concurrencyの-warn-concurrencyを試してみたつもりですが、いかがでしたか?なんとなく大袈裟に感じたように思う。現実には、URLSessionはすでにSwift Concurrencyに対応したインターフェースを持っているので、こういうコードを書くことはない。とはいえ-warn-concurrencyすると、普通のアプリでもSendableに関連した警告がいくらでも出てくるのではないか。

Swiftは安全な言語を志向しているから、デフォルトでデータ競合からも安全になっていく。(これは別にSwiftに限ったことではない。)このことはポジティブに受け止めていいはずだし、Swift 6が楽しみである。

とはいえ現状では、-warn-concurrencyでやっていくのは少し難しい。スレッドセーフなclassでもSendableがついていなかったりするので、単純にやりにくい。Swift自体もConcurrencyのサポートを始めたところで、Swift 6までにまだよくなるだろうから、焦らなくてもいいはずだ。

特にアプリの開発では、たまに-warn-concurrencyを有効にしてみて、なるほど、くらいでいいような気がする。反対にライブラリの場合は、今のうちから-warn-concurrencyを設定しておくと、将来的な破壊的変更を避けられるかもしれない。

こちらからは以上です。

WWDC21大夢想

毎年この時期になると、毎日のようにWWDCのことを夢想している。

去年はSwiftUIのアップデートとApple Silicon搭載のMac、ホーム画面のウィジェットに期待していた。

去年の期待は、いろいろなことをうまく言い当てているようにも見えるし、少し過剰なところもあった。WWDC20では叶わなかったいくつかの部分については、引き続きWWDC21でも期待している。

ではWWDC21では何が発表されるのか。

Swift

2014年にSwiftが発表されてから7年になる。SwiftはOSSで開発されているので、次にどのようなアップデートがあるか、事前に窺い知ることができる。

swift-evolutionによると、次のバージョンはSwift 5.5となり、特に並行処理の言語的なサポートに注力されている。async/awaitの構文や並行処理の単位としてのTask、actorモデルの導入が決まっている。

OSSではありながらも、プロジェクトの大きな方向性はAppleが握っており、Appleプラットフォームと足並みを揃えている。裏を返せば、Swiftの開発状況からAppleプラットフォームの方向性を占えるということになる。

例えば基本的な部分でも、Concurrency Interoperability with Objective-Cによって、既存のAPIがSwiftの並行処理の仕組みで扱いやすくなるだろう。しかし何よりも、SwiftUIに期待が集まる。

SwiftUI

SwiftUIが今年も大きく強化されることは疑いようがない。

Swiftの並行処理とSwiftUIがどのように連携するのかは、大きなトピックだ。少なくともCombineフレームワークとの相互運用性が担保されるはずだ。例えばStructured concurrencyTaskとCombineのFutureは形が似ているし、あるいはCombineのPublisherAsyncSequenceはコンセプトが近いように感じられる。これらがうまく連携しないはずはない。

CombineとSwiftの並行処理が関連づけられるだけでも、最低限SwiftUIからSwiftの並行処理が扱える。しかしそれでは少しまどろっこしい。ObservableObjectにasyncメソッドを生やしたくなるし、それをTask.detatchすることなくViewから直接呼び出せたらとも思う。ViewbodyプロパティにEffectful Read-only Propertiesでasyncやthrowsをつけられるようになればおもしろい。おもしろいが、これはとりもなおさず副作用を持つということで、扱いが難しくなる懸念もある。

ところで、actorモデルがSwiftUIとの接点を持つのか計りかねている。プロセッサがメニーコアの時代を迎えたいま、複数のコアを効果的に活用することは至上命題であり、actorモデルは並行性の複雑さに対する強力な武器になる。Actorを導入すると、actorで分離されたデータを参照するような処理はすべてasyncになる。SwiftUIがこのような非同期的な処理をうまく扱えると、actorを使いやすいはずだ。

他にも、Extend Property Wrappers to Function and Closure Parametersでプロパティラッパが色々なところで使えるようになるが、これがSwiftUIの新しいAPIに利用される可能性もある。

当然ながら、新しい組み込みのViewも多く追加されるだろう。ナビゲーション周りがもう少し抽象化されると便利なように思う。テストを書くための仕組みも必要だ。

Swift Package Manager

Swift Package Managerもまた、Swift 5.5でしっかりと進化する。これはおそらくXcodeに影響を与える。Xcode 11からパッケージ管理にSwift Package Managerが利用できるようになっているが、引き続き連携が改良されることだろう。Xcodeがパッケージ管理の機能を強化していくことで、サードパーティのツールに依存せずに開発が行えるようになっていく。

要するに、Xcode for iPadに期待している。

パッケージ管理の強化に伴ってもう一つ期待したいのは、Appleが提供するオフィシャルなライブラリの導入である。Androidで言うJetpackのように、OSに含まれるフレームワークとは別のライブラリが導入されるとおもしろい。OSに含まれるライブラリはOSと同時にしか更新されないし、利用者がOSをアップデートしない限りは、アプリ側で新しい機能を使えない。裏を返すと、OSに内蔵できるフレームワークというのは、より保守的なものになる。しかしSwift Package Managerで導入できるライブラリはもっと自由度が高い。高い頻度で改善できるし、バージョンの古いOSに向けてバックポートすることもできる。Appleが高品質で多様なライブラリを提供することで、アプリのエコシステムが大きく改善されるはずだ。

データ

Core Dataは、SwiftUIから扱いやすいようなサポートが行われている。Environmentに.managedObjectContextが用意されているほか、@FetchRequestFetchedResultsを使って簡単にデータを取得できる。さらにNSManagedObjectObservableObjectになっていることで、データの変更に追従できる。

いっけんよさそうに思われるが、実際に使ってみると、少し使いにくい。例えばNSManagedObjectのプロパティは、プリミティブ型でない限り、デフォルト値が指定されていてもOptionalになってしまう。

Swiftから扱いやすいSwiftDataのようなものが登場するのを期待する声がある。Swiftの並行処理ともうまく連携されていたらさらに便利そうだ。これは別にCloudKitでもいいかもしれない。

OS

WWDCでは各OSのメジャーアップデートが発表されるのが常である。

iPadOS

今年一番の期待はiPadOSのアップデートだろう。Apple M1を搭載した高性能なiPad Proが発売され、その評判のほとんどは、ハードウェアの性能をソフトウェアが活かせていない、というものだ。当然そんなことはAppleだって百も承知のはずであるから、iPadOSは飛躍的な改善がなされるに違いない。そうでないと困る。

マルチタスキングに関する改善は、想像しやすい。自由にウインドウを配置できるようになるとは思いにくいが、それでもふたつ以上のアプリを自由に行き来しながら利用できるようになってほしい。また外部ディスプレイとポインティングデバイスを利用しているときに、外部ディスプレイをミラーリングではなく拡張されたデスクトップとして利用できるようになってほしい。

プロ向けのアプリは当然必要だ。Final Cut ProやLogic Pro、そしてXcodeiPadバージョンが発表されることに期待したい。Xcodeは例えばSwiftUIアプリだけを開発できるのでもよさそうだ。ところで、プロ向けのアプリが作りやすいように、UIKit側にも拡張があっていいはずだ。iPadOS 14でサイドバーや新しいピッカーが追加されたように、例えば複数のUIWindowをタブで扱えるようなUIとか、そういうものが増えてもいいと思う。

iOS

iOS 14ではホーム画面のウィジェットが一大トピックだった。ウィジェットiOSをより魅力的なものにしたと思う。ウィジェットの体験を演繹すると、いくつかの可能性が考えられる。

ひとつはインタラクティブウィジェットである。ウィジェットはその仕組みから、リンクを設定する以外にはインタラクティブな要素がない。これは余分なリソースを使わないための仕組みであるから、それが変わるとは思いにくいが、例えば事前に用意しておいた表示と行き来させるとか、そういった対応は可能なはずだ。

また別な発想で、アプリのアイコンをウィジェットと同じ仕組みで変えられるようにする、というのも考えられる。天気予報のアプリとかで有用だろうと思うし、特に技術的な制約があるとは思えない。

あるいはホーム画面にとどまらずに、ロックスクリーンの表示をカスタマイズできるようにする方向性もある。ロックスクリーンをうまく設定できるようになると、通知以外の方法で即時性の高い情報を得られるようになりそうだ。

加えて、iOS 5から搭載されているSiriも、ついに10歳だ。近年では音声アシスタントとしてだけでなく、様々なAI機能のブランドになってきているが、そろそろ大きなアップデートがあってもよさそうに思う。

macOS

昨年のmacOS 11でユーザーインターフェースがアップデートされ、またApple Silicon搭載のMaciOSアプリがそのまま実行できるようになった。macOS 12は、それと較べると規模の小さなアップデートになると噂されている。過去の例からすると、ユーザーインターフェースはもう少し調整されていくだろうから、macOS 12でも改善が期待できる。

iOSやiPadOSと較べれば、macOSは始めから生産性のためのOSである。新型コロナウイルスパンデミックですっかり様相が変わったこの世界では、生産性もまた再定義されつつある。このことからして、macOS 12では(もちろん他のプラットフォームでも)コラボレーションに関連する新しい機能がフィーチャーされるのではないかと予想する。特にメッセージアプリは、macOS 12でMac Catalyst製になったこともあって、iOSと合わせて改善しやすい状況になっており、わかりやすい例になると思う。

他にも、iOSやiPadOSからmacOSに持ち込まれるものがあるかもしれない。例えばショートカットアプリがmacOSにも導入されると便利だろうと思う。Automatorと競合するのがネックではある。あるいはTestFlightのmacOSバージョンがあってもよさそうだ。

watchOS

watchOSは、ヘルスケア関係の機能が強化されるであろうこと以外には、あまり想像が及ばない。今秋に発売されるであろうApple Watch Series 7でハードウェアの刷新があるとすれば、watchOSアプリもその影響を受けて、ユーザーインターフェースがリフレッシュされる可能性がある。

tvOS

tvOSはここ数年、比較的マイナーなアップデートにとどまっている。順当に考えれば、今年もそうなる可能性が高いように思われる。HomeKitに関連した部分では、今年ついに共通企画となるMatterが策定されており、対応する機器が出てくるであろうという情勢だ。tvOSは家庭内の機器のハブとしての役割が担わされており、なんらかのアップデートがあるかもしれない。

サービス

Appleはサービス面でも拡大を続けている。iCloudWWDC 2011で発表されたサービスで、こちらも今年10周年を迎えようとしている。WWDC21でもiCloudに新しい機能が加わる可能性はあるだろう。

あるいはまだ日本でサービスインしていない、Apple NewsやApple Fitnessについても、サービスする地域の拡大はもちろん、その強化が期待されるところである。ニュースとプラットフォーマーの関係は昨今でもホットなトピックである。信頼できるニュースが21世紀も生き残っていくためには、そのエコシステムが重要である。

ハードウェア

昨年のWWDC20でMacApple Siliconへ移行させていくことが発表されてから1年経った。その間にMacBook AirMacBook ProMac Mini、そしてiMacの一部が、Apple M1を搭載するようになった。サードパーティApple Siliconへの対応状況なども含めて、現況がしっかりと宣伝されるのは想像に難くない。

WWDC21ではその続報が望まれる。Apple M1XなのかApple M2なのか(Apple A14と同じ世代ならM1Xであろうと思われるが)わからないものの、より高性能な(TDPの大きな)Apple Siliconを搭載したMacがアナウンスされる可能性は高いだろう。何しろApple SiliconはAppleにとっての虎の子であるはずで、Macがこれほど注目されるタイミングは他にない。

ほか

Apple GlassesのようなXRプラットフォームは毎年期待している。そろそろ何か発表されてもよいようにも思うが、なにもわからない。

ということで

WWDCへの期待が溢れ出ている。

SwiftUIのDynamicPropertyを試す

SwiftUIにはDynamicPropertyというprotocolがある。

これを使ってみようという趣旨の記事を見かけた。

ので、私も試してみました。

@Now

import Combine
import SwiftUI

class Clock: ObservableObject {
    @Published private(set) var date: Date = Date()

    init() {
        Timer.publish(every: 1, on: .main, in: .default)
            .autoconnect()
            .assign(to: &$date)
    }
}

@propertyWrapper
struct Now: DynamicProperty {
    @StateObject private var clock = Clock()

    var wrappedValue: Date {
        get {
            clock.date
        }
    }
}

こういうのを作っておいて

import SwiftUI

struct ContentView: View {
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .none
        formatter.timeStyle = .long
        return formatter
    }()

    @Now private var date: Date

    var body: some View {
        Text(date, formatter: Self.dateFormatter)
    }
}

こう使う。

これでContentViewは毎秒更新され、現在時刻を表示し続ける。

DynamicPropertyって何

ドキュメントには、Viewが再計算される前にDynamicPropertyupdateメソッドを呼んでくれるくらいの情報しかないが、実際にはもうちょっといろいろあるようだ。

上記の例でもわかるように、@ObservedObjectの更新によってViewが再描画される。

ここで急に、SwiftUIのCore Dataサポートのように、@FetchRequest的な形で何かできるんじゃないか、と気づくと思う。

実際にRealmにはそういう機能があった。

現実のユースケース

DynamicPropertyでは、StateなりStateObject(またはObservedObject)なり、あるいはEnvironment(やEnvironmentObject)を、Viewのpropertyで使えるようだ。

これは少しReact Hooksに似ている。ReactのuseStateとSwiftUIの@Stateの対称性のように、ReactのCustom Hookとちょっと似ている。

とはいえ、@Nowの例は、単にObservableObjectをそのまま使えばいいわけで、独自のDynamicPropertyを作ることが正当化されるような場面は稀かも。

Relayに学ぶGraphQLのスキーマ設計

2018年の初めくらいから、仕事でGraphQL APIを何度も作っている。サーバーサイドもクライアントサイドも実装している。

最近クライアント側にRelayを使ってみている。

GraphQLのクライアントとしてはApolloを使う場合が多いと思うが、Facebook製のRelayもかなりよくできている。以前はTypeScriptに対応していなかったが、今はTypeScriptも使える。最近のバージョンではhooksのAPIがexperimentalではなくなり、ReactのSuspense API(Suspense for Data Fetchingは使わずに)と合わせて使える。

RelayはGraphQLのスキーマに制約を設けることで、クライアント側のAPIがデータの再取得やページネーションなどを抽象化している。換言すると、Relayからデータの再取得やページネーションに必要なスキーマ上の制約を学べる、ということだ。

ということで、スキーマの設計について抽象的な理解を得たので、それを記す。

RelayのGraphQL Server Specificationに倣う

GraphQL Clientとしてrelayを利用することもそうでないこともあるが、いずれにしても、これに倣っておくとうまくいくことが多い。

RelayのGraphQL Server Specificationは、再取得とページネーションのための仕様である。

Node

id という ID! 型のフィールドを持った Node インターフェースを定義する。

interface Node {
  id: ID!
}

ID はグローバルに(型に関わらず)ユニークな識別子。内部的には base64(type + ":" + internal_identifier) のような実装にすることが多い。クライアントサイドでは透過的な値として扱い、パースを試みるべきではない。

トップレベルのクエリに node(id: ID!): Node を持つ。Node インターフェースに準拠する型は、このトップレベルクエリから取得できるようにする。

type Query {
  node(id: ID!): Node
}

このようにしておくと、Node はクライアントから便利に扱える。キャッシュのノーマライズもできるし、再取得も簡単である。

Connection

Connectionはページングに関連している。要するに、ページネーションを行うフィールドで Connection というのを返したりすると、カーソルベースのページネーションができる。

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}
type User implements Node {
  id: ID!
}
type UserEdge {
  node: User!
  cursor: String!
}
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
}
type Query {
  users(first: Int, after: String, last: Int, before: String): UserConnection!
}

組み合わせ

例えば Userfriends(first: Int!, after: String!): UserConnection! というページネーションできるフィールドがあるとき、2ページ目を取得するには User から指定しなければならない。

type User implements Node {
  id: ID!
  friends(first: Int!, after: String!): UserConnection!
}
query {
  node(id: "xxx") {
    ... on User {
      friends(first: 10, after: "yyy") {
        ...
      }
    }
  }
}

このようにネストしたフィールドでのページネーションでは、目的のフィールドまでのパスが一意に定まる必要がある。

Viewerパターン

サービスにもよるが、GraphQL APIを認証情報付きで呼び出している利用者を、type Viewer として表し、トップレベルから viewer フィールドで取得できるようにする。

type Viewer {
  name: String!
}

type Query {
  viewer: Viewer
}

このようなフィールドがあると、例えばログインしている利用者の名前を表示する際に便利なショートカットになる。従って利用者自身に紐づくリソースは Viewer に持たせるとよい。

Viewer 以外にも Visitor のような語彙でもよさそうではあるが、統一されている方が便利なので、特別な理由がなければ合わせるとよい。また、Relayではこのようなフィールドを Viewer に決め打ちして特別扱いしている。

取得可能性

type Query のフィールド、Node、そしてViewerパターンでは、いずれも目的のオブジェクトを直接取得可能である。使い分けは、グローバルにユニークなオブジェクトなら type Query のフィールド、そうでなければNode、そして現在の利用者を指すショートカットとしてViewerパターン、となるだろう。

Relayではもうひとつ、@fetchable ディレクティブと fetch__Xxx というトップレベルのクエリを組み合わせることができる。

directive @fetchable(field_name: String!) on OBJECT

type User @fetchable(field_name: "id") {
  id: ID
}

type Query {
  fetch__User(id: ID!): User
}

見ての通り、ほとんどNodeと同じである。

合わせて4つが、直接取得可能なフィールドということになる。Relay Compiler(Rustで書かれている)でも、これらが特別扱いされている。

GraphQLで扱うオブジェクトは、なるべく直接取得ができるように設計されていると、取り回しがよい。例えばパーマリンクとして扱うには直接取得が可能でなければならないだろう。

Mutationの戻り値

Mutationでオブジェクトを作成、更新、もしくは削除したときの戻り値をどうするか。

更新

GraphQLクライアントは普通、内部にノーマライズされたキャッシュを保持している。

例えばTwitterを作るとして、ツイート一覧画面について考える。一覧でツイートを選択して、個別のツイートの画面で「いいね」する。その後ツイート一覧に戻ってきたとき、一覧の中でも該当のツイートが「いいね」状態になっていてほしい。

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Tweet implements Node {
  id: ID!
  text: String!
  likeCount: Int!
  viewerLiked: Boolean
}

type TweetEdge {
  node: Tweet!
  cursor: String!
}
type TweetConnection {
  edges: [TweetEdge!]!
  pageInfo: PageInfo!
}

type Query {
  node(id: ID!): Node
  timeline(first: Int!, after: String): TweetConnection!
}

このとき、TweetNode なので、GraphQLクライアントは id の値をキーとしてキャッシュをノーマライズする。Mutationとして like(tweetID: ID!): Tweet! が用意されていれば、これの戻り値を使ってキャッシュを更新できるため、「いいね」状態の一貫性が維持される。

要するに、特に Node の更新については、戻り値は Node そのものであればよい。

作成

作成されたオブジェクトが一覧にも表示される場合、取得されたConnectionのキャッシュにも要素を付け加える必要がある。

大まかには、戻り値が Node であればよい。RelayのConnectionを利用している場合、戻り値をEdgeにしてもよい。戻り値を使ってConnectionのキャッシュを書き換える必要がある。

Relayの場合、@appendEdge / @prependEdge もしくは @appendNode / @prependNode といったディレクティブが用意されており、Connectionの前後に要素を付け加えるのが少し簡単になっている。

削除

オブジェクトを削除する場合も、やはり一覧での表示から取り除く必要がある。

Connectionから取り除くには、Nodeid さえわかっていればいい。従って戻り値は ID! で十分である。

Relayでは @deleteEdge ディレクティブが用意されている。

まとめると

GraphQLのスキーマを設計するとき、とりあえず従っておくべき指針をRelayから得た。

まずはRelayのGraphQL Server Specificationに従っておく。NodeとConnectionを実装する。

そして直接的に取得可能な、type Query のフィールドやNode、Viewerパターンを意識する。

Mutationの戻り値は、作成や更新ならNode、削除はIDが基本になる。

例外はいくらでもあると思うが、最初はこれくらいから考えると概ね妥当と思う。

Work From Living Roomで使うマイク

プロローグ

西暦2021年、COVID-19のパンデミックによって人類が活動の抑制を余儀なくされてからおよそ1年が経っていた。一部のデスクワーカーは在宅勤務、「Work From Home(WFH)」にシフトすることで、他者との物理的な接触を避けながら職務を継続した。ビデオ会議が対面でのミーティングにとって代わり、人々はこぞってカメラやマイク、照明を買い漁った。


……というプロローグでやっているわけだが、人によって住環境も様々で、Work From HomeというかWork From BedroomとかWork From Living roomのような状況の人も多いと思う。かくいう私も部屋数の少ない賃貸住宅で暮らしており、まさにWFLの様相を呈している。

リビングの端で窓を背に陣取っていて、それなりに快適にやっているのだけど、ビデオ会議のときに生活音が入ってしまうのが気になっていた。特に子供がうるさくする音は、会議に同席する同僚たちは寛容にも気にしないでいてくれるのだけど、それでも居心地はよくない。そのうち引っ越したら仕事部屋を用意しよう、くらいにのんびり構えていたが、最近になって急に、指向性のあるマイクが気になり始めた。

ということでマイクについて調べてみました。

指向性と構造

指向性については、audio-technicaマイクロホンの指向特性のページがわかりやすい。なるほど、シンプルだけど存外に物理的な仕組みである。

ついでにaudio-technicaマイクロホンの内部構造のページを見ると、「ダイナミック型」や「コンデンサ型」のような駆動方式があり、空気の振動をどうやって電気信号に変換するかというものだが、方式によって感度などに差が出るということだった。

これまで、なんとなくコンデンサ型の方が音質がいいような印象を持っていたが、実際には用途に合わせるとよいようである。生活音を入れたくないならダイナミック型の方が合っているのかも。

何を買ったらいいのか

なんとなく理屈はわかってきたが、パソコンに容易に接続可能なダイナミック型のマイクはあまり多くない。Blue Yetiのような製品はコンデンサ型だった。オーディオインターフェースから揃えるとなると面倒。

というところでいろいろ教えてもらって、ゲーミング用のヘッドセットもいいと聞いたので心が揺れた。しかし昨年末に AirPods Maxを買ってしまっていて、ついでにPlayStation 5用のPULSE 3D ワイヤレスヘッドセットも持っているので、頭に着けるものがこれ以上増えることについて、妻がいい顔をしなかった。

結局どうしたかというとShure MV7を買いました。これは2020年11月に発売されたもので、なんとなくよさそうな雰囲気があるが、あまりわかっていない。とにかくUSB接続できて、ダイナミック型で、カーディオイド型の単一指向性がある。あと見た目がいい。

試しに買ってみるにはちょっとpriceyではあるが、しかしマイクの良し悪しについて、まして生活音をどれくらい拾わないかなんて、事前にわかるはずもない。だから決め手は id:nagayama さんが使っているという事実である。nagayamaさんが使っているなら間違いなくないですか。

ということでnagayamaさんが使っていることをもって妻も納得し、購入に至った。口から近い位置に設置できた方がいいので、適当な安いアームも買った。ディスプレイのUSBポートに接続したので、Macをディスプレイに繋げばマイクも使える状態になった。

2週間ほど使ってみたところ、同僚からの評判はいい。いい声になったとさえ言われる。録音して試してみた感じでは、同じ部屋で子供が騒がしくしていてもかなり小さな音量になっている。Discordのようにノイズキャンセリング処理のしっかりしたツールならほとんど伝わることがない。これが他の製品よりよい結果なのかどうかはわからない、かなり満足した。

f:id:cockscomb:20210228095551j:plain
ディスプレイを介して接続されたShure MV7


エピローグ

パンデミックを機に、新しい暮らし方、働き方を模索する動きも多い。私の勤務先でも「フレキシブルワークスタイル制度」というのが導入されている*1。マイクを買ったことで、ビデオ会議への心理的な負担が小さくなったのはもちろん、いいマイクを持っているという不思議な充足感が得られた。

気をよくして、3歳の息子とPodcastごっこをしてみたが、ハマっているゲームについて話すばかり。10年後くらいに聞き返したい。