From 2fdef48750af89597c765ec2bbf362c2fecaa2dc Mon Sep 17 00:00:00 2001
From: nsfisis 2022-04-09 から 2022-04-11 にかけて開催された、PHPerKaigi 2022 に、一般参加者として参加した。
+弊社デジタルサーカス株式会社 はダイヤモンドスポンサーとなっており、スポンサー枠のチケットを使わせていただいた。 昨年のレポートはこちら。 多くの素晴らしいトークの中から、特におすすめのものを 5つ選んだ。是非聞いてほしい。引用部分は、リンク先プロポーザルから引用している。 予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント PHP はバージョンを追う毎に型宣言、例外、表明、列挙型などの機能が大幅に強化され、堅牢なコードを書くための機能が充実してきました。それらの機能はどう使うと効果的なのでしょうか。 本講演では PHP 8.1 をベースにして、誤りを想定してチェックするのではなく、そもそも誤りにくい設計とはどのようなものか、つまり「予防」の観点を軸足に、堅牢なコードを導くための様々な設計のヒントをご紹介します。 PHPを使ってるとよく遭遇する Fatal error / Parse error / Warning / Notice 理解していますか? 毎日流れてくるエラーに皆さんはどう向き合ってますか? ISUCON11のPHP実装は、何を考え、どのようにして作られていたのか 昨年開催されたISUCON11にて問題(参考実装)のPHPへの移植を担当させていただきました。 最終的なソースコードこそシンプルなWebアプリケーションではありますが、その裏には 本発表はそれらを共有することで チームの仕事はまわっていたけど、メンバーはそれぞれモヤモヤを抱えていた話──40名の大規模開発チームで1on1ログを公開してみた サイボウズの大企業向けグループウェアのGaroon(ガルーン)は、PHPで開発されている20年目の製品です。ガルーン開発チームは日本で40名、ベトナムで50名の計90名ほどのチームになっています。また、コロナ禍でフルリモートでの活動がこの2年ほど継続してきました。 フルリモートになっても仕事はまわっており、継続的にリリースはしていましたが、一方でお互いの考えていることや感じている問題意識が見えづらくなり、モヤモヤを抱えているメンバーが増えていました。 このセッションでは、そういう状況で私がチーム外からジョインし、聴き役に徹しながら見える化することで状況を改善していった取り組みを紹介します。同じように大きなチームやリモートワークで難しさを感じている人に、難しさの原因への気づきや取り組みへのヒントがあれば幸いです。 今回は、PHPer チャレンジ用に弊社のトークン問題を 3題作成した。こちらについては別途記事にしているので、そちらを参照されたい。 1位になった。 去年の参加レポ では、こんなことを書いた。 1つ個人的な反省点としては、(中略) Discord しかりアンカンファレンスしかり「このイベントのこの瞬間にしかないコンテンツ」に触れずに、後から見返せる発表やスライドに注力してしまった、ということだ。発表の詳細な見直しはあとからできるのだから、今しかできないことを考えるべきだった。 この反省を踏まえ、今年は積極的にほかの場 (公式の Discord サーバや、アンカンファレンス) にも参加した。 なお、アンカンファレンスについては、1日目の終わりにトークン問題の解説放送もおこなった。 また、今年はオフラインとオンラインのハイブリッド開催であったが、去年の全オンラインと比べて、オンライン参加の体験が落ちていなかったのは、特筆すべきであろう。
+今年は 3回目のワクチン接種が間に合わなかったこともあり現地参加は見送ったのだが、来年は是非オフラインで参加したい。 PHPerKaigi 2023 があるかどうか存じ上げないが、あるとすれば、次の 4つを目標としたい。 最後になりましたが、PHPerKaigi のスタッフ、スポンサー、スピーカーのみなさん、素敵な時間をありがとうございました。 ではまた来年。 本日開始された PHPerKaigi 2022 の PHPer チャレンジにおいて、弊社 デジタルサーカス株式会社 の問題を 3問作成した。この記事では、これらの問題の解説をおこなう。 リポジトリはこちら: https://github.com/nsfisis/PHPerKaigi2022-tokens ソースコードはこちら。実行には PHP 8.1 以上が必要なので注意。 この問題は、単に適切なバージョンの PHP で動かせばトークンが得られる。 まず目につくのは大量の絵文字だろう。
+PHP は識別子に使用できる文字の範囲が広く、絵文字も使うことができる。 Brainf*ck のインタプリタとプログラムになっている。
+Brainf*ck とは、難解プログラミング言語のひとつであり、ここで説明するよりも Wikipedia の該当ページを読んだ方がよい。 https://ja.wikipedia.org/wiki/Brainfuck なお、brainf*ck プログラムを普通の書き方で書くと、次のようになる。 実行結果はこちら: https://ideone.com/22VWmb それぞれの絵文字で表された関数が、各命令に対応している。 なお、 おおよそ意味に合致するよう選んでいるが、 ソースコードのライセンスを示したこの部分だが、 完全に合法な PHP のコードである。
+ ソースコード中に、ほとんど数値リテラルが書かれていないことにお気づきだろうか。
+PHP では、型変換を利用することで任意の整数を作り出すことができる。 また、 三項演算子ないし 不動点コンビネータを使って無名再帰する (詳しい説明は省略する。これらの単語で検索してほしい)。
+ここでは、一般に Z コンビネータとして知られるものを使った ( 実際のところ、 なお、PHP は末尾再帰の最適化をおこなわない (少なくとも今のところは) ので、
+あまりに長い brainf*ck プログラムを書くとスタックオーバーフローする。 ソースコードはこちら。実行には PHP 8.0 以上が必要なので注意。 さて、この問題はさきほどのように単純に実行しただけでは、謎のブロックが表示されるだけでトークンは得られない。
+トークンを得るためには、ソースコードを読み、定数 ここでは、私の想定解を解説する。 まずはソースコードを読んでいく。 数値からなる まずは排他的論理和 (xor) を取り、 二進数に変換して、 0 を空白に、1 を 5文字ごとに区切ったあと、改行で結合している。 次に、ソースコードに書いてあるヒントを読んでいく。 ここで、PHPer トークンは必ず ここまでわかれば、あと一歩で解ける。すなわち、 なのでブルートフォースしてもよいが、ここではブルートフォースしない方法を紹介する。 この一連の変換に対する逆変換を考えると、次のようになる。 これを実行すると、 ソースコードはこちら。 コメントにもあるとおり、次のようにして実行すれば答えがでてくる。 実際にはもう少しパイプで繋げなければならない。 コメントにもあるとおり、これは quine (風) のプログラムになっている。
+Quine とは、自分のソースコードをそっくりそのまま出力するようなプログラムのことである。 このプログラムは、実行すると自身とほとんど同じプログラムを出力する。
+異なるのはトークンになっている部分のみである。 トークンの何文字目まで出力したかを、ソースコードを変えずに (quine なので) 覚えておく必要がある。
+このプログラムでは、トークンが出力されるとソースコードがだんだんと長くなっていくのを利用して、 Quine は、素朴に書くとプログラムの一部が 2回記述されてしまう。
+これがあまり美しくないので、 それにしてもなぜこんなものが標準ライブラリに……。 解いていただいたみなさん、また、難易度調整につきあっていただいた社内のみなさん、ありがとうございました。 今回は直前に作りはじめたのもあり、3問だけかつ使い古されたネタばかりになってしまいましたが、
+来年は 5問、より面白い問題を持っていきます。 実はもう作りはじめているので、どうか来年もありますように……。 2021-03-26 から 2021-03-28 にかけて開催された、PHPerKaigi 2021 に一般参加者として参加した。
+弊社デジタルサーカス株式会社 (今年1月から勤務) はダイヤモンドスポンサーとなっており、スポンサー枠のチケットを使わせていただいた。 このようなカンファレンスには初めて参加するのでかねてより心待ちにしていたのだが、生憎2日目から体調を崩してしまい、この記事も途中までとなっている。まだ見ていないセッションも多いが、ひとまず現時点での参加レポを書いておく。 発表はトラック A、B に分かれていたのだが、今回はすべて A トラックを視聴している (切り替えるのが面倒だっただけ)。 発表・スライドのメモ (引用ではない) 感想など PHP で AWS Lambda Rails のプロジェクトを PHPer のメンバのみでメンテ
+→他のメンバもわかる PHP にリプレースを検討 サーバレス ゼロベースで作れる案件が (Railsの件とは別に) あるため、そちらで試験的に導入? AWSの学習
+AWS のドキュメント
+DevelopersIO AWS Lambda のカスタムランタイムで PHP を動かす サーバのセットアップや維持管理を気にしなくて良い
+サーバーレスで PHP を動かすツールがすでにある
+サーバーレス構築はすんなり 今は Laravel がルーティングしている
+Laravel Livewire を Lambda に載せられないか?
+デプロイ方法は?
+バッチ処理は? (Lambda は 15分の制限) Lambda でコンテナイメージがサポートされるように 抽象化されたもの「だけ」しか知らないよりも具象の理解は助けになる AWS Lambda のような Function as a Service はマイクロサービス化における一つの到達点に思えるのだが、これを使って実際に web サービスを作る具体的なイメージがまだ見えない (注: すべて for me として書いている)。 PHP on AWS Lambda があれだけ簡単に動かせるのには驚いた。 勝手に AWS Lambda だとフットプリントの軽さが求められそう (= PHP + Laravel などでは動かなさそう) だという先入観を持っていたのだが、この発表のデモによればそうでもないらしい。 大規模サイトの SEO 大規模サイト (100万ページ以上)
+Google の基準 クロールバジェットを意識したSEO 大規模サイトでコンテンツが中頻度 (1回/週) で更新 OR 中規模サイト (10,000以上) でコンテンツが目まぐるしく変更される
+これを満たさないなら、クロールバジェットを考えなくてもいい サーチコンソール
+「カバレッジ」の「除外」
+多すぎるのは問題→クロールバジェットを浪費している リニューアル前のURL インデックスは移行される
+リンクのURLが存在する限り、別のURLとしてクロールされる
+リダイレクトされるとはいえ、リニューアル前のURLは移行した方が良い
+リニューアルで無視されるようになったパラメータも注意 robotes.txt で拒否しているのにクロールされる
+一時的に拒否を外して 404 や 301 を読ませる
+内部リンクを確認する
+JS でのリンクに書き換え クエリパラメータからURLのパスに
+ URL 設計だいじ SEO (Search Engine Optimization) は大して知らないので新鮮な話が多かった。その分語れることも少ない……。 知覚可能
+操作可能
+理解可能
+堅牢 ちゃんとしたHTMLを書く (閉じタグ・入れ子構造など) 標準の HTML を適切に使う WAI-ARIA キーボードフレンドリー マシンフレンドリー SEOフレンドリー button タグ
+→キーボード
+h1 タグ
+→スクリーンリーダー・クローラ
+a タグ WAI-ARIA
+HTML では表現できないセマンティクスを追加する まずは標準の HTML 要素で解決する
+何でもかんでも WAI-ARIA を使えばいいというものではない マウスホバーでツールチップが出てくるが、キーボード操作では出てこない VoiceOver 全ての属性を使う必要はない
+あくまでアクセシビリティを上げるための方法の一つにすぎない つい最近 WAI-ARIA についての記事を読んだばかりだったので個人的にタイムリーな話題だった。(あまりこの言葉を使いたくないのだが) いわゆる「健常者」にとって、こうした問題を普段の生活の中で意識するのは難しい。だからこそ情報へのアンテナは張っておくようにしたい。 PHP で FUSE 個人的に楽しみだった発表。 VFS (virtual filesystem) vs 具体的なファイルシステム 最適な実装方法は状況により異なる アプリケーションに見せるAPIは変えずに実装を隠蔽する→VFS カーネルのプログラムを作るのは難しい Filesystem in USEr space (FUSE) SSHFS / s3fs / Docker Desktop Linux 以外でも使える VFS: システムコールが呼ばれると、ファイルシステムによってコール
+FUSE: カーネル空間からユーザ空間へ通信 高レベルなラッパで型をつける PHP 以外では Wordpress を FUSE にマウントする実装がある (C, Python など) 期待通りの興味深い発表だった。FUSE 自体も今回の発表で知ったのだが、これ本体の実装を見るのも面白そうだ。
+この発表を聞きながらファイルシステムにマウントできそうなものを考えていたのだが、およそ木構造をしているものすべてと言えそうだ (ハンマーしか持っていないと云々)。何かできそうだがなかなか思いつかない。 ATDD ユーザストーリーの受け入れ条件が曖昧になりがち
+デグレチェックがユニットレベルでは収まらない場合、手動で同じシナリオをテストしている Q2の強化
+アジャイルテストの4象限 技術面/ビジネス面
+開発チーム支援(コーディング前・コーディング中)/製品批評(コーディング後) Agile Alliance
+ユーザストーリーのスキルレベルを高める テストピラミッド UI Tests Service Tests Unit Tests 異なる粒度のテストを書く 高レベルになるほど、持つべきテストは少なくなる 高レベルテストが多すぎる→アイスクリームコーン アンチパターン ATDD (Acceptance TDD)
+API経由・UI経由での高レベルテスト E2E test ストーリ受け入れテスト 入れ子のフィードバックループ
+ATDD(外側) と TDD(内側) 外部品質・内部品質 バーティカルスライスのデリバリー ユビキタス言語
+手動テストもspecに書く
+自動化は可能だがコスパが悪い
+失敗することがわかっているテスト(レッドテスト)はCIから外す
+失敗時の原因究明が難しい
+饒舌なエラーメッセージ
+状況のスナップショット Continuous Testing User Acceptance Test (UAT) くらいの規模になると個人開発・趣味開発では触れない領域なので、大いに勉強になった。スライドに添付されている資料が相当に充実していたので、これを読むのが本番といった様相すら感じる。
+高レベルテストの自動化は現在のプロジェクトでも感じており、自動化のチャンスは伺っている。とはいえセッションでも指摘されているように自動化することにコストがかかりすぎる領域があるのも事実で、そのバランスが難しい。 型解析を用いたリファクタリング 型のある世界で生きてきた身として大いに楽しみにしていた発表。 autoload も認識できる
+bootstrapFiles 編集箇所と利用箇所を CI でチェック
+ルールレベルを徐々に引き上げていく
+警告が多すぎると見落としてしまう・無視されやすくなる 型がついていないことによるエラーが多い 型よりも詳細な検査 SQL を静的解析
+placeholder の型付け 警告レベルを低いレベルから導入
+タイプヒントを積極的に書いていく
+PHPStan の拡張を追加する 昨今、動的型付き言語での型宣言・型アノテーション・型ヒントの導入が相次いでいる。長らく静的型付き言語を書いてきた私からすると、ようやく気づいたかといったところだが、ともかく型を導入する言語が増えてきた。
+今のプロジェクトでも新しく追加するコードには型をつけるよう努めているが、どうしても古いコードには型がついていない。個人的には型のないコードに対してどう型を自動的に付けるかという点に興味があり、その点で Ruby の typeprof には注目している。 昼食をとっていた。事前に何か食料を買っておくべきだった。 Documentation as Code この発表も以前から非常に楽しみにしていた。 開発開始までのオーバーヘッド
+新規にチームにジョイン
+担当範囲外の機能を理解
+オンボーディングのコスト PHPerKaigi 2020 で発表あり 継続的にシステムの理解を助けるドキュメント 継続的ドキュメンテーション
+システムとドキュメントの乖離 書いてあることが間違っている・足りない システムとドキュメントは対応関係がある システムとドキュメントの乖離を定量化する
+継続的に
+システムの更新に近いタイミングで ドキュメントを更新し続ける Documentation as Code コードと同じツールでドキュメントを書く 開発者
+システム
+ドキュメント
+構造化データ
+ソフトウェア システムから構造化データを抽出する
+PHPDoc
+OpenAPI ビュー 関心ごとに合わせてアーキテクチャを一つ以上の側面(断面)で説明する ビューの単位でドキュメントに スタックトレースからのドキュメント生成 ドキュメントの管理は現プロジェクトでも課題と感じている。作られた当初は正しくても、実態と乖離していくのを止めるのは困難を極める。全体的に興味深い発表だったが、特にスタックトレースからのドキュメント生成というアイデアに惹かれるものを感じた。スタックトレースという実態と不可分な (乖離しない) 情報を起点にするのは理にかなっている。問題はトレースをいつ、どう取るかだろうか。それを自動化しなければ、実態との乖離が避けられないだろう。 cookie による session 管理 全体的に基本的な話だったので特に触れない。Cookie やセッションの話としては非常に分かりやすくまとめられていたので、知らない人が学ぶにはいい教材だろう。 PHP のエラーと例外 エラー PHPエンジンがエラーを通知する
+例外 プログラムが投げる PHP7-8とエラー PHPエンジンのエラーの一部が \Error に変換されるようになった
+→ try-catch で捕捉できる \Error は例外とは異なる PHP8 でエラーレベルの引き上げ 例外 開発段階で例外を把握し、ハンドリングを考えておく \Throwable \Exception と \Error \Error はキャッチすべきでない \Error \LogicException \RuntimeException 捕捉して対応するのではなく、未然に防ぐ 独自例外を使う
+\Exception を投げてしまうと、
+catch (\Exception)せざるを得ない
+→catch 範囲が広すぎる SPL の例外を使う 例外翻訳
+上位のレイヤが下位のレイヤの例外を捕捉し、上位レイヤのAPIに「翻訳」する
+下位レイヤの知識に依存させない @throws
+捕捉してほしい例外を書き連ねておく 呼び出しもとに負わせたい責任 PHP を学んでいる途中の私としては、今まさに聞きたい発表だった (現時点で PHP を書き始めてから 4ヶ月ほどになる)。 個人的に例外やエラーを最もうまく扱っているのは Go、Swift、Rust、Haskell などのエラーを「値として」扱う言語だと思っている。try-catch は通常の処理フローを完全に壊してしまう上、構文としても重すぎる。値としてのエラー通知は C言語時代への回帰ともいえるが、その頃と異なるのはエラーを暗黙のうちに握り潰すことがないということだ。これらの言語は型を持っており、静的に検証ができる (C のそれはまともな型付けではない。念のため)。 PHP のように、すでに例外が言語システムに根ざしている言語ではどうすればよいか。この場合も同じく静的検証の力を借りることになるだろう。 Laravel のメール認証 Laravel の知識がない私にはまったくついていけなかった。また、個人的にタイトルがややミスリーディングに感じた。 gRPC Unary RPCs
+Server streaming RPCs
+Client streaming RPCs
+Bidirectional streaming RPCs Protobuf 実装とAPIが乖離しにくい
+自動生成
+複数言語でも相互に使える マイクロサービスのサービス通信
+スマホアプリ
+ゲームサーバ PHP では? PHP ではストリーミングが難しい
+リクエストごとにプロセスが使い捨て PHP ではgRPCのクライアントしか対応していない gRPC-Web
+ブラウザで扱うためのJSライブラリ+プロトコル HTTP/1.1 でも使える
+Unary RPC と Server streaming RPC のみ Envoy
+Nginx などで相互に gRPC と gRPC-Web で変換 Amp
+イベント駆動な並行処理のフレームワーク HTTP/2 対応 C#のgRPC-Webが楽 (発表の中でもまさに同じことをおっしゃっていたが) PHP 以外の方が向いているだろう、というのが第一の感想である。gRPC はそれ自体というよりも Protobuf というエコシステムに乗れることのメリットが大きいと感じる。そのエコシステムにうまく乗れない時点で、うーんという感じ。 アーキテクチャテスト Independent Core Layer Pattern 開発初期のアーキテクチャが崩れる
+アーキテクチャ観点のコードレビューができない どこにクラスを置けばよいか?
+ドキュメントがない アーキテクチャ設計に関する知識が属人化・暗黙知化 ガイドライン PHP の特性 アーキテクチャテスト
+クラスの依存関係や実装ルールをコードとして表現し、自動テスト化する Independent Core Layer Pattern アーキテクチャテストの失敗 モジュラーモノリス→マイクロサービスへ 冒頭に書いた通り、2日目から体調が悪くまともに聴けていない。途中までは頭痛を我慢しつつ見ていたのだが、まともに入ってこなかった。 残念ではあるが、いずれにせよ見られていない発表は他にもあるので、今週末にでもまとめて見ようと思う。 Day 2 にほとんど参加できなかったのは残念だが、イベント自体は大変楽しく、また興味深いものであった。自分がまったく知らない領域の話を聞けるのはこうしたイベントならではだと感じる。オンライン開催ゆえ現地に行く必要がなく、気軽に参加できたのも (特に初参加者として) 嬉しいポイントだった。 今回、雑談/登壇者への質問等向けに Discord サーバもあったのだが、こちらは参加こそしたものの ROM のままになってしまった。発表に1ウィンドウ、メモを書くのに1ウィンドウ、Discord 表示に 1ウィンドウで私にはもう脳のリソースとディスプレイのスペースが追いつかなかった (さらにいうと Zoom でアンカンファレンスもやっていたようだ。こちらはまったく参加していない)。 1つ個人的な反省点としては、一つ一つのセッションを真剣に聞き過ぎたというものがある。もっと適当に聞いておけばよかった。これだけだと大変語弊があるのだが、言い方を変えると、Discord しかりアンカンファレンスしかり「このイベントのこの瞬間にしかないコンテンツ」に触れずに、後から見返せる発表やスライドに注力してしまった、ということだ。発表の詳細な見直しはあとからできるのだから、今しかできないことを考えるべきだった。
+まあ初カンファレンスだし、とお茶を濁しておこう。 さて、カンファレンスで一つ気になったことがある。それは、Discord という書き込み場所が増えたことでニコ生のコメントの流量が吸い取られてしまったのではないか、という点だ。ニコニコだけ見ていると過疎っているかのように見えた発表も、Discord の方では盛り上がっている、というのを何度か見かけた。ニコニコのコメント方式は盛り上がりを如実に反映するが、逆もまたしかり。Discord があったこと自体はプラスだったと思うが、この点はマイナスだったのではないかと感じる。 最後になりましたが、毎年の PHPerKaigi 開催にご尽力されている皆様、スピーカーの皆様、楽しい3日間でした。ありがとうございました!
+(ずっと常体で書いてしまったのでいきなり仏頂面から笑顔になったようで気持ち悪い) ではまた来年。感想
+厳選おすすめトーク
+
+
+
+
+
+
+
+これらのエラー文を理解することで、すぐにエラーの原因に気付き適切に対象できる様になります!
+またそれらを理解した上でのエラーハンドリングを学びましょう。
+
+
+エラーを出さない事が一番ですが、完全に塞ぐ事は難しいと考えます。
+サービス運用の中で本番環境から発生するエラー(サーバー・クライアントサイド・サードパーティ起因のエラー)への監視体制と、
+エラー・バグ防御のためチームで行っているテストコード文化づくりの話をします。
+
+
+・「(私の思う)良い設計」を実現するための意思決定
+・「ISUCONの問題」という位置付けに由来する取捨選択
+・移植中に遭遇したトラブルとその解決策
+といった文脈や葛藤が存在しています。
+・PHPアプリケーションの設計、実装事例として役立ててもらう
+・ISUCONの言語移植に興味を持ってもらう
+・ISUCON問題移植の「実装や設計の練習をする教材」としての可能性を知ってもらう
+ことを目的とします。
+
+トークン問題の作成
+PHPer チャレンジ
+
+また、賞品として Echo Show 15 をいただいた。カンファレンス全体への感想
+
+
+
+まあ初カンファレンスだし、とお茶を濁しておこう。
+これにより、参加体験の質がはるかに向上した。特に Discord に関しては、登壇者ご本人による補足や、質問への回答などがおこなわれる (ことが多い) ため、特別な理由のない限り、発言はしないまでも参加はしておいたほうが良いと思われる。そして来年へ……?
+
+
+
+第1問 brainf_ck.php
+<?php
+
+declare(strict_types=0O1);
+
+namespace Dgcircus\PHPerKaigi\Y2022;
+
+/**
+ * @todo
+ * Run this program to acquire a PHPer token.
+ */
+
+https://creativecommons.org/publicdomain/zero/1.0/
+
+\error_reporting(~+!'We are hiring!');
+
+$z = fn($f) => (fn($x) => $f(fn(...$xs) => $x($x)(...$xs)))(fn($x) => $f(fn(...$xs) => $x($x)(...$xs)));
+$id = \spl_object_id(...);
+$put = fn($c) => \printf('%c', $c);
+$mm = fn($p, $n) => new \ArrayObject(\array_fill(+!![], $n, $p));
+
+$👉 = fn($m, $p, $b, $e, $mp, $pc) => [++$mp, ++$pc];
+$👈 = fn($m, $p, $b, $e, $mp, $pc) => [--$mp, ++$pc];
+$👍 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, ++$m[$mp]];
+$👎 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, --$m[$mp]];
+$📝 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, $put($m[$mp])];
+$🤡 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
+ +!![] => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
+ $b => $loop(++$pc, ++$n),
+ $e => $n === +!![] ? ++$pc : $loop(++$pc, --$n),
+ default => $loop(++$pc, $n),
+ })($pc, -![])],
+ default => [$mp, ++$pc],
+};
+$🎪 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
+ +!![] => [$mp, ++$pc],
+ default => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
+ $e => $loop(--$pc, ++$n),
+ $b => $n === +!![] ? $pc+![] : $loop(--$pc, --$n),
+ default => $loop(--$pc, $n),
+ })($pc, -![])],
+};
+$🐘 = fn($p) => $z(fn($loop) => fn($m, $p, $b, $e, $mp, $pc) =>
+ isset($p[$pc]) && $loop($m, $p, $b, $e, ...($p[$pc]($m, $p, $b, $e, $mp, $pc)))
+)($mm(+!![], +(![].![])), $p, $id($🤡), $id($🎪), +!![], +!![]);
+
+$🐘([
+ $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $🤡,
+ $👉, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $👈, $👈, $👈, $👈, $👎,
+ $🎪,
+ $👉, $👍, $👍, $👍, $👍, $👍, $📝,
+ $👎, $👎, $📝,
+ $👉, $👎, $👎, $👎, $📝,
+ $👉, $👎, $👎, $👎, $📝,
+ $👎, $👎, $📝,
+ $👎, $📝,
+ $👈, $📝,
+ $👉, $👉, $👎, $👎, $📝,
+ $👍, $👍, $👍, $👍, $👍, $👍, $👍, $📝,
+ $👈, $👎, $👎, $👎, $👎, $📝,
+ $👈, $📝,
+ $👉, $👍, $👍, $📝,
+ $👉, $👎, $📝,
+ $👈, $📝,
+]);
+解説
+絵文字
+プログラム全体
++ + + + + + + + + +
+[
+ > + + +
+ > + + + + +
+ > + + + + + + + + + + + +
+ > + + + + + + + + + +
+ < < < < -
+]
+> + + + + + .
+- - .
+> - - - .
+> - - - .
+- - .
+- .
+< .
+> > - - .
++ + + + + + + .
+< - - - - .
+< .
+> + + .
+> - .
+< .
+
+
+$👉: >$👈: <$👍: +$👎: -$📝: .$🤡: [$🎪: ], (入力) に対応する関数はない (このプログラムでは使わないので用意していない)。$🐘 はいわゆる main 関数であり、プログラムの実行部分である。絵文字の選択
+$🤡 と $🎪 は弊社デジタルサーカスにちなんでいる。
+また、$🐘 は PHP のマスコットの象に由来する。strict_types
+declare 文の strict_types に指定できるのは、0 か 1 の数値リテラルだが、
+0x0 や 0b1 のような値も受け付ける。
+今回は、PHP 8.1 から追加された、0O または 0o から始まる八進数リテラルを使った。URL
+https://creativecommons.org/publicdomain/zero/1.0/
+https: 部分はラベル、// 以降は行コメントになっている。リテラルなしで数値を生成する
+assert(0 === +!![]);
+assert(1 === +![]);
+assert(2 === ![]+![]);
+assert(3 === ![]+![]+![]);
+assert(10 === +(![].+!![]));
+[] に ! を適用すると true が返ってくる。それに +
+を適用すると、bool から int ヘの型変換が走り、1 が生成される。10
+はさらにトリッキーだ。まず 1 と 0 を作り、. で文字列として結合する
+('10')。これに + を適用すると、string から int
+への型変換が走り、10 が生まれる (コード量に頓着しないなら、1 を 10
+個足し合わせてももちろん 10 が作れる)。error_reporting に指定しているのは -1 である。
+これは、! によって文字列を false にし、+ によって false を 0 にし、さらにビット反転して -1 にしている。
+if 文なしで条件分岐match 式を使うことで、if を一切書かずに条件分岐ができる。
+また、&& / || も使えることがある。
+遅延評価が不要なケースでは、[$t, $f][$cond] のような形で分岐することもできる。
+while、for 文なしでループ$z)。$🤡 や $🎪、$🐘 は、一度 Scheme (Lisp の一種) で書いてから PHP に翻訳する形で記述した。第2問 riddle.php
+<?php
+
+/*********************************************************
+ * This program displays a PHPer token. *
+ * Guess 'N'. *
+ * *
+ * Hints: *
+ * - N itself has no special meaning, e.g., 42, 8128, *
+ * it is selected at random. *
+ * - Each element of $token represents a single letter. *
+ * - One letter consists of 5x5 cells. *
+ * - Remember, the output is a complete PHPer token. *
+ * *
+ * License: *
+ * https://creativecommons.org/publicdomain/zero/1.0/ *
+ *********************************************************/
+const N = 0 /* Change it to your answer. */;
+assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
+
+$token = [
+ 0x14B499C,
+ 0x0BE34CC, 0x01C9C69,
+ 0x0ECA069, 0x01C2449, 0x0FDB166, 0x01C9C69,
+ 0x01C1C66, 0x0FC1C47, 0x01C1C66,
+ 0x10C5858, 0x1E4E3B8, 0x1A2F2F8,
+];
+foreach ($token as $x) {
+ $x = $x ^ N;
+
+ $x = sprintf('%025b', $x);
+ $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+ $x = implode("\n", str_split($x, length: 5));
+ echo "{$x}\n\n";
+}
+N を特定する必要がある。読解
+$token = [
+ // 略
+];
+$token があり、各要素をループしている。 $x = $x ^ N;
+ $x = sprintf('%025b', $x);
+ $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+# にし、 $x = implode("\n", str_split($x, length: 5));
+ヒント
+
+
+N それ自体は、42 や 8128 といったような特別な意味を持たず、ランダムに決められている$token の各要素は、1文字を表す# 記号から始まることを思いだすと、
+$token の最初の数字 0x14B499C は、変換の結果 # になるのではないかと予想される (なお、このことは、リポジトリの README ファイルに追加ヒントとして書かれている)。解く
+0x14B499C が # に変換されるような N を見つければよい。N は高々assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
+<?php
+
+$x = 0x14B499C;
+
+$x = $x ^ N;
+
+$x = sprintf('%025b', $x);
+$x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+$x = implode("\n", str_split($x, length: 5));
+
+assert($x ===
+ " # # \n" .
+ "#####\n" .
+ " # # \n" .
+ "#####\n" .
+ " # # ");
+<?php
+
+$x =
+ " # # \n" .
+ "#####\n" .
+ " # # \n" .
+ "#####\n" .
+ " # # ";
+
+$x = implode('', explode("\n", $x));
+$x = str_replace(search: [' ', '#'], replace: ['0', '1'], subject: $x);
+$x = bindec($x);
+
+$n = $x ^ 0x14B499C;
+
+echo "N = $n\n";
+N が得られる。第3問 toquine.php
+<?php
+
+// License: https://creativecommons.org/publicdomain/zero/1.0/
+// This is a quine-like program to generate a PHPer token.
+// Execute it like this: php toquine.php | php | php | php | ...
+
+$s = <<<'Q'
+<?cuc
+// Yvprafr: uggcf://perngvirpbzzbaf.bet/choyvpqbznva/mreb/1.0/
+// Guvf vf n dhvar-yvxr cebtenz gb trarengr n CUCre gbxra.
+// Rkrphgr vg yvxr guvf: cuc gbdhvar.cuc | cuc | cuc | cuc | ...
+%f$f = %f;
+$f = fge_ebg13($f); $kf = [
+%f,
+];
+$g = ahyy.snyfr; sbe ($v = 0; $v <= vagqvi(__YVAR__-035,6); ++$v) vs (!vffrg($kf[$v])) oernx; ryfr
+$g .= vzcybqr("\a", fge_fcyvg(fge_ercynpr(['0','1'], [' ','##'], fcevags(pue(37) . '025o', $kf[$v])), 012)) . "\a\a";
+$jf = neenl_znc(sa($j) => vzcybqr(', ', $j), neenl_puhax(neenl_znc(sa($k) => fcevags('0k' . pue(37) . '07K', $k), $kf), 10));
+cevags($f, $g, fge_ebg13("<<<'Q'\a{$f}\aQ"), vzcybqr(",\a", $jf));
+Q;
+$s = str_rot13($s); $xs = [
+0x0AFABEA, 0x1F294A7, 0x1F2109F, 0x1F294A7, 0x0002800, 0x1F2109F, 0x0117041, 0x1F294A7, 0x1FAD6B5, 0x1F295B7,
+0x010FC21, 0x1FAD6B5, 0x1151151, 0x010FC21, 0x1F294A7, 0x1F295B7, 0x1FAD6B5, 0x1F294A7, 0x1F295B7, 0x1F8C63F,
+0x1F8C631, 0x1FAD6B5, 0x17AD6BD, 0x17AD6BD, 0x1F8C63F, 0x1F295B7,
+];
+$t = null.false; for ($i = 0; $i <= intdiv(__LINE__-035,6); ++$i) if (!isset($xs[$i])) break; else
+$t .= implode("\n", str_split(str_replace(['0','1'], [' ','##'], sprintf(chr(37) . '025b', $xs[$i])), 012)) . "\n\n";
+$ws = array_map(fn($w) => implode(', ', $w), array_chunk(array_map(fn($x) => sprintf('0x' . chr(37) . '07X', $x), $xs), 10));
+printf($s, $t, str_rot13("<<<'D'\n{$s}\nD"), implode(",\n", $ws));
+$ php toquine.php | php | php | php | ...
+解説
+プログラム全体
+トークン
+$xs がトークンに対応している。変換のロジックは riddle.php とほぼ同じなので省略する。状態保持
+__LINE__ から情報を取得している。ROT 13
+toquine.php では、ROT 13 変換を使って難読化した。おわりに
+凡例
+
+
+Day 0 前夜祭 (2021/03/27)
+17:30 [A]
+
+
+
+
+18:10 [A]
+
+
+
+
+/tokyo?area=HOGE → /tokyo/HOGE18:50 [A]
+
+
+
+
+
+
+
+
+
+
+
+
+19:30 [A]
+
+
+
+
+
+
+
+
+
+
+Day 1 (2021/03/27)
+10:50 [A]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:50 [A]
+
+
+
+
+Util_Assert::min12:30 [A]
+13:10 [A]
+
+
+
+
+
+
+
+
+14:10 [A]
+14:50 [A]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+15:30 [A]
+16:10 [A]
+
+
+16:50 [A]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Day 2 (2021/03/28)
+全体の感想
+
+
タイトル落ち。まずはこのコードを見て欲しい。
+#include <iostream>
+
+[[alignas]] [[alignof]] [[and]] [[and_eq]] [[asm]] [[auto]] [[bitand]]
+[[bitor]] [[bool]] [[break]] [[case]] [[catch]] [[char]] [[char16_t]]
+[[char32_t]] [[class]] [[compl]] [[const]] [[const_cast]] [[constexpr]]
+[[continue]] [[decltype]] [[default]] [[delete]] [[do]] [[double]]
+[[dynamic_cast]] [[else]] [[enum]] [[explicit]] [[export]] [[extern]] [[false]]
+[[final]] [[float]] [[for]] [[friend]] [[goto]] [[if]] [[inline]] [[int]]
+[[long]] [[mutable]] [[namespace]] [[new]] [[noexcept]] [[not]] [[not_eq]]
+[[nullptr]] [[operator]] [[or]] [[or_eq]] [[override]] [[private]]
+[[protected]] [[public]] [[register]] [[reinterpret_cast]] [[return]] [[short]]
+[[signed]] [[sizeof]] [[static]] [[static_assert]] [[static_cast]] [[struct]]
+[[switch]] [[template]] [[this]] [[thread_local]] [[throw]] [[true]] [[try]]
+[[typedef]] [[typeid]] [[typename]] [[union]] [[unsigned]]
+[[virtual]] [[void]] [[volatile]] [[wchar_t]] [[while]] [[xor]] [[xor_eq]]
+// [[using]]
+int main() {
+ std::cout << "Hello, World!" << std::endl;
+}
+++コンパイラのバージョン +$ clang++ –version +Apple clang version 11.0.0 (clang-1100.0.33.8) +Target: x86_64-apple-darwin19.6.0 +Thread model: posix +InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
+コンパイルコマンド (C++17指定) +$ clang++ –std=c++17 hoge.cpp
+
この記事から得られるものはこれ以上ないので以下は蛇足になる。
+別件で cppreference.com の identifier のページ を読んでいた時、次の文が目に止まった。
++++
+- the identifiers that are keywords cannot be used for other purposes; +
++
+- The only place they can be used as non-keywords is in an attribute-token. (e.g. [[private]] is a valid attribute) (since C++11)
+
キーワードでも属性として指定する場合は非キーワードとして使えるらしい。 +実際にやってみる。
+同サイトの [keywords のページ] (https://en.cppreference.com/w/cpp/keyword) から一覧を拝借し、上のコードが出来上がった (C++17 においてキーワードでないものなど、一部省いている)。 +大量の警告 (unknown attribute ‘〇〇’ ignored) がコンパイラから出力されるが、コンパイルできる。
+上のコードでは [[using]] をコメントアウトしているが、これは using キーワードのみ属性構文の中で意味を持つからであり、このコメントアウトを外すとコンパイルに失敗する。
// using の例
+[[using foo: attr1, attr2]] int x; // [[foo::attr1, foo::attr2]] の糖衣構文
+C++17 の仕様も見てみる (正確には標準化前のドラフト)。
+引用元: https://timsong-cpp.github.io/cppwp/n4659/dcl.attr#grammar-4
+++If a keyword or an alternative token that satisfies the syntactic requirements of an identifier is contained in an attribute-token, it is considered an identifier.
+
「identifier の構文上の要件を満たすキーワードまたは代替トークンが attribute-token に含まれている場合、identifier とみなされる」とある。どうやら間違いないようだ。
ところで、代替トークン (alternative token) とは and (&) や bitor (|) などのことだが、identifier の構文上の要件を満たさないような代替トークンなどあるのか?
+疑問に思って調べたところ、代替トークンという語にはダイグラフも含まれるらしい (参考: 同ドラフト)
<% → {%> → }<: → [:> → ]%: → #%:%: → ##「identifier の構文上の要件を満たさないような代替トークン」はこれらが当てはまると思われる。
調べた感想: 字句解析器か構文解析器が辛そう
+]]> diff --git a/docs/tags/cpp17/feed.xml b/docs/tags/cpp17/feed.xml index be4782a..b1e9aa9 100644 --- a/docs/tags/cpp17/feed.xml +++ b/docs/tags/cpp17/feed.xml @@ -13,7 +13,76 @@タイトル落ち。まずはこのコードを見て欲しい。
+#include <iostream>
+
+[[alignas]] [[alignof]] [[and]] [[and_eq]] [[asm]] [[auto]] [[bitand]]
+[[bitor]] [[bool]] [[break]] [[case]] [[catch]] [[char]] [[char16_t]]
+[[char32_t]] [[class]] [[compl]] [[const]] [[const_cast]] [[constexpr]]
+[[continue]] [[decltype]] [[default]] [[delete]] [[do]] [[double]]
+[[dynamic_cast]] [[else]] [[enum]] [[explicit]] [[export]] [[extern]] [[false]]
+[[final]] [[float]] [[for]] [[friend]] [[goto]] [[if]] [[inline]] [[int]]
+[[long]] [[mutable]] [[namespace]] [[new]] [[noexcept]] [[not]] [[not_eq]]
+[[nullptr]] [[operator]] [[or]] [[or_eq]] [[override]] [[private]]
+[[protected]] [[public]] [[register]] [[reinterpret_cast]] [[return]] [[short]]
+[[signed]] [[sizeof]] [[static]] [[static_assert]] [[static_cast]] [[struct]]
+[[switch]] [[template]] [[this]] [[thread_local]] [[throw]] [[true]] [[try]]
+[[typedef]] [[typeid]] [[typename]] [[union]] [[unsigned]]
+[[virtual]] [[void]] [[volatile]] [[wchar_t]] [[while]] [[xor]] [[xor_eq]]
+// [[using]]
+int main() {
+ std::cout << "Hello, World!" << std::endl;
+}
+++コンパイラのバージョン +$ clang++ –version +Apple clang version 11.0.0 (clang-1100.0.33.8) +Target: x86_64-apple-darwin19.6.0 +Thread model: posix +InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
+コンパイルコマンド (C++17指定) +$ clang++ –std=c++17 hoge.cpp
+
この記事から得られるものはこれ以上ないので以下は蛇足になる。
+別件で cppreference.com の identifier のページ を読んでいた時、次の文が目に止まった。
++++
+- the identifiers that are keywords cannot be used for other purposes; +
++
+- The only place they can be used as non-keywords is in an attribute-token. (e.g. [[private]] is a valid attribute) (since C++11)
+
キーワードでも属性として指定する場合は非キーワードとして使えるらしい。 +実際にやってみる。
+同サイトの [keywords のページ] (https://en.cppreference.com/w/cpp/keyword) から一覧を拝借し、上のコードが出来上がった (C++17 においてキーワードでないものなど、一部省いている)。 +大量の警告 (unknown attribute ‘〇〇’ ignored) がコンパイラから出力されるが、コンパイルできる。
+上のコードでは [[using]] をコメントアウトしているが、これは using キーワードのみ属性構文の中で意味を持つからであり、このコメントアウトを外すとコンパイルに失敗する。
// using の例
+[[using foo: attr1, attr2]] int x; // [[foo::attr1, foo::attr2]] の糖衣構文
+C++17 の仕様も見てみる (正確には標準化前のドラフト)。
+引用元: https://timsong-cpp.github.io/cppwp/n4659/dcl.attr#grammar-4
+++If a keyword or an alternative token that satisfies the syntactic requirements of an identifier is contained in an attribute-token, it is considered an identifier.
+
「identifier の構文上の要件を満たすキーワードまたは代替トークンが attribute-token に含まれている場合、identifier とみなされる」とある。どうやら間違いないようだ。
ところで、代替トークン (alternative token) とは and (&) や bitor (|) などのことだが、identifier の構文上の要件を満たさないような代替トークンなどあるのか?
+疑問に思って調べたところ、代替トークンという語にはダイグラフも含まれるらしい (参考: 同ドラフト)
<% → {%> → }<: → [:> → ]%: → #%:%: → ##「identifier の構文上の要件を満たさないような代替トークン」はこれらが当てはまると思われる。
調べた感想: 字句解析器か構文解析器が辛そう
+]]>こんなものを作った。
+$ term-banner 'Hello, World!' 'こんにちは、' '世界!'
+
コマンドライン引数として渡した文字列をターミナルに大きく表示する。
+リポジトリはこちら: https://github.com/nsfisis/term-banner
+以前、big-clock-mode という似たようなプログラムを書いた。
+これは tmux の :clock-mode コマンドに着想を得たもので、:clock-mode よりも大きく現在時刻を表示する。
big-clock-mode を開発したのは、次のようなシチュエーションで使うためである。
+弊社では現在リモートワークが基本だが、web 会議などで画面共有しているときに、休憩を挟んで特定の時刻から再開する、ということがある。
+こういったケースで、画面上に現在の時刻を大きめに表示しておくと、モニタから離れても遠くから時刻がわかるので便利である。
それこそタイマアプリか何かを使えばいいのだが、ターミナルに棲むいきものとしては、住処から離れたくないわけだ。
+しばらく便利に使っていたのだが、ひとつ不満点が出てきた。それは、再開する時刻がいつだったかを覚えておかなければならないということだ。 +どこかにメモしておいてもいいが、せっかくなら現在時刻とともに表示させておきたい。
+そんなわけで、「任意の文字列をターミナルに表示する」プログラムを書く運びとなった。 +まあ、作らなくても探せばあると思うが、作りたいものは作りたいので知ったことではない。
+全体の流れは次のようになっている。
+big-clock-mode が Go 製なので、今回も Go で書いた。
+PNG が標準ライブラリにあったり、Shift-JIS のエンコーディングが準標準ライブラリにあったりしたのは助かった。
フォントファイルは go:embed で実行ファイルに埋め込んでいるので、ビルド後はワンバイナリで動く。
+仕事ではスクリプト言語ばかり書いているが、やはりコンパイル言語はいい。
フリーの 8x8 ビットマップフォントである、美咲フォント 2021-05-05a 版 を使わせていただいた。
+はじめは自分でポチポチ打っていたのだが、「き」くらいまでやって挫折した。 +同じく 8x8 で作っていたのだが、平仮名でさえも、この小さなキャンバスにはとても収められない。
+美咲フォントは、平仮名・片仮名に留まらず、JIS 第一・第二水準の漢字までサポートしている。 +第二水準ともなると一生お目にかかることのない字の方が多いくらいだが、これをこの大きさで書くというのは、もはや芸術の域である。
+さらに言うと、実のところ美咲フォントは実サイズ 7x7 で作られており、余白が設けられている。 +これは、単純にそのまま並べても字間・行間を確保できるようにという配慮である。 +おかげでコーディングまで楽になった。
+ゴシック体と明朝体があったが、私の好みで明朝体の方にした。 +ただ、ゴシック体の方が見やすい気がするので、フォントを選べるように後ほど拡張するかもしれない。
+2022-04-27 追記: -f オプションで選べるようにした。
あなたもターミナルに住んでみませんか?
+]]>2022-04-09 から 2022-04-11 にかけて開催された、PHPerKaigi 2022 に、一般参加者として参加した。 +弊社デジタルサーカス株式会社 はダイヤモンドスポンサーとなっており、スポンサー枠のチケットを使わせていただいた。
+昨年のレポートはこちら。
+多くの素晴らしいトークの中から、特におすすめのものを 5つ選んだ。是非聞いてほしい。引用部分は、リンク先プロポーザルから引用している。
+予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント
+++ +PHP はバージョンを追う毎に型宣言、例外、表明、列挙型などの機能が大幅に強化され、堅牢なコードを書くための機能が充実してきました。それらの機能はどう使うと効果的なのでしょうか。
+本講演では PHP 8.1 をベースにして、誤りを想定してチェックするのではなく、そもそも誤りにくい設計とはどのようなものか、つまり「予防」の観点を軸足に、堅牢なコードを導くための様々な設計のヒントをご紹介します。
+
++ +PHPを使ってるとよく遭遇する Fatal error / Parse error / Warning / Notice 理解していますか?
+
+これらのエラー文を理解することで、すぐにエラーの原因に気付き適切に対象できる様になります!
+またそれらを理解した上でのエラーハンドリングを学びましょう。
++毎日流れてくるエラーに皆さんはどう向き合ってますか?
+
+エラーを出さない事が一番ですが、完全に塞ぐ事は難しいと考えます。
+サービス運用の中で本番環境から発生するエラー(サーバー・クライアントサイド・サードパーティ起因のエラー)への監視体制と、
+エラー・バグ防御のためチームで行っているテストコード文化づくりの話をします。
ISUCON11のPHP実装は、何を考え、どのようにして作られていたのか
+++昨年開催されたISUCON11にて問題(参考実装)のPHPへの移植を担当させていただきました。
+最終的なソースコードこそシンプルなWebアプリケーションではありますが、その裏には
+
+・「(私の思う)良い設計」を実現するための意思決定
+・「ISUCONの問題」という位置付けに由来する取捨選択
+・移植中に遭遇したトラブルとその解決策
+といった文脈や葛藤が存在しています。本発表はそれらを共有することで
+
+・PHPアプリケーションの設計、実装事例として役立ててもらう
+・ISUCONの言語移植に興味を持ってもらう
+・ISUCON問題移植の「実装や設計の練習をする教材」としての可能性を知ってもらう
+ことを目的とします。
チームの仕事はまわっていたけど、メンバーはそれぞれモヤモヤを抱えていた話──40名の大規模開発チームで1on1ログを公開してみた
+++サイボウズの大企業向けグループウェアのGaroon(ガルーン)は、PHPで開発されている20年目の製品です。ガルーン開発チームは日本で40名、ベトナムで50名の計90名ほどのチームになっています。また、コロナ禍でフルリモートでの活動がこの2年ほど継続してきました。
+フルリモートになっても仕事はまわっており、継続的にリリースはしていましたが、一方でお互いの考えていることや感じている問題意識が見えづらくなり、モヤモヤを抱えているメンバーが増えていました。
+このセッションでは、そういう状況で私がチーム外からジョインし、聴き役に徹しながら見える化することで状況を改善していった取り組みを紹介します。同じように大きなチームやリモートワークで難しさを感じている人に、難しさの原因への気づきや取り組みへのヒントがあれば幸いです。
+
今回は、PHPer チャレンジ用に弊社のトークン問題を 3題作成した。こちらについては別途記事にしているので、そちらを参照されたい。
+1位になった。
+また、賞品として Echo Show 15 をいただいた。
去年の参加レポ では、こんなことを書いた。
+++1つ個人的な反省点としては、(中略) Discord しかりアンカンファレンスしかり「このイベントのこの瞬間にしかないコンテンツ」に触れずに、後から見返せる発表やスライドに注力してしまった、ということだ。発表の詳細な見直しはあとからできるのだから、今しかできないことを考えるべきだった。
+
+まあ初カンファレンスだし、とお茶を濁しておこう。
この反省を踏まえ、今年は積極的にほかの場 (公式の Discord サーバや、アンカンファレンス) にも参加した。
+これにより、参加体験の質がはるかに向上した。特に Discord に関しては、登壇者ご本人による補足や、質問への回答などがおこなわれる (ことが多い) ため、特別な理由のない限り、発言はしないまでも参加はしておいたほうが良いと思われる。
なお、アンカンファレンスについては、1日目の終わりにトークン問題の解説放送もおこなった。
+また、今年はオフラインとオンラインのハイブリッド開催であったが、去年の全オンラインと比べて、オンライン参加の体験が落ちていなかったのは、特筆すべきであろう。 +今年は 3回目のワクチン接種が間に合わなかったこともあり現地参加は見送ったのだが、来年は是非オフラインで参加したい。
+PHPerKaigi 2023 があるかどうか存じ上げないが、あるとすれば、次の 4つを目標としたい。
+最後になりましたが、PHPerKaigi のスタッフ、スポンサー、スピーカーのみなさん、素敵な時間をありがとうございました。
+ではまた来年。
+]]>本日開始された PHPerKaigi 2022 の PHPer チャレンジにおいて、弊社 デジタルサーカス株式会社 の問題を 3問作成した。この記事では、これらの問題の解説をおこなう。
+リポジトリはこちら: https://github.com/nsfisis/PHPerKaigi2022-tokens
+ソースコードはこちら。実行には PHP 8.1 以上が必要なので注意。
+<?php
+
+declare(strict_types=0O1);
+
+namespace Dgcircus\PHPerKaigi\Y2022;
+
+/**
+ * @todo
+ * Run this program to acquire a PHPer token.
+ */
+
+https://creativecommons.org/publicdomain/zero/1.0/
+
+\error_reporting(~+!'We are hiring!');
+
+$z = fn($f) => (fn($x) => $f(fn(...$xs) => $x($x)(...$xs)))(fn($x) => $f(fn(...$xs) => $x($x)(...$xs)));
+$id = \spl_object_id(...);
+$put = fn($c) => \printf('%c', $c);
+$mm = fn($p, $n) => new \ArrayObject(\array_fill(+!![], $n, $p));
+
+$👉 = fn($m, $p, $b, $e, $mp, $pc) => [++$mp, ++$pc];
+$👈 = fn($m, $p, $b, $e, $mp, $pc) => [--$mp, ++$pc];
+$👍 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, ++$m[$mp]];
+$👎 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, --$m[$mp]];
+$📝 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, $put($m[$mp])];
+$🤡 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
+ +!![] => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
+ $b => $loop(++$pc, ++$n),
+ $e => $n === +!![] ? ++$pc : $loop(++$pc, --$n),
+ default => $loop(++$pc, $n),
+ })($pc, -![])],
+ default => [$mp, ++$pc],
+};
+$🎪 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
+ +!![] => [$mp, ++$pc],
+ default => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
+ $e => $loop(--$pc, ++$n),
+ $b => $n === +!![] ? $pc+![] : $loop(--$pc, --$n),
+ default => $loop(--$pc, $n),
+ })($pc, -![])],
+};
+$🐘 = fn($p) => $z(fn($loop) => fn($m, $p, $b, $e, $mp, $pc) =>
+ isset($p[$pc]) && $loop($m, $p, $b, $e, ...($p[$pc]($m, $p, $b, $e, $mp, $pc)))
+)($mm(+!![], +(![].![])), $p, $id($🤡), $id($🎪), +!![], +!![]);
+
+$🐘([
+ $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $🤡,
+ $👉, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $👈, $👈, $👈, $👈, $👎,
+ $🎪,
+ $👉, $👍, $👍, $👍, $👍, $👍, $📝,
+ $👎, $👎, $📝,
+ $👉, $👎, $👎, $👎, $📝,
+ $👉, $👎, $👎, $👎, $📝,
+ $👎, $👎, $📝,
+ $👎, $📝,
+ $👈, $📝,
+ $👉, $👉, $👎, $👎, $📝,
+ $👍, $👍, $👍, $👍, $👍, $👍, $👍, $📝,
+ $👈, $👎, $👎, $👎, $👎, $📝,
+ $👈, $📝,
+ $👉, $👍, $👍, $📝,
+ $👉, $👎, $📝,
+ $👈, $📝,
+]);
+この問題は、単に適切なバージョンの PHP で動かせばトークンが得られる。
+まず目につくのは大量の絵文字だろう。 +PHP は識別子に使用できる文字の範囲が広く、絵文字も使うことができる。
+Brainf*ck のインタプリタとプログラムになっている。 +Brainf*ck とは、難解プログラミング言語のひとつであり、ここで説明するよりも Wikipedia の該当ページを読んだ方がよい。
+https://ja.wikipedia.org/wiki/Brainfuck
+なお、brainf*ck プログラムを普通の書き方で書くと、次のようになる。
++ + + + + + + + + +
+[
+ > + + +
+ > + + + + +
+ > + + + + + + + + + + + +
+ > + + + + + + + + + +
+ < < < < -
+]
+> + + + + + .
+- - .
+> - - - .
+> - - - .
+- - .
+- .
+< .
+> > - - .
++ + + + + + + .
+< - - - - .
+< .
+> + + .
+> - .
+< .
+実行結果はこちら: https://ideone.com/22VWmb
+それぞれの絵文字で表された関数が、各命令に対応している。
+$👉: >$👈: <$👍: +$👎: -$📝: .$🤡: [$🎪: ], (入力) に対応する関数はない (このプログラムでは使わないので用意していない)。
なお、$🐘 はいわゆる main 関数であり、プログラムの実行部分である。
おおよそ意味に合致するよう選んでいるが、$🤡 と $🎪 は弊社デジタルサーカスにちなんでいる。
+また、$🐘 は PHP のマスコットの象に由来する。
declare 文の strict_types に指定できるのは、0 か 1 の数値リテラルだが、
+0x0 や 0b1 のような値も受け付ける。
+今回は、PHP 8.1 から追加された、0O または 0o から始まる八進数リテラルを使った。
ソースコードのライセンスを示したこの部分だが、
+https://creativecommons.org/publicdomain/zero/1.0/
+完全に合法な PHP のコードである。
+https: 部分はラベル、// 以降は行コメントになっている。
ソースコード中に、ほとんど数値リテラルが書かれていないことにお気づきだろうか。 +PHP では、型変換を利用することで任意の整数を作り出すことができる。
+assert(0 === +!![]);
+assert(1 === +![]);
+assert(2 === ![]+![]);
+assert(3 === ![]+![]+![]);
+assert(10 === +(![].+!![]));
+[] に ! を適用すると true が返ってくる。それに +
+を適用すると、bool から int ヘの型変換が走り、1 が生成される。10
+はさらにトリッキーだ。まず 1 と 0 を作り、. で文字列として結合する
+('10')。これに + を適用すると、string から int
+への型変換が走り、10 が生まれる (コード量に頓着しないなら、1 を 10
+個足し合わせてももちろん 10 が作れる)。
また、error_reporting に指定しているのは -1 である。
+これは、! によって文字列を false にし、+ によって false を 0 にし、さらにビット反転して -1 にしている。
if 文なしで条件分岐三項演算子ないし match 式を使うことで、if を一切書かずに条件分岐ができる。
+また、&& / || も使えることがある。
+遅延評価が不要なケースでは、[$t, $f][$cond] のような形で分岐することもできる。
while、for 文なしでループ不動点コンビネータを使って無名再帰する (詳しい説明は省略する。これらの単語で検索してほしい)。
+ここでは、一般に Z コンビネータとして知られるものを使った ($z)。
実際のところ、$🤡 や $🎪、$🐘 は、一度 Scheme (Lisp の一種) で書いてから PHP に翻訳する形で記述した。
なお、PHP は末尾再帰の最適化をおこなわない (少なくとも今のところは) ので、 +あまりに長い brainf*ck プログラムを書くとスタックオーバーフローする。
+ソースコードはこちら。実行には PHP 8.0 以上が必要なので注意。
+<?php
+
+/*********************************************************
+ * This program displays a PHPer token. *
+ * Guess 'N'. *
+ * *
+ * Hints: *
+ * - N itself has no special meaning, e.g., 42, 8128, *
+ * it is selected at random. *
+ * - Each element of $token represents a single letter. *
+ * - One letter consists of 5x5 cells. *
+ * - Remember, the output is a complete PHPer token. *
+ * *
+ * License: *
+ * https://creativecommons.org/publicdomain/zero/1.0/ *
+ *********************************************************/
+const N = 0 /* Change it to your answer. */;
+assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
+
+$token = [
+ 0x14B499C,
+ 0x0BE34CC, 0x01C9C69,
+ 0x0ECA069, 0x01C2449, 0x0FDB166, 0x01C9C69,
+ 0x01C1C66, 0x0FC1C47, 0x01C1C66,
+ 0x10C5858, 0x1E4E3B8, 0x1A2F2F8,
+];
+foreach ($token as $x) {
+ $x = $x ^ N;
+
+ $x = sprintf('%025b', $x);
+ $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+ $x = implode("\n", str_split($x, length: 5));
+ echo "{$x}\n\n";
+}
+さて、この問題はさきほどのように単純に実行しただけでは、謎のブロックが表示されるだけでトークンは得られない。
+トークンを得るためには、ソースコードを読み、定数 N を特定する必要がある。
ここでは、私の想定解を解説する。
+まずはソースコードを読んでいく。
+$token = [
+ // 略
+];
+数値からなる $token があり、各要素をループしている。
$x = $x ^ N;
+まずは排他的論理和 (xor) を取り、
+ $x = sprintf('%025b', $x);
+二進数に変換して、
+ $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+0 を空白に、1 を # にし、
$x = implode("\n", str_split($x, length: 5));
+5文字ごとに区切ったあと、改行で結合している。
+次に、ソースコードに書いてあるヒントを読んでいく。
+N それ自体は、42 や 8128 といったような特別な意味を持たず、ランダムに決められている$token の各要素は、1文字を表すここで、PHPer トークンは必ず # 記号から始まることを思いだすと、
+$token の最初の数字 0x14B499C は、変換の結果 # になるのではないかと予想される (なお、このことは、リポジトリの README ファイルに追加ヒントとして書かれている)。
ここまでわかれば、あと一歩で解ける。すなわち、0x14B499C が # に変換されるような N を見つければよい。
N は高々
assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
+なのでブルートフォースしてもよいが、ここではブルートフォースしない方法を紹介する。
+<?php
+
+$x = 0x14B499C;
+
+$x = $x ^ N;
+
+$x = sprintf('%025b', $x);
+$x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+$x = implode("\n", str_split($x, length: 5));
+
+assert($x ===
+ " # # \n" .
+ "#####\n" .
+ " # # \n" .
+ "#####\n" .
+ " # # ");
+この一連の変換に対する逆変換を考えると、次のようになる。
+<?php
+
+$x =
+ " # # \n" .
+ "#####\n" .
+ " # # \n" .
+ "#####\n" .
+ " # # ";
+
+$x = implode('', explode("\n", $x));
+$x = str_replace(search: [' ', '#'], replace: ['0', '1'], subject: $x);
+$x = bindec($x);
+
+$n = $x ^ 0x14B499C;
+
+echo "N = $n\n";
+これを実行すると、N が得られる。
ソースコードはこちら。
+<?php
+
+// License: https://creativecommons.org/publicdomain/zero/1.0/
+// This is a quine-like program to generate a PHPer token.
+// Execute it like this: php toquine.php | php | php | php | ...
+
+$s = <<<'Q'
+<?cuc
+// Yvprafr: uggcf://perngvirpbzzbaf.bet/choyvpqbznva/mreb/1.0/
+// Guvf vf n dhvar-yvxr cebtenz gb trarengr n CUCre gbxra.
+// Rkrphgr vg yvxr guvf: cuc gbdhvar.cuc | cuc | cuc | cuc | ...
+%f$f = %f;
+$f = fge_ebg13($f); $kf = [
+%f,
+];
+$g = ahyy.snyfr; sbe ($v = 0; $v <= vagqvi(__YVAR__-035,6); ++$v) vs (!vffrg($kf[$v])) oernx; ryfr
+$g .= vzcybqr("\a", fge_fcyvg(fge_ercynpr(['0','1'], [' ','##'], fcevags(pue(37) . '025o', $kf[$v])), 012)) . "\a\a";
+$jf = neenl_znc(sa($j) => vzcybqr(', ', $j), neenl_puhax(neenl_znc(sa($k) => fcevags('0k' . pue(37) . '07K', $k), $kf), 10));
+cevags($f, $g, fge_ebg13("<<<'Q'\a{$f}\aQ"), vzcybqr(",\a", $jf));
+Q;
+$s = str_rot13($s); $xs = [
+0x0AFABEA, 0x1F294A7, 0x1F2109F, 0x1F294A7, 0x0002800, 0x1F2109F, 0x0117041, 0x1F294A7, 0x1FAD6B5, 0x1F295B7,
+0x010FC21, 0x1FAD6B5, 0x1151151, 0x010FC21, 0x1F294A7, 0x1F295B7, 0x1FAD6B5, 0x1F294A7, 0x1F295B7, 0x1F8C63F,
+0x1F8C631, 0x1FAD6B5, 0x17AD6BD, 0x17AD6BD, 0x1F8C63F, 0x1F295B7,
+];
+$t = null.false; for ($i = 0; $i <= intdiv(__LINE__-035,6); ++$i) if (!isset($xs[$i])) break; else
+$t .= implode("\n", str_split(str_replace(['0','1'], [' ','##'], sprintf(chr(37) . '025b', $xs[$i])), 012)) . "\n\n";
+$ws = array_map(fn($w) => implode(', ', $w), array_chunk(array_map(fn($x) => sprintf('0x' . chr(37) . '07X', $x), $xs), 10));
+printf($s, $t, str_rot13("<<<'D'\n{$s}\nD"), implode(",\n", $ws));
+コメントにもあるとおり、次のようにして実行すれば答えがでてくる。
+$ php toquine.php | php | php | php | ...
+実際にはもう少しパイプで繋げなければならない。
+コメントにもあるとおり、これは quine (風) のプログラムになっている。 +Quine とは、自分のソースコードをそっくりそのまま出力するようなプログラムのことである。
+このプログラムは、実行すると自身とほとんど同じプログラムを出力する。 +異なるのはトークンになっている部分のみである。
+$xs がトークンに対応している。変換のロジックは riddle.php とほぼ同じなので省略する。
トークンの何文字目まで出力したかを、ソースコードを変えずに (quine なので) 覚えておく必要がある。
+このプログラムでは、トークンが出力されるとソースコードがだんだんと長くなっていくのを利用して、__LINE__ から情報を取得している。
Quine は、素朴に書くとプログラムの一部が 2回記述されてしまう。
+これがあまり美しくないので、toquine.php では、ROT 13 変換を使って難読化した。
それにしてもなぜこんなものが標準ライブラリに……。
+解いていただいたみなさん、また、難易度調整につきあっていただいた社内のみなさん、ありがとうございました。
+今回は直前に作りはじめたのもあり、3問だけかつ使い古されたネタばかりになってしまいましたが、 +来年は 5問、より面白い問題を持っていきます。
+実はもう作りはじめているので、どうか来年もありますように……。
+]]>2021-03-26 から 2021-03-28 にかけて開催された、PHPerKaigi 2021 に一般参加者として参加した。 +弊社デジタルサーカス株式会社 (今年1月から勤務) はダイヤモンドスポンサーとなっており、スポンサー枠のチケットを使わせていただいた。
+このようなカンファレンスには初めて参加するのでかねてより心待ちにしていたのだが、生憎2日目から体調を崩してしまい、この記事も途中までとなっている。まだ見ていないセッションも多いが、ひとまず現時点での参加レポを書いておく。
+発表はトラック A、B に分かれていたのだが、今回はすべて A トラックを視聴している (切り替えるのが面倒だっただけ)。
+++発表・スライドのメモ (引用ではない)
+
感想など
+PHP で AWS Lambda
+++Rails のプロジェクトを PHPer のメンバのみでメンテ +→他のメンバもわかる PHP にリプレースを検討
+サーバレス
++
+- サーバ・インフラの管理が不要
+- アプリケーションコードの知識だけで保守可能
+ゼロベースで作れる案件が (Railsの件とは別に) あるため、そちらで試験的に導入?
+AWSの学習 +AWS のドキュメント +DevelopersIO
+AWS Lambda のカスタムランタイムで PHP を動かす
+サーバのセットアップや維持管理を気にしなくて良い +サーバーレスで PHP を動かすツールがすでにある +サーバーレス構築はすんなり
+今は Laravel がルーティングしている +Laravel Livewire を Lambda に載せられないか? +デプロイ方法は? +バッチ処理は? (Lambda は 15分の制限)
+Lambda でコンテナイメージがサポートされるように
+抽象化されたもの「だけ」しか知らないよりも具象の理解は助けになる
+
AWS Lambda のような Function as a Service はマイクロサービス化における一つの到達点に思えるのだが、これを使って実際に web サービスを作る具体的なイメージがまだ見えない (注: すべて for me として書いている)。
+PHP on AWS Lambda があれだけ簡単に動かせるのには驚いた。
+勝手に AWS Lambda だとフットプリントの軽さが求められそう (= PHP + Laravel などでは動かなさそう) だという先入観を持っていたのだが、この発表のデモによればそうでもないらしい。
+大規模サイトの SEO
+++大規模サイト (100万ページ以上) +Google の基準
+クロールバジェットを意識したSEO
+大規模サイトでコンテンツが中頻度 (1回/週) で更新 OR 中規模サイト (10,000以上) でコンテンツが目まぐるしく変更される +これを満たさないなら、クロールバジェットを考えなくてもいい
+サーチコンソール +「カバレッジ」の「除外」 +多すぎるのは問題→クロールバジェットを浪費している
++
+- クエリの順番を決める
+- 空の値のルールを決めておく
+- リダイレクトすればインデックスはうまくいく
+- リンクが存在する限りクロールはされる
+リニューアル前のURL
+インデックスは移行される +リンクのURLが存在する限り、別のURLとしてクロールされる +リダイレクトされるとはいえ、リニューアル前のURLは移行した方が良い +リニューアルで無視されるようになったパラメータも注意
+robotes.txt で拒否しているのにクロールされる +一時的に拒否を外して 404 や 301 を読ませる +内部リンクを確認する +JS でのリンクに書き換え
+クエリパラメータからURLのパスに +
+/tokyo?area=HOGE→/tokyo/HOGEURL 設計だいじ
+
SEO (Search Engine Optimization) は大して知らないので新鮮な話が多かった。その分語れることも少ない……。
+++知覚可能 +操作可能 +理解可能 +堅牢 ちゃんとしたHTMLを書く (閉じタグ・入れ子構造など)
++
+- +
+標準の HTML を適切に使う
+- +
+WAI-ARIA
+- +
+キーボードフレンドリー
+- +
+マシンフレンドリー
+- +
+SEOフレンドリー
+button タグ +→キーボード +h1 タグ +→スクリーンリーダー・クローラ +a タグ
+WAI-ARIA +HTML では表現できないセマンティクスを追加する
++
+- ロール +
++
+- 何をするのか?
+- ユーザーアクションによって変化しない
+- プロパティ +
++
+- 関連づけられたデータ
+- ステート +
++
+- 現在の状態
+まずは標準の HTML 要素で解決する +何でもかんでも WAI-ARIA を使えばいいというものではない
+マウスホバーでツールチップが出てくるが、キーボード操作では出てこない
+VoiceOver
+全ての属性を使う必要はない +あくまでアクセシビリティを上げるための方法の一つにすぎない
+
つい最近 WAI-ARIA についての記事を読んだばかりだったので個人的にタイムリーな話題だった。(あまりこの言葉を使いたくないのだが) いわゆる「健常者」にとって、こうした問題を普段の生活の中で意識するのは難しい。だからこそ情報へのアンテナは張っておくようにしたい。
+PHP で FUSE
+個人的に楽しみだった発表。
+++VFS (virtual filesystem) vs 具体的なファイルシステム
+最適な実装方法は状況により異なる
+アプリケーションに見せるAPIは変えずに実装を隠蔽する→VFS
+カーネルのプログラムを作るのは難しい
++
+- 権限がデカすぎる
+- システム全体がクラッシュ
+- セキュリティリスク
+- 開発サイクルを回しづらい
+- ネイティブコードにコンパイルされる言語である必要がある
+Filesystem in USEr space (FUSE)
++
+- 特定の C の関数を呼ぶことで filesystem が作れる
+- FFI を持つ言語なら FUSE が使える
+SSHFS / s3fs / Docker Desktop
+Linux 以外でも使える
++
+- dokany (on Windows)
+- osxfuse
+VFS: システムコールが呼ばれると、ファイルシステムによってコール +FUSE: カーネル空間からユーザ空間へ通信
+高レベルなラッパで型をつける
+PHP 以外では Wordpress を FUSE にマウントする実装がある (C, Python など)
++
+- grep できる
+- sed できる
+- 編集できる
+
期待通りの興味深い発表だった。FUSE 自体も今回の発表で知ったのだが、これ本体の実装を見るのも面白そうだ。 +この発表を聞きながらファイルシステムにマウントできそうなものを考えていたのだが、およそ木構造をしているものすべてと言えそうだ (ハンマーしか持っていないと云々)。何かできそうだがなかなか思いつかない。
+ATDD
++++
+- ユーザーストーリー
+- ユニットテスト
+- CI/CD
+ユーザストーリーの受け入れ条件が曖昧になりがち +デグレチェックがユニットレベルでは収まらない場合、手動で同じシナリオをテストしている
+Q2の強化 +アジャイルテストの4象限
+技術面/ビジネス面 +開発チーム支援(コーディング前・コーディング中)/製品批評(コーディング後)
++
+- Q1: 技術面 & チーム支援 +
++
+- TDD
+- ユニットテストなど
+- Q2: ビジネス面 & チーム支援 +
++
+- ATDD
+- ビジネス面の受け入れテストで駆動する
+Agile Alliance +ユーザストーリーのスキルレベルを高める
+テストピラミッド
++
+- +
+UI Tests
+- +
+Service Tests
+- +
+Unit Tests
+- +
+異なる粒度のテストを書く
+- +
+高レベルになるほど、持つべきテストは少なくなる
++
+- ピラミッド型になる
+高レベルテストが多すぎる→アイスクリームコーン アンチパターン
+ATDD (Acceptance TDD) +API経由・UI経由での高レベルテスト E2E test
+ストーリ受け入れテスト
+入れ子のフィードバックループ +ATDD(外側) と TDD(内側)
+外部品質・内部品質
+バーティカルスライスのデリバリー
++
+- cucumber
+- gauge
+- behat
+ユビキタス言語 +手動テストもspecに書く +自動化は可能だがコスパが悪い +失敗することがわかっているテスト(レッドテスト)はCIから外す +失敗時の原因究明が難しい +饒舌なエラーメッセージ +状況のスナップショット
+Continuous Testing
+
User Acceptance Test (UAT) くらいの規模になると個人開発・趣味開発では触れない領域なので、大いに勉強になった。スライドに添付されている資料が相当に充実していたので、これを読むのが本番といった様相すら感じる。 +高レベルテストの自動化は現在のプロジェクトでも感じており、自動化のチャンスは伺っている。とはいえセッションでも指摘されているように自動化することにコストがかかりすぎる領域があるのも事実で、そのバランスが難しい。
+型解析を用いたリファクタリング
+型のある世界で生きてきた身として大いに楽しみにしていた発表。
++++
+- PHPStan
+- Phan
+- Psalm
+autoload も認識できる +bootstrapFiles
+編集箇所と利用箇所を CI でチェック +ルールレベルを徐々に引き上げていく +警告が多すぎると見落としてしまう・無視されやすくなる
+型がついていないことによるエラーが多い
+型よりも詳細な検査
+Util_Assert::minSQL を静的解析 +placeholder の型付け
+警告レベルを低いレベルから導入 +タイプヒントを積極的に書いていく +PHPStan の拡張を追加する
+
昨今、動的型付き言語での型宣言・型アノテーション・型ヒントの導入が相次いでいる。長らく静的型付き言語を書いてきた私からすると、ようやく気づいたかといったところだが、ともかく型を導入する言語が増えてきた。 +今のプロジェクトでも新しく追加するコードには型をつけるよう努めているが、どうしても古いコードには型がついていない。個人的には型のないコードに対してどう型を自動的に付けるかという点に興味があり、その点で Ruby の typeprof には注目している。
+昼食をとっていた。事前に何か食料を買っておくべきだった。
+Documentation as Code
+この発表も以前から非常に楽しみにしていた。
+++開発開始までのオーバーヘッド +新規にチームにジョイン +担当範囲外の機能を理解 +オンボーディングのコスト
+PHPerKaigi 2020 で発表あり
+継続的にシステムの理解を助けるドキュメント
+継続的ドキュメンテーション +システムとドキュメントの乖離
+書いてあることが間違っている・足りない
++
+- 徐々にずれていく
+- システムの更新タイミングとドキュメントの更新タイミングに差がある
+システムとドキュメントは対応関係がある
++
+- 間違ったドキュメント
+- 存在しないドキュメント
+システムとドキュメントの乖離を定量化する +継続的に +システムの更新に近いタイミングで ドキュメントを更新し続ける
+Documentation as Code
+コードと同じツールでドキュメントを書く
++
+- issue tracker
+- vcs
+- plain text markup
+- automation
+開発者 +システム +ドキュメント +構造化データ +ソフトウェア
+システムから構造化データを抽出する +PHPDoc +OpenAPI
+ビュー 関心ごとに合わせてアーキテクチャを一つ以上の側面(断面)で説明する
+ビューの単位でドキュメントに
+スタックトレースからのドキュメント生成
+
ドキュメントの管理は現プロジェクトでも課題と感じている。作られた当初は正しくても、実態と乖離していくのを止めるのは困難を極める。全体的に興味深い発表だったが、特にスタックトレースからのドキュメント生成というアイデアに惹かれるものを感じた。スタックトレースという実態と不可分な (乖離しない) 情報を起点にするのは理にかなっている。問題はトレースをいつ、どう取るかだろうか。それを自動化しなければ、実態との乖離が避けられないだろう。
+cookie による session 管理
+全体的に基本的な話だったので特に触れない。Cookie やセッションの話としては非常に分かりやすくまとめられていたので、知らない人が学ぶにはいい教材だろう。
+PHP のエラーと例外
+++エラー PHPエンジンがエラーを通知する +例外 プログラムが投げる
+PHP7-8とエラー
+PHPエンジンのエラーの一部が \Error に変換されるようになった +→ try-catch で捕捉できる
+\Error は例外とは異なる
+PHP8 でエラーレベルの引き上げ
++
+- 捕捉すべきもの +
++
+- recoverable
+- 捕捉すべきでないもの +
++
+- unrecoverable
+- 開発時に対処できるもの
+例外
++
+- 捕捉して事後処理
+- 捕捉せず(or 捕捉した上で)さらに上に是非を問う
+開発段階で例外を把握し、ハンドリングを考えておく
+\Throwable \Exception と \Error
+\Error はキャッチすべきでない
++
+- +
+\Error
++
+- 本番で起きてはいけない
+- +
+\LogicException
++
+- 本番で起きてはいけない +→生じないのだから捕捉もしない
+- +
+\RuntimeException
++
+- 起こるかもしれないので本番環境でも考慮する
+捕捉して対応するのではなく、未然に防ぐ
+独自例外を使う +\Exception を投げてしまうと、 +catch (\Exception)せざるを得ない +→catch 範囲が広すぎる
+SPL の例外を使う
+例外翻訳 +上位のレイヤが下位のレイヤの例外を捕捉し、上位レイヤのAPIに「翻訳」する +下位レイヤの知識に依存させない
+@throws +捕捉してほしい例外を書き連ねておく
+呼び出しもとに負わせたい責任
+
PHP を学んでいる途中の私としては、今まさに聞きたい発表だった (現時点で PHP を書き始めてから 4ヶ月ほどになる)。
+個人的に例外やエラーを最もうまく扱っているのは Go、Swift、Rust、Haskell などのエラーを「値として」扱う言語だと思っている。try-catch は通常の処理フローを完全に壊してしまう上、構文としても重すぎる。値としてのエラー通知は C言語時代への回帰ともいえるが、その頃と異なるのはエラーを暗黙のうちに握り潰すことがないということだ。これらの言語は型を持っており、静的に検証ができる (C のそれはまともな型付けではない。念のため)。
+PHP のように、すでに例外が言語システムに根ざしている言語ではどうすればよいか。この場合も同じく静的検証の力を借りることになるだろう。
+Laravel のメール認証
+Laravel の知識がない私にはまったくついていけなかった。また、個人的にタイトルがややミスリーディングに感じた。
+gRPC
+++Unary RPCs +Server streaming RPCs +Client streaming RPCs +Bidirectional streaming RPCs
+Protobuf
+実装とAPIが乖離しにくい +自動生成 +複数言語でも相互に使える
+マイクロサービスのサービス通信 +スマホアプリ +ゲームサーバ
+PHP では?
+PHP ではストリーミングが難しい +リクエストごとにプロセスが使い捨て
+PHP ではgRPCのクライアントしか対応していない
+gRPC-Web +ブラウザで扱うためのJSライブラリ+プロトコル
+HTTP/1.1 でも使える +Unary RPC と Server streaming RPC のみ
+Envoy +Nginx などで相互に gRPC と gRPC-Web で変換
+Amp +イベント駆動な並行処理のフレームワーク
+HTTP/2 対応
+C#のgRPC-Webが楽
+
(発表の中でもまさに同じことをおっしゃっていたが) PHP 以外の方が向いているだろう、というのが第一の感想である。gRPC はそれ自体というよりも Protobuf というエコシステムに乗れることのメリットが大きいと感じる。そのエコシステムにうまく乗れない時点で、うーんという感じ。
+アーキテクチャテスト
+++Independent Core Layer Pattern
+開発初期のアーキテクチャが崩れる +アーキテクチャ観点のコードレビューができない
+どこにクラスを置けばよいか? +ドキュメントがない
+アーキテクチャ設計に関する知識が属人化・暗黙知化
+ガイドライン
++
+- 最初にルールを決めるのは簡単
+- ルール通り作り始めるのも簡単 +
++
+- →維持するのが難しい、人が決めたものゆえ壊れやすい
+PHP の特性
++
+- クラスは public
+- 可視性の制御が public / protected / private のみ
+- 依存関係の制御が困難
+アーキテクチャテスト +クラスの依存関係や実装ルールをコードとして表現し、自動テスト化する
++
+- deptrac
+- phpat
+Independent Core Layer Pattern
+アーキテクチャテストの失敗
++
+- 実装誤り
+- or アーキテクチャが適切でない +
++
+- 開発の過程でフィードバックしていく
+モジュラーモノリス→マイクロサービスへ
+
冒頭に書いた通り、2日目から体調が悪くまともに聴けていない。途中までは頭痛を我慢しつつ見ていたのだが、まともに入ってこなかった。
+残念ではあるが、いずれにせよ見られていない発表は他にもあるので、今週末にでもまとめて見ようと思う。
+Day 2 にほとんど参加できなかったのは残念だが、イベント自体は大変楽しく、また興味深いものであった。自分がまったく知らない領域の話を聞けるのはこうしたイベントならではだと感じる。オンライン開催ゆえ現地に行く必要がなく、気軽に参加できたのも (特に初参加者として) 嬉しいポイントだった。
+今回、雑談/登壇者への質問等向けに Discord サーバもあったのだが、こちらは参加こそしたものの ROM のままになってしまった。発表に1ウィンドウ、メモを書くのに1ウィンドウ、Discord 表示に 1ウィンドウで私にはもう脳のリソースとディスプレイのスペースが追いつかなかった (さらにいうと Zoom でアンカンファレンスもやっていたようだ。こちらはまったく参加していない)。
+1つ個人的な反省点としては、一つ一つのセッションを真剣に聞き過ぎたというものがある。もっと適当に聞いておけばよかった。これだけだと大変語弊があるのだが、言い方を変えると、Discord しかりアンカンファレンスしかり「このイベントのこの瞬間にしかないコンテンツ」に触れずに、後から見返せる発表やスライドに注力してしまった、ということだ。発表の詳細な見直しはあとからできるのだから、今しかできないことを考えるべきだった。 +まあ初カンファレンスだし、とお茶を濁しておこう。
+さて、カンファレンスで一つ気になったことがある。それは、Discord という書き込み場所が増えたことでニコ生のコメントの流量が吸い取られてしまったのではないか、という点だ。ニコニコだけ見ていると過疎っているかのように見えた発表も、Discord の方では盛り上がっている、というのを何度か見かけた。ニコニコのコメント方式は盛り上がりを如実に反映するが、逆もまたしかり。Discord があったこと自体はプラスだったと思うが、この点はマイナスだったのではないかと感じる。
+最後になりましたが、毎年の PHPerKaigi 開催にご尽力されている皆様、スピーカーの皆様、楽しい3日間でした。ありがとうございました! +(ずっと常体で書いてしまったのでいきなり仏頂面から笑顔になったようで気持ち悪い)
+ではまた来年。
+]]>2022-04-09 から 2022-04-11 にかけて開催された、PHPerKaigi 2022 に、一般参加者として参加した。 +弊社デジタルサーカス株式会社 はダイヤモンドスポンサーとなっており、スポンサー枠のチケットを使わせていただいた。
+昨年のレポートはこちら。
+多くの素晴らしいトークの中から、特におすすめのものを 5つ選んだ。是非聞いてほしい。引用部分は、リンク先プロポーザルから引用している。
+予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント
+++ +PHP はバージョンを追う毎に型宣言、例外、表明、列挙型などの機能が大幅に強化され、堅牢なコードを書くための機能が充実してきました。それらの機能はどう使うと効果的なのでしょうか。
+本講演では PHP 8.1 をベースにして、誤りを想定してチェックするのではなく、そもそも誤りにくい設計とはどのようなものか、つまり「予防」の観点を軸足に、堅牢なコードを導くための様々な設計のヒントをご紹介します。
+
++ +PHPを使ってるとよく遭遇する Fatal error / Parse error / Warning / Notice 理解していますか?
+
+これらのエラー文を理解することで、すぐにエラーの原因に気付き適切に対象できる様になります!
+またそれらを理解した上でのエラーハンドリングを学びましょう。
++毎日流れてくるエラーに皆さんはどう向き合ってますか?
+
+エラーを出さない事が一番ですが、完全に塞ぐ事は難しいと考えます。
+サービス運用の中で本番環境から発生するエラー(サーバー・クライアントサイド・サードパーティ起因のエラー)への監視体制と、
+エラー・バグ防御のためチームで行っているテストコード文化づくりの話をします。
ISUCON11のPHP実装は、何を考え、どのようにして作られていたのか
+++昨年開催されたISUCON11にて問題(参考実装)のPHPへの移植を担当させていただきました。
+最終的なソースコードこそシンプルなWebアプリケーションではありますが、その裏には
+
+・「(私の思う)良い設計」を実現するための意思決定
+・「ISUCONの問題」という位置付けに由来する取捨選択
+・移植中に遭遇したトラブルとその解決策
+といった文脈や葛藤が存在しています。本発表はそれらを共有することで
+
+・PHPアプリケーションの設計、実装事例として役立ててもらう
+・ISUCONの言語移植に興味を持ってもらう
+・ISUCON問題移植の「実装や設計の練習をする教材」としての可能性を知ってもらう
+ことを目的とします。
チームの仕事はまわっていたけど、メンバーはそれぞれモヤモヤを抱えていた話──40名の大規模開発チームで1on1ログを公開してみた
+++サイボウズの大企業向けグループウェアのGaroon(ガルーン)は、PHPで開発されている20年目の製品です。ガルーン開発チームは日本で40名、ベトナムで50名の計90名ほどのチームになっています。また、コロナ禍でフルリモートでの活動がこの2年ほど継続してきました。
+フルリモートになっても仕事はまわっており、継続的にリリースはしていましたが、一方でお互いの考えていることや感じている問題意識が見えづらくなり、モヤモヤを抱えているメンバーが増えていました。
+このセッションでは、そういう状況で私がチーム外からジョインし、聴き役に徹しながら見える化することで状況を改善していった取り組みを紹介します。同じように大きなチームやリモートワークで難しさを感じている人に、難しさの原因への気づきや取り組みへのヒントがあれば幸いです。
+
今回は、PHPer チャレンジ用に弊社のトークン問題を 3題作成した。こちらについては別途記事にしているので、そちらを参照されたい。
+1位になった。
+また、賞品として Echo Show 15 をいただいた。
去年の参加レポ では、こんなことを書いた。
+++1つ個人的な反省点としては、(中略) Discord しかりアンカンファレンスしかり「このイベントのこの瞬間にしかないコンテンツ」に触れずに、後から見返せる発表やスライドに注力してしまった、ということだ。発表の詳細な見直しはあとからできるのだから、今しかできないことを考えるべきだった。
+
+まあ初カンファレンスだし、とお茶を濁しておこう。
この反省を踏まえ、今年は積極的にほかの場 (公式の Discord サーバや、アンカンファレンス) にも参加した。
+これにより、参加体験の質がはるかに向上した。特に Discord に関しては、登壇者ご本人による補足や、質問への回答などがおこなわれる (ことが多い) ため、特別な理由のない限り、発言はしないまでも参加はしておいたほうが良いと思われる。
なお、アンカンファレンスについては、1日目の終わりにトークン問題の解説放送もおこなった。
+また、今年はオフラインとオンラインのハイブリッド開催であったが、去年の全オンラインと比べて、オンライン参加の体験が落ちていなかったのは、特筆すべきであろう。 +今年は 3回目のワクチン接種が間に合わなかったこともあり現地参加は見送ったのだが、来年は是非オフラインで参加したい。
+PHPerKaigi 2023 があるかどうか存じ上げないが、あるとすれば、次の 4つを目標としたい。
+最後になりましたが、PHPerKaigi のスタッフ、スポンサー、スピーカーのみなさん、素敵な時間をありがとうございました。
+ではまた来年。
+]]>本日開始された PHPerKaigi 2022 の PHPer チャレンジにおいて、弊社 デジタルサーカス株式会社 の問題を 3問作成した。この記事では、これらの問題の解説をおこなう。
+リポジトリはこちら: https://github.com/nsfisis/PHPerKaigi2022-tokens
+ソースコードはこちら。実行には PHP 8.1 以上が必要なので注意。
+<?php
+
+declare(strict_types=0O1);
+
+namespace Dgcircus\PHPerKaigi\Y2022;
+
+/**
+ * @todo
+ * Run this program to acquire a PHPer token.
+ */
+
+https://creativecommons.org/publicdomain/zero/1.0/
+
+\error_reporting(~+!'We are hiring!');
+
+$z = fn($f) => (fn($x) => $f(fn(...$xs) => $x($x)(...$xs)))(fn($x) => $f(fn(...$xs) => $x($x)(...$xs)));
+$id = \spl_object_id(...);
+$put = fn($c) => \printf('%c', $c);
+$mm = fn($p, $n) => new \ArrayObject(\array_fill(+!![], $n, $p));
+
+$👉 = fn($m, $p, $b, $e, $mp, $pc) => [++$mp, ++$pc];
+$👈 = fn($m, $p, $b, $e, $mp, $pc) => [--$mp, ++$pc];
+$👍 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, ++$m[$mp]];
+$👎 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, --$m[$mp]];
+$📝 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, $put($m[$mp])];
+$🤡 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
+ +!![] => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
+ $b => $loop(++$pc, ++$n),
+ $e => $n === +!![] ? ++$pc : $loop(++$pc, --$n),
+ default => $loop(++$pc, $n),
+ })($pc, -![])],
+ default => [$mp, ++$pc],
+};
+$🎪 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
+ +!![] => [$mp, ++$pc],
+ default => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
+ $e => $loop(--$pc, ++$n),
+ $b => $n === +!![] ? $pc+![] : $loop(--$pc, --$n),
+ default => $loop(--$pc, $n),
+ })($pc, -![])],
+};
+$🐘 = fn($p) => $z(fn($loop) => fn($m, $p, $b, $e, $mp, $pc) =>
+ isset($p[$pc]) && $loop($m, $p, $b, $e, ...($p[$pc]($m, $p, $b, $e, $mp, $pc)))
+)($mm(+!![], +(![].![])), $p, $id($🤡), $id($🎪), +!![], +!![]);
+
+$🐘([
+ $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $🤡,
+ $👉, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $👈, $👈, $👈, $👈, $👎,
+ $🎪,
+ $👉, $👍, $👍, $👍, $👍, $👍, $📝,
+ $👎, $👎, $📝,
+ $👉, $👎, $👎, $👎, $📝,
+ $👉, $👎, $👎, $👎, $📝,
+ $👎, $👎, $📝,
+ $👎, $📝,
+ $👈, $📝,
+ $👉, $👉, $👎, $👎, $📝,
+ $👍, $👍, $👍, $👍, $👍, $👍, $👍, $📝,
+ $👈, $👎, $👎, $👎, $👎, $📝,
+ $👈, $📝,
+ $👉, $👍, $👍, $📝,
+ $👉, $👎, $📝,
+ $👈, $📝,
+]);
+この問題は、単に適切なバージョンの PHP で動かせばトークンが得られる。
+まず目につくのは大量の絵文字だろう。 +PHP は識別子に使用できる文字の範囲が広く、絵文字も使うことができる。
+Brainf*ck のインタプリタとプログラムになっている。 +Brainf*ck とは、難解プログラミング言語のひとつであり、ここで説明するよりも Wikipedia の該当ページを読んだ方がよい。
+https://ja.wikipedia.org/wiki/Brainfuck
+なお、brainf*ck プログラムを普通の書き方で書くと、次のようになる。
++ + + + + + + + + +
+[
+ > + + +
+ > + + + + +
+ > + + + + + + + + + + + +
+ > + + + + + + + + + +
+ < < < < -
+]
+> + + + + + .
+- - .
+> - - - .
+> - - - .
+- - .
+- .
+< .
+> > - - .
++ + + + + + + .
+< - - - - .
+< .
+> + + .
+> - .
+< .
+実行結果はこちら: https://ideone.com/22VWmb
+それぞれの絵文字で表された関数が、各命令に対応している。
+$👉: >$👈: <$👍: +$👎: -$📝: .$🤡: [$🎪: ], (入力) に対応する関数はない (このプログラムでは使わないので用意していない)。
なお、$🐘 はいわゆる main 関数であり、プログラムの実行部分である。
おおよそ意味に合致するよう選んでいるが、$🤡 と $🎪 は弊社デジタルサーカスにちなんでいる。
+また、$🐘 は PHP のマスコットの象に由来する。
declare 文の strict_types に指定できるのは、0 か 1 の数値リテラルだが、
+0x0 や 0b1 のような値も受け付ける。
+今回は、PHP 8.1 から追加された、0O または 0o から始まる八進数リテラルを使った。
ソースコードのライセンスを示したこの部分だが、
+https://creativecommons.org/publicdomain/zero/1.0/
+完全に合法な PHP のコードである。
+https: 部分はラベル、// 以降は行コメントになっている。
ソースコード中に、ほとんど数値リテラルが書かれていないことにお気づきだろうか。 +PHP では、型変換を利用することで任意の整数を作り出すことができる。
+assert(0 === +!![]);
+assert(1 === +![]);
+assert(2 === ![]+![]);
+assert(3 === ![]+![]+![]);
+assert(10 === +(![].+!![]));
+[] に ! を適用すると true が返ってくる。それに +
+を適用すると、bool から int ヘの型変換が走り、1 が生成される。10
+はさらにトリッキーだ。まず 1 と 0 を作り、. で文字列として結合する
+('10')。これに + を適用すると、string から int
+への型変換が走り、10 が生まれる (コード量に頓着しないなら、1 を 10
+個足し合わせてももちろん 10 が作れる)。
また、error_reporting に指定しているのは -1 である。
+これは、! によって文字列を false にし、+ によって false を 0 にし、さらにビット反転して -1 にしている。
if 文なしで条件分岐三項演算子ないし match 式を使うことで、if を一切書かずに条件分岐ができる。
+また、&& / || も使えることがある。
+遅延評価が不要なケースでは、[$t, $f][$cond] のような形で分岐することもできる。
while、for 文なしでループ不動点コンビネータを使って無名再帰する (詳しい説明は省略する。これらの単語で検索してほしい)。
+ここでは、一般に Z コンビネータとして知られるものを使った ($z)。
実際のところ、$🤡 や $🎪、$🐘 は、一度 Scheme (Lisp の一種) で書いてから PHP に翻訳する形で記述した。
なお、PHP は末尾再帰の最適化をおこなわない (少なくとも今のところは) ので、 +あまりに長い brainf*ck プログラムを書くとスタックオーバーフローする。
+ソースコードはこちら。実行には PHP 8.0 以上が必要なので注意。
+<?php
+
+/*********************************************************
+ * This program displays a PHPer token. *
+ * Guess 'N'. *
+ * *
+ * Hints: *
+ * - N itself has no special meaning, e.g., 42, 8128, *
+ * it is selected at random. *
+ * - Each element of $token represents a single letter. *
+ * - One letter consists of 5x5 cells. *
+ * - Remember, the output is a complete PHPer token. *
+ * *
+ * License: *
+ * https://creativecommons.org/publicdomain/zero/1.0/ *
+ *********************************************************/
+const N = 0 /* Change it to your answer. */;
+assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
+
+$token = [
+ 0x14B499C,
+ 0x0BE34CC, 0x01C9C69,
+ 0x0ECA069, 0x01C2449, 0x0FDB166, 0x01C9C69,
+ 0x01C1C66, 0x0FC1C47, 0x01C1C66,
+ 0x10C5858, 0x1E4E3B8, 0x1A2F2F8,
+];
+foreach ($token as $x) {
+ $x = $x ^ N;
+
+ $x = sprintf('%025b', $x);
+ $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+ $x = implode("\n", str_split($x, length: 5));
+ echo "{$x}\n\n";
+}
+さて、この問題はさきほどのように単純に実行しただけでは、謎のブロックが表示されるだけでトークンは得られない。
+トークンを得るためには、ソースコードを読み、定数 N を特定する必要がある。
ここでは、私の想定解を解説する。
+まずはソースコードを読んでいく。
+$token = [
+ // 略
+];
+数値からなる $token があり、各要素をループしている。
$x = $x ^ N;
+まずは排他的論理和 (xor) を取り、
+ $x = sprintf('%025b', $x);
+二進数に変換して、
+ $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+0 を空白に、1 を # にし、
$x = implode("\n", str_split($x, length: 5));
+5文字ごとに区切ったあと、改行で結合している。
+次に、ソースコードに書いてあるヒントを読んでいく。
+N それ自体は、42 や 8128 といったような特別な意味を持たず、ランダムに決められている$token の各要素は、1文字を表すここで、PHPer トークンは必ず # 記号から始まることを思いだすと、
+$token の最初の数字 0x14B499C は、変換の結果 # になるのではないかと予想される (なお、このことは、リポジトリの README ファイルに追加ヒントとして書かれている)。
ここまでわかれば、あと一歩で解ける。すなわち、0x14B499C が # に変換されるような N を見つければよい。
N は高々
assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
+なのでブルートフォースしてもよいが、ここではブルートフォースしない方法を紹介する。
+<?php
+
+$x = 0x14B499C;
+
+$x = $x ^ N;
+
+$x = sprintf('%025b', $x);
+$x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+$x = implode("\n", str_split($x, length: 5));
+
+assert($x ===
+ " # # \n" .
+ "#####\n" .
+ " # # \n" .
+ "#####\n" .
+ " # # ");
+この一連の変換に対する逆変換を考えると、次のようになる。
+<?php
+
+$x =
+ " # # \n" .
+ "#####\n" .
+ " # # \n" .
+ "#####\n" .
+ " # # ";
+
+$x = implode('', explode("\n", $x));
+$x = str_replace(search: [' ', '#'], replace: ['0', '1'], subject: $x);
+$x = bindec($x);
+
+$n = $x ^ 0x14B499C;
+
+echo "N = $n\n";
+これを実行すると、N が得られる。
ソースコードはこちら。
+<?php
+
+// License: https://creativecommons.org/publicdomain/zero/1.0/
+// This is a quine-like program to generate a PHPer token.
+// Execute it like this: php toquine.php | php | php | php | ...
+
+$s = <<<'Q'
+<?cuc
+// Yvprafr: uggcf://perngvirpbzzbaf.bet/choyvpqbznva/mreb/1.0/
+// Guvf vf n dhvar-yvxr cebtenz gb trarengr n CUCre gbxra.
+// Rkrphgr vg yvxr guvf: cuc gbdhvar.cuc | cuc | cuc | cuc | ...
+%f$f = %f;
+$f = fge_ebg13($f); $kf = [
+%f,
+];
+$g = ahyy.snyfr; sbe ($v = 0; $v <= vagqvi(__YVAR__-035,6); ++$v) vs (!vffrg($kf[$v])) oernx; ryfr
+$g .= vzcybqr("\a", fge_fcyvg(fge_ercynpr(['0','1'], [' ','##'], fcevags(pue(37) . '025o', $kf[$v])), 012)) . "\a\a";
+$jf = neenl_znc(sa($j) => vzcybqr(', ', $j), neenl_puhax(neenl_znc(sa($k) => fcevags('0k' . pue(37) . '07K', $k), $kf), 10));
+cevags($f, $g, fge_ebg13("<<<'Q'\a{$f}\aQ"), vzcybqr(",\a", $jf));
+Q;
+$s = str_rot13($s); $xs = [
+0x0AFABEA, 0x1F294A7, 0x1F2109F, 0x1F294A7, 0x0002800, 0x1F2109F, 0x0117041, 0x1F294A7, 0x1FAD6B5, 0x1F295B7,
+0x010FC21, 0x1FAD6B5, 0x1151151, 0x010FC21, 0x1F294A7, 0x1F295B7, 0x1FAD6B5, 0x1F294A7, 0x1F295B7, 0x1F8C63F,
+0x1F8C631, 0x1FAD6B5, 0x17AD6BD, 0x17AD6BD, 0x1F8C63F, 0x1F295B7,
+];
+$t = null.false; for ($i = 0; $i <= intdiv(__LINE__-035,6); ++$i) if (!isset($xs[$i])) break; else
+$t .= implode("\n", str_split(str_replace(['0','1'], [' ','##'], sprintf(chr(37) . '025b', $xs[$i])), 012)) . "\n\n";
+$ws = array_map(fn($w) => implode(', ', $w), array_chunk(array_map(fn($x) => sprintf('0x' . chr(37) . '07X', $x), $xs), 10));
+printf($s, $t, str_rot13("<<<'D'\n{$s}\nD"), implode(",\n", $ws));
+コメントにもあるとおり、次のようにして実行すれば答えがでてくる。
+$ php toquine.php | php | php | php | ...
+実際にはもう少しパイプで繋げなければならない。
+コメントにもあるとおり、これは quine (風) のプログラムになっている。 +Quine とは、自分のソースコードをそっくりそのまま出力するようなプログラムのことである。
+このプログラムは、実行すると自身とほとんど同じプログラムを出力する。 +異なるのはトークンになっている部分のみである。
+$xs がトークンに対応している。変換のロジックは riddle.php とほぼ同じなので省略する。
トークンの何文字目まで出力したかを、ソースコードを変えずに (quine なので) 覚えておく必要がある。
+このプログラムでは、トークンが出力されるとソースコードがだんだんと長くなっていくのを利用して、__LINE__ から情報を取得している。
Quine は、素朴に書くとプログラムの一部が 2回記述されてしまう。
+これがあまり美しくないので、toquine.php では、ROT 13 変換を使って難読化した。
それにしてもなぜこんなものが標準ライブラリに……。
+解いていただいたみなさん、また、難易度調整につきあっていただいた社内のみなさん、ありがとうございました。
+今回は直前に作りはじめたのもあり、3問だけかつ使い古されたネタばかりになってしまいましたが、 +来年は 5問、より面白い問題を持っていきます。
+実はもう作りはじめているので、どうか来年もありますように……。
+]]>2021-03-26 から 2021-03-28 にかけて開催された、PHPerKaigi 2021 に一般参加者として参加した。 +弊社デジタルサーカス株式会社 (今年1月から勤務) はダイヤモンドスポンサーとなっており、スポンサー枠のチケットを使わせていただいた。
+このようなカンファレンスには初めて参加するのでかねてより心待ちにしていたのだが、生憎2日目から体調を崩してしまい、この記事も途中までとなっている。まだ見ていないセッションも多いが、ひとまず現時点での参加レポを書いておく。
+発表はトラック A、B に分かれていたのだが、今回はすべて A トラックを視聴している (切り替えるのが面倒だっただけ)。
+++発表・スライドのメモ (引用ではない)
+
感想など
+PHP で AWS Lambda
+++Rails のプロジェクトを PHPer のメンバのみでメンテ +→他のメンバもわかる PHP にリプレースを検討
+サーバレス
++
+- サーバ・インフラの管理が不要
+- アプリケーションコードの知識だけで保守可能
+ゼロベースで作れる案件が (Railsの件とは別に) あるため、そちらで試験的に導入?
+AWSの学習 +AWS のドキュメント +DevelopersIO
+AWS Lambda のカスタムランタイムで PHP を動かす
+サーバのセットアップや維持管理を気にしなくて良い +サーバーレスで PHP を動かすツールがすでにある +サーバーレス構築はすんなり
+今は Laravel がルーティングしている +Laravel Livewire を Lambda に載せられないか? +デプロイ方法は? +バッチ処理は? (Lambda は 15分の制限)
+Lambda でコンテナイメージがサポートされるように
+抽象化されたもの「だけ」しか知らないよりも具象の理解は助けになる
+
AWS Lambda のような Function as a Service はマイクロサービス化における一つの到達点に思えるのだが、これを使って実際に web サービスを作る具体的なイメージがまだ見えない (注: すべて for me として書いている)。
+PHP on AWS Lambda があれだけ簡単に動かせるのには驚いた。
+勝手に AWS Lambda だとフットプリントの軽さが求められそう (= PHP + Laravel などでは動かなさそう) だという先入観を持っていたのだが、この発表のデモによればそうでもないらしい。
+大規模サイトの SEO
+++大規模サイト (100万ページ以上) +Google の基準
+クロールバジェットを意識したSEO
+大規模サイトでコンテンツが中頻度 (1回/週) で更新 OR 中規模サイト (10,000以上) でコンテンツが目まぐるしく変更される +これを満たさないなら、クロールバジェットを考えなくてもいい
+サーチコンソール +「カバレッジ」の「除外」 +多すぎるのは問題→クロールバジェットを浪費している
++
+- クエリの順番を決める
+- 空の値のルールを決めておく
+- リダイレクトすればインデックスはうまくいく
+- リンクが存在する限りクロールはされる
+リニューアル前のURL
+インデックスは移行される +リンクのURLが存在する限り、別のURLとしてクロールされる +リダイレクトされるとはいえ、リニューアル前のURLは移行した方が良い +リニューアルで無視されるようになったパラメータも注意
+robotes.txt で拒否しているのにクロールされる +一時的に拒否を外して 404 や 301 を読ませる +内部リンクを確認する +JS でのリンクに書き換え
+クエリパラメータからURLのパスに +
+/tokyo?area=HOGE→/tokyo/HOGEURL 設計だいじ
+
SEO (Search Engine Optimization) は大して知らないので新鮮な話が多かった。その分語れることも少ない……。
+++知覚可能 +操作可能 +理解可能 +堅牢 ちゃんとしたHTMLを書く (閉じタグ・入れ子構造など)
++
+- +
+標準の HTML を適切に使う
+- +
+WAI-ARIA
+- +
+キーボードフレンドリー
+- +
+マシンフレンドリー
+- +
+SEOフレンドリー
+button タグ +→キーボード +h1 タグ +→スクリーンリーダー・クローラ +a タグ
+WAI-ARIA +HTML では表現できないセマンティクスを追加する
++
+- ロール +
++
+- 何をするのか?
+- ユーザーアクションによって変化しない
+- プロパティ +
++
+- 関連づけられたデータ
+- ステート +
++
+- 現在の状態
+まずは標準の HTML 要素で解決する +何でもかんでも WAI-ARIA を使えばいいというものではない
+マウスホバーでツールチップが出てくるが、キーボード操作では出てこない
+VoiceOver
+全ての属性を使う必要はない +あくまでアクセシビリティを上げるための方法の一つにすぎない
+
つい最近 WAI-ARIA についての記事を読んだばかりだったので個人的にタイムリーな話題だった。(あまりこの言葉を使いたくないのだが) いわゆる「健常者」にとって、こうした問題を普段の生活の中で意識するのは難しい。だからこそ情報へのアンテナは張っておくようにしたい。
+PHP で FUSE
+個人的に楽しみだった発表。
+++VFS (virtual filesystem) vs 具体的なファイルシステム
+最適な実装方法は状況により異なる
+アプリケーションに見せるAPIは変えずに実装を隠蔽する→VFS
+カーネルのプログラムを作るのは難しい
++
+- 権限がデカすぎる
+- システム全体がクラッシュ
+- セキュリティリスク
+- 開発サイクルを回しづらい
+- ネイティブコードにコンパイルされる言語である必要がある
+Filesystem in USEr space (FUSE)
++
+- 特定の C の関数を呼ぶことで filesystem が作れる
+- FFI を持つ言語なら FUSE が使える
+SSHFS / s3fs / Docker Desktop
+Linux 以外でも使える
++
+- dokany (on Windows)
+- osxfuse
+VFS: システムコールが呼ばれると、ファイルシステムによってコール +FUSE: カーネル空間からユーザ空間へ通信
+高レベルなラッパで型をつける
+PHP 以外では Wordpress を FUSE にマウントする実装がある (C, Python など)
++
+- grep できる
+- sed できる
+- 編集できる
+
期待通りの興味深い発表だった。FUSE 自体も今回の発表で知ったのだが、これ本体の実装を見るのも面白そうだ。 +この発表を聞きながらファイルシステムにマウントできそうなものを考えていたのだが、およそ木構造をしているものすべてと言えそうだ (ハンマーしか持っていないと云々)。何かできそうだがなかなか思いつかない。
+ATDD
++++
+- ユーザーストーリー
+- ユニットテスト
+- CI/CD
+ユーザストーリーの受け入れ条件が曖昧になりがち +デグレチェックがユニットレベルでは収まらない場合、手動で同じシナリオをテストしている
+Q2の強化 +アジャイルテストの4象限
+技術面/ビジネス面 +開発チーム支援(コーディング前・コーディング中)/製品批評(コーディング後)
++
+- Q1: 技術面 & チーム支援 +
++
+- TDD
+- ユニットテストなど
+- Q2: ビジネス面 & チーム支援 +
++
+- ATDD
+- ビジネス面の受け入れテストで駆動する
+Agile Alliance +ユーザストーリーのスキルレベルを高める
+テストピラミッド
++
+- +
+UI Tests
+- +
+Service Tests
+- +
+Unit Tests
+- +
+異なる粒度のテストを書く
+- +
+高レベルになるほど、持つべきテストは少なくなる
++
+- ピラミッド型になる
+高レベルテストが多すぎる→アイスクリームコーン アンチパターン
+ATDD (Acceptance TDD) +API経由・UI経由での高レベルテスト E2E test
+ストーリ受け入れテスト
+入れ子のフィードバックループ +ATDD(外側) と TDD(内側)
+外部品質・内部品質
+バーティカルスライスのデリバリー
++
+- cucumber
+- gauge
+- behat
+ユビキタス言語 +手動テストもspecに書く +自動化は可能だがコスパが悪い +失敗することがわかっているテスト(レッドテスト)はCIから外す +失敗時の原因究明が難しい +饒舌なエラーメッセージ +状況のスナップショット
+Continuous Testing
+
User Acceptance Test (UAT) くらいの規模になると個人開発・趣味開発では触れない領域なので、大いに勉強になった。スライドに添付されている資料が相当に充実していたので、これを読むのが本番といった様相すら感じる。 +高レベルテストの自動化は現在のプロジェクトでも感じており、自動化のチャンスは伺っている。とはいえセッションでも指摘されているように自動化することにコストがかかりすぎる領域があるのも事実で、そのバランスが難しい。
+型解析を用いたリファクタリング
+型のある世界で生きてきた身として大いに楽しみにしていた発表。
++++
+- PHPStan
+- Phan
+- Psalm
+autoload も認識できる +bootstrapFiles
+編集箇所と利用箇所を CI でチェック +ルールレベルを徐々に引き上げていく +警告が多すぎると見落としてしまう・無視されやすくなる
+型がついていないことによるエラーが多い
+型よりも詳細な検査
+Util_Assert::minSQL を静的解析 +placeholder の型付け
+警告レベルを低いレベルから導入 +タイプヒントを積極的に書いていく +PHPStan の拡張を追加する
+
昨今、動的型付き言語での型宣言・型アノテーション・型ヒントの導入が相次いでいる。長らく静的型付き言語を書いてきた私からすると、ようやく気づいたかといったところだが、ともかく型を導入する言語が増えてきた。 +今のプロジェクトでも新しく追加するコードには型をつけるよう努めているが、どうしても古いコードには型がついていない。個人的には型のないコードに対してどう型を自動的に付けるかという点に興味があり、その点で Ruby の typeprof には注目している。
+昼食をとっていた。事前に何か食料を買っておくべきだった。
+Documentation as Code
+この発表も以前から非常に楽しみにしていた。
+++開発開始までのオーバーヘッド +新規にチームにジョイン +担当範囲外の機能を理解 +オンボーディングのコスト
+PHPerKaigi 2020 で発表あり
+継続的にシステムの理解を助けるドキュメント
+継続的ドキュメンテーション +システムとドキュメントの乖離
+書いてあることが間違っている・足りない
++
+- 徐々にずれていく
+- システムの更新タイミングとドキュメントの更新タイミングに差がある
+システムとドキュメントは対応関係がある
++
+- 間違ったドキュメント
+- 存在しないドキュメント
+システムとドキュメントの乖離を定量化する +継続的に +システムの更新に近いタイミングで ドキュメントを更新し続ける
+Documentation as Code
+コードと同じツールでドキュメントを書く
++
+- issue tracker
+- vcs
+- plain text markup
+- automation
+開発者 +システム +ドキュメント +構造化データ +ソフトウェア
+システムから構造化データを抽出する +PHPDoc +OpenAPI
+ビュー 関心ごとに合わせてアーキテクチャを一つ以上の側面(断面)で説明する
+ビューの単位でドキュメントに
+スタックトレースからのドキュメント生成
+
ドキュメントの管理は現プロジェクトでも課題と感じている。作られた当初は正しくても、実態と乖離していくのを止めるのは困難を極める。全体的に興味深い発表だったが、特にスタックトレースからのドキュメント生成というアイデアに惹かれるものを感じた。スタックトレースという実態と不可分な (乖離しない) 情報を起点にするのは理にかなっている。問題はトレースをいつ、どう取るかだろうか。それを自動化しなければ、実態との乖離が避けられないだろう。
+cookie による session 管理
+全体的に基本的な話だったので特に触れない。Cookie やセッションの話としては非常に分かりやすくまとめられていたので、知らない人が学ぶにはいい教材だろう。
+PHP のエラーと例外
+++エラー PHPエンジンがエラーを通知する +例外 プログラムが投げる
+PHP7-8とエラー
+PHPエンジンのエラーの一部が \Error に変換されるようになった +→ try-catch で捕捉できる
+\Error は例外とは異なる
+PHP8 でエラーレベルの引き上げ
++
+- 捕捉すべきもの +
++
+- recoverable
+- 捕捉すべきでないもの +
++
+- unrecoverable
+- 開発時に対処できるもの
+例外
++
+- 捕捉して事後処理
+- 捕捉せず(or 捕捉した上で)さらに上に是非を問う
+開発段階で例外を把握し、ハンドリングを考えておく
+\Throwable \Exception と \Error
+\Error はキャッチすべきでない
++
+- +
+\Error
++
+- 本番で起きてはいけない
+- +
+\LogicException
++
+- 本番で起きてはいけない +→生じないのだから捕捉もしない
+- +
+\RuntimeException
++
+- 起こるかもしれないので本番環境でも考慮する
+捕捉して対応するのではなく、未然に防ぐ
+独自例外を使う +\Exception を投げてしまうと、 +catch (\Exception)せざるを得ない +→catch 範囲が広すぎる
+SPL の例外を使う
+例外翻訳 +上位のレイヤが下位のレイヤの例外を捕捉し、上位レイヤのAPIに「翻訳」する +下位レイヤの知識に依存させない
+@throws +捕捉してほしい例外を書き連ねておく
+呼び出しもとに負わせたい責任
+
PHP を学んでいる途中の私としては、今まさに聞きたい発表だった (現時点で PHP を書き始めてから 4ヶ月ほどになる)。
+個人的に例外やエラーを最もうまく扱っているのは Go、Swift、Rust、Haskell などのエラーを「値として」扱う言語だと思っている。try-catch は通常の処理フローを完全に壊してしまう上、構文としても重すぎる。値としてのエラー通知は C言語時代への回帰ともいえるが、その頃と異なるのはエラーを暗黙のうちに握り潰すことがないということだ。これらの言語は型を持っており、静的に検証ができる (C のそれはまともな型付けではない。念のため)。
+PHP のように、すでに例外が言語システムに根ざしている言語ではどうすればよいか。この場合も同じく静的検証の力を借りることになるだろう。
+Laravel のメール認証
+Laravel の知識がない私にはまったくついていけなかった。また、個人的にタイトルがややミスリーディングに感じた。
+gRPC
+++Unary RPCs +Server streaming RPCs +Client streaming RPCs +Bidirectional streaming RPCs
+Protobuf
+実装とAPIが乖離しにくい +自動生成 +複数言語でも相互に使える
+マイクロサービスのサービス通信 +スマホアプリ +ゲームサーバ
+PHP では?
+PHP ではストリーミングが難しい +リクエストごとにプロセスが使い捨て
+PHP ではgRPCのクライアントしか対応していない
+gRPC-Web +ブラウザで扱うためのJSライブラリ+プロトコル
+HTTP/1.1 でも使える +Unary RPC と Server streaming RPC のみ
+Envoy +Nginx などで相互に gRPC と gRPC-Web で変換
+Amp +イベント駆動な並行処理のフレームワーク
+HTTP/2 対応
+C#のgRPC-Webが楽
+
(発表の中でもまさに同じことをおっしゃっていたが) PHP 以外の方が向いているだろう、というのが第一の感想である。gRPC はそれ自体というよりも Protobuf というエコシステムに乗れることのメリットが大きいと感じる。そのエコシステムにうまく乗れない時点で、うーんという感じ。
+アーキテクチャテスト
+++Independent Core Layer Pattern
+開発初期のアーキテクチャが崩れる +アーキテクチャ観点のコードレビューができない
+どこにクラスを置けばよいか? +ドキュメントがない
+アーキテクチャ設計に関する知識が属人化・暗黙知化
+ガイドライン
++
+- 最初にルールを決めるのは簡単
+- ルール通り作り始めるのも簡単 +
++
+- →維持するのが難しい、人が決めたものゆえ壊れやすい
+PHP の特性
++
+- クラスは public
+- 可視性の制御が public / protected / private のみ
+- 依存関係の制御が困難
+アーキテクチャテスト +クラスの依存関係や実装ルールをコードとして表現し、自動テスト化する
++
+- deptrac
+- phpat
+Independent Core Layer Pattern
+アーキテクチャテストの失敗
++
+- 実装誤り
+- or アーキテクチャが適切でない +
++
+- 開発の過程でフィードバックしていく
+モジュラーモノリス→マイクロサービスへ
+
冒頭に書いた通り、2日目から体調が悪くまともに聴けていない。途中までは頭痛を我慢しつつ見ていたのだが、まともに入ってこなかった。
+残念ではあるが、いずれにせよ見られていない発表は他にもあるので、今週末にでもまとめて見ようと思う。
+Day 2 にほとんど参加できなかったのは残念だが、イベント自体は大変楽しく、また興味深いものであった。自分がまったく知らない領域の話を聞けるのはこうしたイベントならではだと感じる。オンライン開催ゆえ現地に行く必要がなく、気軽に参加できたのも (特に初参加者として) 嬉しいポイントだった。
+今回、雑談/登壇者への質問等向けに Discord サーバもあったのだが、こちらは参加こそしたものの ROM のままになってしまった。発表に1ウィンドウ、メモを書くのに1ウィンドウ、Discord 表示に 1ウィンドウで私にはもう脳のリソースとディスプレイのスペースが追いつかなかった (さらにいうと Zoom でアンカンファレンスもやっていたようだ。こちらはまったく参加していない)。
+1つ個人的な反省点としては、一つ一つのセッションを真剣に聞き過ぎたというものがある。もっと適当に聞いておけばよかった。これだけだと大変語弊があるのだが、言い方を変えると、Discord しかりアンカンファレンスしかり「このイベントのこの瞬間にしかないコンテンツ」に触れずに、後から見返せる発表やスライドに注力してしまった、ということだ。発表の詳細な見直しはあとからできるのだから、今しかできないことを考えるべきだった。 +まあ初カンファレンスだし、とお茶を濁しておこう。
+さて、カンファレンスで一つ気になったことがある。それは、Discord という書き込み場所が増えたことでニコ生のコメントの流量が吸い取られてしまったのではないか、という点だ。ニコニコだけ見ていると過疎っているかのように見えた発表も、Discord の方では盛り上がっている、というのを何度か見かけた。ニコニコのコメント方式は盛り上がりを如実に反映するが、逆もまたしかり。Discord があったこと自体はプラスだったと思うが、この点はマイナスだったのではないかと感じる。
+最後になりましたが、毎年の PHPerKaigi 開催にご尽力されている皆様、スピーカーの皆様、楽しい3日間でした。ありがとうございました! +(ずっと常体で書いてしまったのでいきなり仏頂面から笑顔になったようで気持ち悪い)
+ではまた来年。
+]]>本記事は Python 3.7.6 の動作結果を元にして書かれている。
+Python でクロージャを作ろうと、次のようなコードを書いた。
+def f():
+ x = 0
+ def g():
+ x += 1
+ g()
+
+f()
+関数 g から 関数 f のスコープ内で定義された変数 x を参照し、それに 1 を足そうとしている。
+これを実行すると x += 1 の箇所でエラーが発生する。
++UnboundLocalError: local variable ‘x’ referenced before assignment
+
local変数 x が代入前に参照された、とある。これは、f の x を参照するのではなく、新しく別の変数を g 内に作ってしまっているため。
+前述のコードを宣言と代入を便宜上分けて書き直すと次のようになる。var を変数宣言のための構文として擬似的に利用している。
# 注: var は正しい Python の文法ではない。上記参照のこと
+def f():
+ var x # f の local変数 'x' を宣言
+ x = 0 # x に 0 を代入
+ def g(): # f の内部関数 g を定義
+ var x # g の local変数 'x' を宣言
+ # たまたま f にも同じ名前の変数があるが、それとは別の変数
+ x += 1 # x に 1 を加算 (x = x + 1 の糖衣構文)
+ # 加算する前の値を参照しようとするが、まだ代入されていないためエラー
+ g()
+当初の意図を表現するには、次のように書けばよい。
+def f():
+ x = 0
+ def g():
+ nonlocal x ## (*)
+ x += 1
+ g()
+(*) のように、nonlocal を追加する。これにより一つ外側のスコープ (g の一つ外側 = f) で定義されている x を探しに行くようになる。
本記事は Python 3.7.6 の動作結果を元にして書かれている。
+Python でクロージャを作ろうと、次のようなコードを書いた。
+def f():
+ x = 0
+ def g():
+ x += 1
+ g()
+
+f()
+関数 g から 関数 f のスコープ内で定義された変数 x を参照し、それに 1 を足そうとしている。
+これを実行すると x += 1 の箇所でエラーが発生する。
++UnboundLocalError: local variable ‘x’ referenced before assignment
+
local変数 x が代入前に参照された、とある。これは、f の x を参照するのではなく、新しく別の変数を g 内に作ってしまっているため。
+前述のコードを宣言と代入を便宜上分けて書き直すと次のようになる。var を変数宣言のための構文として擬似的に利用している。
# 注: var は正しい Python の文法ではない。上記参照のこと
+def f():
+ var x # f の local変数 'x' を宣言
+ x = 0 # x に 0 を代入
+ def g(): # f の内部関数 g を定義
+ var x # g の local変数 'x' を宣言
+ # たまたま f にも同じ名前の変数があるが、それとは別の変数
+ x += 1 # x に 1 を加算 (x = x + 1 の糖衣構文)
+ # 加算する前の値を参照しようとするが、まだ代入されていないためエラー
+ g()
+当初の意図を表現するには、次のように書けばよい。
+def f():
+ x = 0
+ def g():
+ nonlocal x ## (*)
+ x += 1
+ g()
+(*) のように、nonlocal を追加する。これにより一つ外側のスコープ (g の一つ外側 = f) で定義されている x を探しに行くようになる。
case - in によるパターンマッチング構文でも、case - when と同じように then が使える (場合によっては使う必要がある)。
then とは使われることは稀だが、Ruby では then がキーワードになっている。次のように使う:
if cond then
+ puts "Y"
+else
+ puts "N"
+end
+このキーワードが現れうる場所はいくつかあり、if、unless、rescue、case 構文がそれに当たる。
+上記のように、何か条件を書いた後 then を置き、式がそこで終了していることを示すマーカーとして機能する。
# Example:
+
+if x then
+ a
+end
+
+unless x then
+ a
+end
+
+begin
+ a
+rescue then
+ b
+end
+
+case x
+when p then
+ a
+end
+普通 Ruby のコードで then を書くことはない。なぜか。次のコードを実行してみるとわかる。
if true puts 'Hello, World!' end
+次のような構文エラーが出力される。
+20:1: syntax error, unexpected local variable or method, expecting `then' or ';' or '\n'
+if true puts 'Hello, World!' end
+ ^~~~
+20:1: syntax error, unexpected `end', expecting end-of-input
+...f true puts 'Hello, World!' end
+二つ目のメッセージは無視して一つ目を読むと、then か ; か改行が来るはずのところ変数だかメソッドだかが現れたことによりエラーとなっているようだ。
ポイントは改行が then (や ;) の代わりとなることである。true の後に改行を入れてみる。
if true
+puts 'Hello, World!' end
+無事 Hello, World! と出力されるようになった。
+then や ; や改行が必要かなぜ then や ; や改行 (以下 「then 等」) が必要なのだろうか。次の例を見てほしい:
if a b end
+then も ; も改行もないのでエラーになるが、これは条件式がどこまで続いているのかわからないためだ。
+この例は二通りに解釈できる。
# a という変数かメソッドの評価結果が truthy なら b という変数かメソッドを評価
+if a then
+ b
+end
+# a というメソッドに b という変数かメソッドの評価結果を渡して呼び出し、
+# その結果が truthy なら何もしない
+if a(b) then
+end
+then 等はこの曖昧性を排除するためにあり、条件式は if から then 等までの間にある、ということを明確にする。
+C系の if 後に来る (/) や、Python の :、Rust/Go/Swift などの { も同じ役割を持つ。
Ruby の場合、プログラマーが書きやすいよう改行でもって then が代用できるので、ほとんどの場合 then は必要ない。
case - in における thenようやく本題にたどり着いた。来る Ruby 3.0 では case と in キーワードを使ったパターンマッチングの構文が入る予定である。この構文でもパターン部との区切りとして then 等が必要になる。
+(現在の) Ruby には formal な形式での文法仕様は存在しないので、yacc の定義ファイルを参照した (yacc の説明は省略)。
https://github.com/ruby/ruby/blob/221ca0f8281d39f0dfdfe13b2448875384bbf735/parse.y#L3961-L3986
+p_case_body : keyword_in
+ {
+ SET_LEX_STATE(EXPR_BEG|EXPR_LABEL);
+ p->command_start = FALSE;
+ $<ctxt>1 = p->ctxt;
+ p->ctxt.in_kwarg = 1;
+ $<tbl>$ = push_pvtbl(p);
+ }
+ {
+ $<tbl>$ = push_pktbl(p);
+ }
+ p_top_expr then
+ {
+ pop_pktbl(p, $<tbl>3);
+ pop_pvtbl(p, $<tbl>2);
+ p->ctxt.in_kwarg = $<ctxt>1.in_kwarg;
+ }
+ compstmt
+ p_cases
+ {
+ /*%%%*/
+ $$ = NEW_IN($4, $7, $8, &@$);
+ /*% %*/
+ /*% ripper: in!($4, $7, escape_Qundef($8)) %*/
+ }
+ ;
+簡略版:
+p_case_body : keyword_in p_top_expr then compstmt p_cases
+ ;
+ここで、keyword_in は文字通り in、p_top_expr はいわゆるパターン、then は then キーワードのことではなく、この記事で then 等と呼んでいるもの、つまり then キーワード、;、改行のいずれかである。
これにより、case - when による従来の構文と同じように、then 等をパターンの後ろに挿入すればよいことがわかった。つまり次の3通りのいずれかになる:
case x
+in 1 then a
+in 2 then b
+in 3 then c
+end
+
+case x
+in 1
+ a
+in 2
+ b
+in 3
+ c
+end
+
+case x
+in 1; a
+in 2; b
+in 3; c
+end
+ところで、p_top_expr には if による guard clause が書けるので、その場合は if - then と似たような見た目になる。
case x
+in 0 then a
+in n if n < 0 then b
+in n then c
+end
+if や case の条件の後ろには then、;、改行のいずれかが必要
+case - in でも then 等が必要になるparse.y を直接読めばよいRuby という言語には複数の実装があるが、それらをスクリプト上からどのようにして programmatically に見分ければよいだろうか。
+Object クラスに定義されている RUBY_ENGINE という定数がこの用途に使える。
上記ページの例から引用する:
+$ ruby-1.9.1 -ve 'p RUBY_ENGINE'
+ruby 1.9.1p0 (2009-03-04 revision 22762) [x86_64-linux]
+"ruby"
+$ jruby -ve 'p RUBY_ENGINE'
+jruby 1.2.0 (ruby 1.8.6 patchlevel 287) (2009-03-16 rev 9419) [i386-java]
+"jruby"
+それぞれの処理系がどのような値を返すかだが、stack overflow に良い質問と回答があった。
+What values for RUBY_ENGINE correspond to which Ruby implementations? より引用:
++++ +
++ + + +RUBY_ENGINE +Implementation ++ +<undefined> +MRI < 1.9 ++ +‘ruby’ +MRI >= 1.9 or REE ++ +‘jruby’ +JRuby ++ +‘macruby’ +MacRuby ++ +‘rbx’ +Rubinius ++ +‘maglev’ +MagLev ++ +‘ironruby’ +IronRuby ++ + +‘cardinal’ +Cardinal +
なお、この質問・回答は 2014年になされたものであり、値は変わっている可能性がある。MRI (aka CRuby) については執筆時現在 (2020/12/8) も 'ruby' が返ってくることを確認済み。
この表にない主要な処理系として、mruby は 'mruby' を返す。
mruby 該当部分のソース より引用:
+/*
+ * Ruby engine.
+ */
+#define MRUBY_RUBY_ENGINE "mruby"
+case - in によるパターンマッチング構文でも、case - when と同じように then が使える (場合によっては使う必要がある)。
then とは使われることは稀だが、Ruby では then がキーワードになっている。次のように使う:
if cond then
+ puts "Y"
+else
+ puts "N"
+end
+このキーワードが現れうる場所はいくつかあり、if、unless、rescue、case 構文がそれに当たる。
+上記のように、何か条件を書いた後 then を置き、式がそこで終了していることを示すマーカーとして機能する。
# Example:
+
+if x then
+ a
+end
+
+unless x then
+ a
+end
+
+begin
+ a
+rescue then
+ b
+end
+
+case x
+when p then
+ a
+end
+普通 Ruby のコードで then を書くことはない。なぜか。次のコードを実行してみるとわかる。
if true puts 'Hello, World!' end
+次のような構文エラーが出力される。
+20:1: syntax error, unexpected local variable or method, expecting `then' or ';' or '\n'
+if true puts 'Hello, World!' end
+ ^~~~
+20:1: syntax error, unexpected `end', expecting end-of-input
+...f true puts 'Hello, World!' end
+二つ目のメッセージは無視して一つ目を読むと、then か ; か改行が来るはずのところ変数だかメソッドだかが現れたことによりエラーとなっているようだ。
ポイントは改行が then (や ;) の代わりとなることである。true の後に改行を入れてみる。
if true
+puts 'Hello, World!' end
+無事 Hello, World! と出力されるようになった。
+then や ; や改行が必要かなぜ then や ; や改行 (以下 「then 等」) が必要なのだろうか。次の例を見てほしい:
if a b end
+then も ; も改行もないのでエラーになるが、これは条件式がどこまで続いているのかわからないためだ。
+この例は二通りに解釈できる。
# a という変数かメソッドの評価結果が truthy なら b という変数かメソッドを評価
+if a then
+ b
+end
+# a というメソッドに b という変数かメソッドの評価結果を渡して呼び出し、
+# その結果が truthy なら何もしない
+if a(b) then
+end
+then 等はこの曖昧性を排除するためにあり、条件式は if から then 等までの間にある、ということを明確にする。
+C系の if 後に来る (/) や、Python の :、Rust/Go/Swift などの { も同じ役割を持つ。
Ruby の場合、プログラマーが書きやすいよう改行でもって then が代用できるので、ほとんどの場合 then は必要ない。
case - in における thenようやく本題にたどり着いた。来る Ruby 3.0 では case と in キーワードを使ったパターンマッチングの構文が入る予定である。この構文でもパターン部との区切りとして then 等が必要になる。
+(現在の) Ruby には formal な形式での文法仕様は存在しないので、yacc の定義ファイルを参照した (yacc の説明は省略)。
https://github.com/ruby/ruby/blob/221ca0f8281d39f0dfdfe13b2448875384bbf735/parse.y#L3961-L3986
+p_case_body : keyword_in
+ {
+ SET_LEX_STATE(EXPR_BEG|EXPR_LABEL);
+ p->command_start = FALSE;
+ $<ctxt>1 = p->ctxt;
+ p->ctxt.in_kwarg = 1;
+ $<tbl>$ = push_pvtbl(p);
+ }
+ {
+ $<tbl>$ = push_pktbl(p);
+ }
+ p_top_expr then
+ {
+ pop_pktbl(p, $<tbl>3);
+ pop_pvtbl(p, $<tbl>2);
+ p->ctxt.in_kwarg = $<ctxt>1.in_kwarg;
+ }
+ compstmt
+ p_cases
+ {
+ /*%%%*/
+ $$ = NEW_IN($4, $7, $8, &@$);
+ /*% %*/
+ /*% ripper: in!($4, $7, escape_Qundef($8)) %*/
+ }
+ ;
+簡略版:
+p_case_body : keyword_in p_top_expr then compstmt p_cases
+ ;
+ここで、keyword_in は文字通り in、p_top_expr はいわゆるパターン、then は then キーワードのことではなく、この記事で then 等と呼んでいるもの、つまり then キーワード、;、改行のいずれかである。
これにより、case - when による従来の構文と同じように、then 等をパターンの後ろに挿入すればよいことがわかった。つまり次の3通りのいずれかになる:
case x
+in 1 then a
+in 2 then b
+in 3 then c
+end
+
+case x
+in 1
+ a
+in 2
+ b
+in 3
+ c
+end
+
+case x
+in 1; a
+in 2; b
+in 3; c
+end
+ところで、p_top_expr には if による guard clause が書けるので、その場合は if - then と似たような見た目になる。
case x
+in 0 then a
+in n if n < 0 then b
+in n then c
+end
+if や case の条件の後ろには then、;、改行のいずれかが必要
+case - in でも then 等が必要になるparse.y を直接読めばよいRust において、プリミティブ型の名前は予約語でない。したがって、次のコードは合法である。
+#![allow(non_camel_case_types)]
+#![allow(dead_code)]
+
+struct bool;
+struct char;
+struct i8;
+struct i16;
+struct i32;
+struct i64;
+struct i128;
+struct isize;
+struct u8;
+struct u16;
+struct u32;
+struct u64;
+struct u128;
+struct usize;
+struct f32;
+struct f64;
+struct str;
+では、普段単に bool と書いたとき、この bool は一体どこから来ているのか。rustc のソースを追ってみた。
++前提知識: 一般的なコンパイラの構造、用語。
+rustcそのものの知識は不要 (というよりも筆者自身がよく知らない)
調査に使用したソース (調査時点での最新 master)
+https://github.com/rust-lang/rust/tree/511ed9f2356af365ad8affe046b3dd33f7ac3c98
+どのようにして調べるか。rustc の構造には詳しくないため、すぐに当たりをつけるのは難しい。
+大雑把な構造としては、compiler フォルダ以下に rustc_* という名前のクレートが数十個入っている。これがどうやら rustc コマンドの実装部のようだ。
rustc はセルフホストされている (= rustc 自身が Rust で書かれている) ので、bool や char などで適当に検索をかけてもノイズが多すぎて話にならない。
+しかし、お誂え向きなことに i128/u128 というコンパイラ自身が使うことがなさそうな型が存在するのでこれを使って git grep してみる。
$ git grep "\bi128\b" | wc # i128
+ 165 1069 15790
+
+$ git grep "\bu128\b" | wc # u128
+ 293 2127 26667
+
+$ git grep "\bbool\b" | wc # cf. bool の結果
+ 3563 23577 294659
+165 程度であれば探すことができそうだ。今回は、クレート名を見ておおよその当たりをつけた。
+$ git grep "\bi128\b"
+...
+rustc_resolve/src/lib.rs: table.insert(sym::i128, Int(IntTy::I128));
+...
+rustc_resolve というのはいかにも名前解決を担いそうなクレート名である。該当箇所を見てみる。
/// Interns the names of the primitive types.
+///
+/// All other types are defined somewhere and possibly imported, but the primitive ones need
+/// special handling, since they have no place of origin.
+struct PrimitiveTypeTable {
+ primitive_types: FxHashMap<Symbol, PrimTy>,
+}
+
+impl PrimitiveTypeTable {
+ fn new() -> PrimitiveTypeTable {
+ let mut table = FxHashMap::default();
+
+ table.insert(sym::bool, Bool);
+ table.insert(sym::char, Char);
+ table.insert(sym::f32, Float(FloatTy::F32));
+ table.insert(sym::f64, Float(FloatTy::F64));
+ table.insert(sym::isize, Int(IntTy::Isize));
+ table.insert(sym::i8, Int(IntTy::I8));
+ table.insert(sym::i16, Int(IntTy::I16));
+ table.insert(sym::i32, Int(IntTy::I32));
+ table.insert(sym::i64, Int(IntTy::I64));
+ table.insert(sym::i128, Int(IntTy::I128));
+ table.insert(sym::str, Str);
+ table.insert(sym::usize, Uint(UintTy::Usize));
+ table.insert(sym::u8, Uint(UintTy::U8));
+ table.insert(sym::u16, Uint(UintTy::U16));
+ table.insert(sym::u32, Uint(UintTy::U32));
+ table.insert(sym::u64, Uint(UintTy::U64));
+ table.insert(sym::u128, Uint(UintTy::U128));
+ Self { primitive_types: table }
+ }
+}
+これは初めに列挙したプリミティブ型の一覧と一致している。doc comment にも、
+++All other types are defined somewhere and possibly imported, but the primitive ones need special handling, since they have no place of origin.
+
とある。次はこの struct の使用箇所を追う。追うと言っても使われている箇所は次の一箇所しかない。なお説明に不要な箇所は大きく削っている。
+ /// This resolves the identifier `ident` in the namespace `ns` in the current lexical scope.
+ /// (略)
+ fn resolve_ident_in_lexical_scope(
+ &mut self,
+ mut ident: Ident,
+ ns: Namespace,
+ // (略)
+ ) -> Option<LexicalScopeBinding<'a>> {
+ // (略)
+
+ if ns == TypeNS {
+ if let Some(prim_ty) = self.primitive_type_table.primitive_types.get(&ident.name) {
+ let binding =
+ (Res::PrimTy(*prim_ty), ty::Visibility::Public, DUMMY_SP, ExpnId::root())
+ .to_name_binding(self.arenas);
+ return Some(LexicalScopeBinding::Item(binding));
+ }
+ }
+
+ None
+ }
+関数名や doc comment が示している通り、この関数は識別子 (identifier, ident) を現在のレキシカルスコープ内で解決 (resolve) する。
+if ns == TypeNS のブロック内では、primitive_type_table (上記の PrimitiveTypeTable::new() で作られた変数) に含まれている識別子 (bool、i32 など) かどうか判定し、そうであればそれに紐づけられたプリミティブ型を返している。
なお、ns は「名前空間」を示す変数である。Rust における名前空間はC言語におけるそれとほとんど同じで、今探している名前が関数名/変数名なのか型なのかマクロなのかを区別している。この if は、プリミティブ型に解決されるのは型を探しているときだけだ、と言っている。
重要なのは、これが resolve_ident_in_lexical_scope() の最後に書かれている点である。つまり、最初に挙げたプリミティブ型の識別子は、「名前解決の最終段階で」、「他に同名の型が見つかっていなければ」プリミティブ型として解決される。
動作がわかったところで、例として次のコードを考える。
+#![allow(non_camel_case_types)]
+
+struct bool;
+
+fn main() {
+ let _: bool = bool;
+}
+ここで main() の bool は struct bool として解決される。なぜなら、プリミティブ型の判定をする前に bool という名前の別の型が見つかるからだ。
Rust のプリミティブ型は予約語ではない。名前解決の最終段階で特別扱いされ、他に同名の型が見つかっていなければ対応するプリミティブ型に解決される。
+]]>:version の一部
++VIM - Vi IMproved 8.2 (2019 Dec 12, compiled Jan 26 2020 11:30:30) +macOS version +Included patches: 1-148 +Huge version without GUI.
+
tac / tailtac や tail -r などの外部コマンドを ! を使って呼び出し、置き換える。
++:h v_!
+
tac コマンドや tail の -r オプションは環境によって利用できないことがあり、複数の環境を行き来する場合に採用しづらい
:g/^/m0こちらは外部コマンドに頼らず、Vim の機能のみを使う。g は :global コマンドの、m は :move コマンドの略
:global コマンドは :[range]global/{pattern}/[command] のように使い、[range] で指定された範囲の行のうち、{pattern} で指定された検索パターンにマッチする行に対して、順番に [command] で指定された Ex コマンドを呼び出す。
++:h :global
+
:move コマンドは [range]:move {address} のように使い、[range] で指定された範囲の行を {address} で指定された位置に移動させる。
++:h :move
+
:g/^/m0 のように組み合わせると、「すべての行を1行ずつ 0行目(1行目の上)に動かす」という動きをする。これは確かに行の入れ替えになっている。
なお、:g/^/m0 は全ての行を入れ替えるが、:N,Mg/^/mN-1 とすることで N行目から M行目を処理範囲とするよう拡張できる。手でこれを入力するわけにはいかないので、次のようなコマンドを用意する。
command! -bar -range=%
+ \ Reverse
+ \ <line1>,<line2>g/^/m<line1>-1
+これは望みの動作をするが、実際に実行してみると全行がハイライトされてしまう。次節で詳細を述べる。
+:g/^/m0 の問題点:global コマンドは各行に対してマッチングを行う際、現在の検索パターンを上書きしてしまう。^ は行の先頭にマッチするため、結果として全ての行がハイライトされてしまう。'hlsearch' オプションを無効にしている場合その限りではないが、その場合でも直前の検索パターンが失われてしまうと n コマンドなどの際に不便である。
++:h @/
+
++[2020/9/28追記] +より簡潔な方法を見つけたので次節に追記した
+
前述した :Reverse コマンドの定義を少し変えて、次のようにする:
function! s:reverse_lines(from, to) abort
+ execute printf("%d,%dg/^/m%d", a:from, a:to, a:from - 1)
+endfunction
+
+command! -bar -range=%
+ \ Reverse
+ \ call <SID>reverse_lines(<line1>, <line2>)
+実行しているコマンドが変わったわけではないが、関数呼び出しを経由するようにした。これだけで前述の問題が解決する。
+この理由は、ユーザー定義関数を実行する際は検索パターンが一度保存され、実行が終了したあと復元されるため。結果として検索パターンが ^ で上書きされることがなくなる。
Vim のヘルプから該当箇所を引用する (強調は筆者による)。
+++:h autocmd-searchpat
+Autocommands do not change the current search patterns. Vim saves the current +search patterns before executing autocommands then restores them after the +autocommands finish. This means that autocommands do not affect the strings +highlighted with the ‘hlsearch’ option.
+
これは autocommand の実行に関しての記述だが、これと同じことがユーザー定義関数の実行時にも適用される。このことは :nohlsearch のヘルプにある。同じく該当箇所を引用する (強調は筆者による)。
++:h :nohlsearch
+(略) This command doesn’t work in an autocommand, because +the highlighting state is saved and restored when +executing autocommands |autocmd-searchpat|. +Same thing for when invoking a user function.
+
この仕様により、:g/^/m0 の呼び出しをユーザー定義関数に切り出すことで上述の問題を解決できる。
++[2020/9/28追記] +より簡潔な方法を見つけたため追記する
+
command! -bar -range=%
+ \ Reverse
+ \ keeppatterns <line1>,<line2>g/^/m<line1>-1
+まさにこのための Exコマンド、:keeppatterns が存在する。:keeppatterns {command} のように使い、読んで字の如く、後ろに続く Exコマンドを「現在の検索パターンを保ったまま」実行する。はるかに分かりやすく意図を表現できる。
++:h :keeppatterns
+
" License: Public Domain
+
+command! -bar -range=%
+ \ Reverse
+ \ keeppatterns <line1>,<line2>g/^/m<line1>-1
+違いはない。ただのエイリアス。
+Vim の autocmd events には似通った名前のものがいくつかある。大抵は :help に説明があるが、この記事のタイトルにある2つを含めた以下のイベントには、その違いについて説明がない。
BufRead/BufReadPostBufWrite/BufWritePreBufAdd/BufCreateこのうち、BufAdd/BufCreate に関しては、:help BufCreate に
++The BufCreate event is for historic reasons.
+
とあり、おそらくは BufAdd のエイリアスであろうということがわかる。他の2組も同様ではないかと予想されるが、確認のため vim と neovim のソースコードを調査した。
++ソースコードへのリンク +vim (調査時点での master branch) +neovim (上に同じ)
+
以下は、autocmd events の名前と内部で使われている整数値とのマッピングを定義している箇所である。見ての通り、上でエイリアスではないかと述べた3組には、それぞれ同じ内部値が使われている。
+https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L85-L86
+ {"BufAdd", EVENT_BUFADD},
+ {"BufCreate", EVENT_BUFADD},
+https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L95-L97
+ {"BufRead", EVENT_BUFREADPOST},
+ {"BufReadCmd", EVENT_BUFREADCMD},
+ {"BufReadPost", EVENT_BUFREADPOST},
+https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L103-L105
+ {"BufWrite", EVENT_BUFWRITEPRE},
+ {"BufWritePost", EVENT_BUFWRITEPOST},
+ {"BufWritePre", EVENT_BUFWRITEPRE},
+neovim の場合でも同様のマッピングが定義されているが、こちらの場合は Lua で書かれている。以下にある通り、はっきり aliases と書かれている。
aliases = {
+ BufCreate = 'BufAdd',
+ BufRead = 'BufReadPost',
+ BufWrite = 'BufWritePre',
+ FileEncoding = 'EncodingChanged',
+ },
+ところで、上では取り上げなかった FileEncoding だが、これは :help FileEncoding にしっかりと書いてある。
*FileEncoding*
+FileEncoding Obsolete. It still works and is equivalent
+ to |EncodingChanged|.
+記事タイトルについて言えば、どちらも変わらないので好きな方を使えばよい。あえて言えば、次のようになるだろう。
+BufAdd/BufCreate
+BufCreate は歴史的な理由により (“for historic reasons”) 存在しているため、新しい方 (BufAdd) を使うBufRead/BufReadPost
+BufReadPre との対称性のため、あるいは BufWritePost との対称性のため BufReadPost を使うBufWrite/BufWritePre
+BufWritePost との対称性のため、あるいは BufReadPre との対称性のため BufWritePre を使うFileEncoding/EncodingChanged
+FileEncoding は “Obsolete” と明言されているので、EncodingChanged を使うところでこの調査で知ったのだが、BufRead と BufWrite は上にある通り発火するタイミングが「後」と「前」で対称性がない。可能なら Pre/Post 付きのものを使った方が分かりやすいだろう。