Upgrade to Pro — share decks privately, control downloads, hide ads and more …

mixi tech note #01

mixi tech note #01

#技術書典6 に出典されたミクシィグループエンジニア有志による技術書です。

当日の詳細はこちら
https://medium.com/mixi-developers/3c1af2525865

<< 目次 >>
1章:非エンジニアのための SQL をたたく環境を整えた話
2章:幸せな開発のために考えていること
3章:GKE で private cluster 化する際に気を付けたい点
4章:The Fun of Extensible
5章:オルタ 3 シミュレータのデバッグ機能開発

<< TECH NOTE 一覧 >>
mixi tech note #01
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-01

mixi tech note #02
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-02

mixi tech note #03
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-03

mixi tech note #04
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-04

mixi tech note #05
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-05

mixi tech note #06
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-06

mixi tech note #07
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-07

MIXI TECH NOTE #08
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-08

XFLAG Tech Note Vol.01
https://speakerdeck.com/mixi_engineers/xflag-tech-note-vol-dot-01

XFLAG Tech Note vol.02
https://speakerdeck.com/mixi_engineers/xflag-tech-note-vol-dot-02

MIXI ENGINEERS

April 14, 2019
Tweet

More Decks by MIXI ENGINEERS

Other Decks in Technology

Transcript

  1. まえがき 本書「mixi tech note #01」は、ミクシィグループに所属する有志達によって執筆・制 作された技術書です。サロンスタッフ直接予約アプリ 「minimo」の開発チームにおける取 り組みや、2019 年 2

    ⽉に発表された⼈⼯⽣命×アンドロイド「オルタ3」4 社共同研究プ ロジェクトで使⽤されている技法など、実際の現場で使われた技術や考え⽅、また、個⼈ 的に興味・関⼼のある分野から、思い思いに執筆いたしました。そのため、各章それぞれで 完結している内容になっていますので、好きな章から好きな順番でお楽しみください。 また、本書は、ミクシィグループにある技術的知⾒やアイデアを積極的に共有・公開して いくことで、世の中により良いサービスが溢れ出すことを願って刊⾏されています。掲載 されている情報は、執筆者⾃⾝の環境で検証し執筆されたものですので、ご参考にされる 際は、ご⾃⾝の責任で判断しご活⽤ください。なお、⽂章表現につきましても、執筆者⾃⾝ の⾔葉で伝えたく、フランクな表現となっておりますことご理解いただければと思います。 ディベロッパーリレーションズチーム⼀同 ◆本書に関するお問い合わせ先   https://twitter.com/mixi_engineers ◆ミクシィグループについて   https://mixi.co.jp/ ※ʠミクシィʡ 、 ʠmixiʡ 、 ʠmixi ロゴʡ は、株式会社ミクシィの商標または登録商標です。 また、各社の会社名、サービス及び製品の名称は、それぞれの所有する商標または登録商 標です。 i
  2. ⽬次 まえがき i 第 1 章 ⾮エンジニアのための SQL をたたく環境を整えた話 1

    1.1 始めに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 minimo の体制とシステム構成 . . . . . . . . . . . . . . . . . . . . . . . . 1 1.3 治安の悪いクエリの登場 . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.4 遊撃チームのとった対応 . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.5 状況の改善と今後の課題 . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.6 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 第 2 章 幸せな開発のために考えていること 9 2.1 幸せの定義 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.2 コードの中の不安 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.3 コードの外の不安 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.4 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 第 3 章 GKE で private cluster 化する際に気を付けたい点 20 3.1 private cluster とは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.2 導⼊⽅法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.3 導⼊時に気を付けたい点 . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 3.4 最後に . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 第 4 章 The Fun of Extensible 25 4.1 Haskell とレコード構⽂ . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 4.2 extensible パッケージ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 4.3 多相バリアント . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 4.4 Do 記法レスプログラミング . . . . . . . . . . . . . . . . . . . . . . . . 35 4.5 YAML 設定を使いこなす . . . . . . . . . . . . . . . . . . . . . . . . . . 37 4.6 終わりに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 ii
  3. ⽬次 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 44 5.1 背景と導⼊

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 5.2 関節オブジェクトの作成 . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 5.3 ギズモによる実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 5.4 ゲームオブジェクトによる実装 . . . . . . . . . . . . . . . . . . . . . . . 53 5.5 最後に . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 著者紹介 67 iii
  4. 第 1 章 ⾮エンジニアのための SQL をたたく環 境を整えた話 1.1 始めに 中⼩規模の事業では、⾮エンジニアが

    SQL を書いて効果測定やデータ分析を⾏うことが 多々あるかと思います。筆者の所属する「minimo 事業部」も全体で 40 ⼈程度の中規模の 組織であり、エンジニアのみならずマーケターやディレクターが SQL を書き、データを集 めています。そのとき必ず問題になるのが、 「治安の悪い激重クエリ」ではないでしょうか。 本章ではそれらを解決するために我々が⾏った対策を紹介します。 1.2 minimo の体制とシステム構成 minimo 事業部は、 「サロンスタッフ直接予約サービス minimo」 *1を作っている部署で す。プロダクトを開発するチームのみならず、マーケティングや営業を⾏うチームもあり ます。まず、minimo 事業部の組織とデータを扱う基盤を紹介します。 minimo のチーム(2019.03.01 現在) minimo 事業部は以下の組織からなっています。 • マーケティンググループ • ビジネスサイドグループ • プロダクト企画グループ – デザインチーム *1 美容室・美容院、ネイルサロン、まつげサロン、エステサロンなどのスタッフに直接予約できるサービス。 https://minimodel.jp/ 1
  5. 第 1 章 ⾮エンジニアのための SQL をたたく環境を整えた話 1.3 治安の悪いクエリの登場 – ディレクションチーム

    • 開発グループ – アプリチーム – 遊撃チーム ビジネスサイドグループは掲載者(サロンスタッフさん) 、マーケティンググループは⼀ 般利⽤者の獲得・満⾜度向上などを主な⽬標としています。そして、プロダクト企画グルー プと開発グループは minimo のプロダクト(アプリや Web 版サービス・サロン向け予約管 理システムなど)を作ります。筆者は開発グループ遊撃チーム所属です。遊撃チームは部 署内のあらゆるチームに対して縛りなくエンジニアリング技術を提供するというチームで、 主にサーバサイドエンジニアが所属しています。業務内容は、プロダクトの開発やインフ ラ周りの保守はもちろん、マーケティンググループやビジネスサイドグループと連携した 施策の実装や調整、また、minimo を担当するカスタマーサポートのチームと連携して管理 ツールの改善やサービス内での不正対策などを⾏っています。 本章での登場⼈物の所属は、遊撃チーム・ディレクションチーム・マーケティングチーム がメインです。 データ分析周りの構成 minimo のインフラは AWS を使⽤しています。データベースは RDS で MySQL を 使⽤し、解析⽤のリードレプリカを⽤意しています。このリードレプリカへは踏み台 サーバを経由してアクセスします。各メンバーがクエリを実⾏するツールとして MySQL Workbench*2の使⽤を推奨しています。また、それとは別のインスタンスで Redash*3を 使⽤しており、先述のリードレプリカを直接たたく以外にもそれを使う⽅法も提供してい ます。書き捨てのクエリは MySQL Workbench を利⽤、定期実⾏するクエリや頻繁に実 ⾏するクエリは Redash を利⽤するという使い分けを⾏っています。 1.3 治安の悪いクエリの登場 さて、minimo ではこれまでに解説したように、ディレクターやマーケターも SQL をた たくことができる構成になっています。それらの利⽤⽅法についてはこれまでサーバサイ ドエンジニアは関与せず、⾃由に使⽤することを許可していました。その結果、以下のよ うな問題が起こるようになってきました。 *2 MySQL が公式で提供している DB の統合ビジュアルツール。SQL の実⾏やスキーマの確認を容易に⾏ える。https://www.mysql.com/jp/products/workbench/ *3 MySQL 等のデータベースや Google Analytics などさまざまなデータソースに対応した OSS のダッシュ ボードツール。https://redash.io/ 2
  6. 第 1 章 ⾮エンジニアのための SQL をたたく環境を整えた話 1.3 治安の悪いクエリの登場 CROSS JOIN

    で阿⿐叫喚 FROM 句にテーブルを 2 つ書くことができる(リスト 1.1)ことを皆さんはご存じで しょうか? その場合、CROSS JOIN という操作が⾏われ、2 つのテーブルの直積をとっ たような⼀時テーブルができあがります。要するに、関係するテーブルのサイズが⼤きく なればなるほど、その⼀時テーブルは⼤きくなっていって地獄のような処理が⾏われます。 リスト 1.1: CROSS JOIN SELECT SUM(table_a.count) - SUM(table_b.count) FROM table_a, table_b WHERE table_a.id = table_b.id AND ’2019-01-01 00:00:00’ <= table_a.created_datetime AND table_a.created_datetime < ’2019-01-01 23:59:59’ たいていの場合 CROSS JOIN を⽤いる必要はなく、INNER JOIN ないし OUTER JOIN を⽤いることで求める処理を⾏うことができます(リスト 1.2) 。 リスト 1.2: INNER JOIN での書き換え SELECT SUM(table_a.count) - SUM(table_b.count) FROM table_a JOIN table_b ON table_a.id = table_b.id WHERE ’2019-01-01 00:00:00’ <= table_a.created_datetime AND table_a.created_datetime < ’2019-01-01 23:59:59’ 誰が⾛らせたかわからない激重クエリ あるころから徐々に解析⽤ DB のレスポンスが遅いとういう苦情が遊撃チームに来るよ うになりました。よく調べてみると、Index の効かないような⾮常に重たいクエリが⾛っ ていることが判明しました。しかし、誰がそのクエリを⾛らせたのかはわからず迷宮⼊り 3
  7. 第 1 章 ⾮エンジニアのための SQL をたたく環境を整えた話 1.4 遊撃チームのとった対応 してしまいました。 1.4

    遊撃チームのとった対応 我々遊撃チームは、前述の問題が起きた原因を以下の 3 点と結論付けました。 • SQL の基礎知識がないユーザーがクエリをたたいてしまっている • 気軽にクエリのレビューを依頼できる場がない • 重いクエリが⾛っていても、本⼈がそれに気付くことができない そこで遊撃チームは以下の対応を⾏いました。 1. クエリをたたくことができるユーザーを集めた Slack チャンネル (#minimo_sql) を 作成 2. minimo SQL 講座を開催 3. 重いクエリが⾛っていたら #minimo_sql チャンネルに通知 #minimo_sql を作成 データのことなら何でもきいてください、というスタンスの channel #minimo_sql を作 成しました。ここでは頻繁にクエリのレビューや、 「このデータを取りたいがどのテーブル を⾒ればよいか」などの質問が⾏われています。 #minimo_sql への依頼時には「どんなデータを」 「何のために取りたいか」を必ず伝えて もらうようにしています。あるデータを取りたいとき、本当にそのアプローチが良いのか、 はたまた Google Analytics などの別のツールでとることが望ましいのではないかを検討す るためです。実際、SQL でデータを取ってくるよりも、アクセスログを AWS Athena で 解析するほうがよい事案などもあり、よい作⽤をもたらしていると感じています。 minimo SQL 講座を開催 SQL の基礎や、minimo 特有の事情などを説明する講座(ハンズオン)を開催しました。 資料中に複数のクエリを記載し、実際に⼿を動かしながらきいてもらう形の講座にしまし た。講座は以下のような内容です。 • データベース・SQL とは何か • データを扱う上での注意点 • minimo のテーブル設計 4
  8. 第 1 章 ⾮エンジニアのための SQL をたたく環境を整えた話 1.4 遊撃チームのとった対応 • 基本的な

    SQL の構⽂ • 知っておくと便利なクエリたち • minimo 特有の注意点 • 課題(数問) (図 1.1) 図 1.1: ハンズオンで実際に⾏った課題例 CROSS JOIN を利⽤しないようにしてほしいこと、⾃信がないクエリは #minimo_sql でレビュー依頼してほしいことなども含まれています(図 1.2) 。このハンズオンは⾮常に 好評で、このときに作成したスライドやテキスト版の資料は最近でも多くのメンバーが活 ⽤をしています。新たに加わった minimo のメンバーにもこの資料は共有していて、今後 もアップデートやハンズオンを開催したいと考えています。 5
  9. 第 1 章 ⾮エンジニアのための SQL をたたく環境を整えた話 1.4 遊撃チームのとった対応 図 1.2:

    まとめのスライド 重いクエリの通知 思いクエリが⾛っていたら Slack に通知してくれる、Heavy Query Notify Baby を作成 しました(図 1.3) *4。 図 1.3: Heavy Query Notify Baby このツールは、定期的に SHOW FULL PROCESSLIST をたたいて⼀定時間以上⾛っ ているクエリがあれば通知するというものです。Go で実装されていて、Redash を動かし ている EC2 インスタンスで実⾏されています。Redash が動いしているインスタンスで楽 *4 Slow Query のほうが⼀般的ですが、Baby の語感に合わせて Heavy としました。バブー。 6
  10. 第 1 章 ⾮エンジニアのための SQL をたたく環境を整えた話 1.5 状況の改善と今後の課題 に動かそうと思うと、Go でバイナリを⽣成するのが楽そうというのが

    Go を採⽤した最も ⼤きな理由です*5。 当初は 5 分以上⾛っているクエリがあれば 5 分おきに通知をするようにしていましたが、 思いの外 5 分以上かかるクエリが多いことが判明したため、現在は 10 分以上⾛っているク エリがあれば 5 分おきに通知をするように変更しています。また、そこで通知されるクエ リをよく⾒ると、特定のテーブルのあるカラムに Index がはられていないことに起因する ものがいくつかあることがわかりました。そこで、解析⽤リードレプリカにのみ Index を 貼るような変更を加え、より快適にデータ分析を⾏うことができるようになりました。 1.5 状況の改善と今後の課題 これまでにお話した 3 つの対応により、minimo メンバーの SQL リテラシーが向上し、 サーバサイドエンジニアがクエリを KILL することがほぼなくなりました。#minimo_sql チャンネルでは、活発にデータ取得の⽅法についての議論も⾏われており、事業全体とし て数値に対する意識が向上したように感じられます。 今後の課題を上げるとすれば、⾮エンジニアが SQL を直接たたく機会を減らすことだ と感じています。たとえば BI ツールの導⼊により SQL を直接たたかなくてはならない機 会を減らしたり、施策の実⾏時に Google Analytics にもイベントを送信し測定できるよう にするなどです。minimo ではこれまでに解説したように、ディレクターやマーケターも SQL をたたくことができる構成になっています。SQL を⾮エンジニアがたたくことによ る弊害は、 「その SQL が正しいと保証できない」ことだと思っています。エンジニアがレ ビューをすれば多くの場合問題がありませんが、レビューを通さずにたたかれたクエリは 正しいとは限りません。しかし、データベースをたたいて取得したデータだから正しいと 盲⽬的に信頼してしまうこともあります。さらに、定期的に実⾏するクエリ(⽉ごとの売 上を取得するクエリなど)を書いたメンバーが minimo を卒業したあと、そのクエリがど のようにその値を取得しているかわからないままクエリがたたかれ続け、メンテナンスさ れないなども⼤きな問題です。このあたりの運⽤については今後模索していきたいと考え ています。 1.6 まとめ minimo では多くのメンバーが SQL をたたき、施策の⽴案や効果測定、事業の将来を考 えるためにデータを取得しています。まだまだ問題はありますが、前出の 3 点の対策によ り効果的にデータを取得・活⽤できるようになったと考えています。対策に取り掛かるま *5 筆者が Go をかいてみたかったというのも理由のひとつです。ちなみに minimo のサーバサイドは Perl (Mojolicious) で実装されています。 7
  11. 第 1 章 ⾮エンジニアのための SQL をたたく環境を整えた話 1.6 まとめ では腰が重かったのですが、始めてみると 4

    ⼈⽇程度の⼯数で前出の対応を⾏うことがで き、⾮常によい改善を⾏うことができたと思っています。 これからさらに minimo を成⻑させていきたいと考えているので、それに伴ったデータ 分析の環境を作っていきたいと考えています。 8
  12. 第 2 章 幸せな開発のために考えていること 皆さんは幸せに開発していますか? 安⼼した気持ちでプロダクトを作り上げています か? もしそうであるなら最⾼ですね。維持していただきたいです。幸せではないというあ なた、⼼中お察し申し上げますが、これから幸せになれる可能性を秘めているわけですか らそれはとても幸せなことですね。ただ機能実装を進めていくだけでは幸福にたどり着く

    のは難しいです。プロダクト開発には幸福を妨げるさまざまな問題が発⽣します。それは コードの中に潜んでいることもあれば、コードの外で起きていることもあります。それら を把握し⼀つ⼀つ向き合うことが幸福への道のりとなっていきます。本章ではプロダクト 開発において、エンジニアが幸せに開発するために何を考え、どう⾏動すべきかという私 の⼀つの考えを書いていきます。 なお、宗教的な話は⼀切ありません。 2.1 幸せの定義 ⼀⼝に幸せと⾔っても内容は個々⼈によって⼤きく変化します。ですので、ソフトウェ ア開発においてエンジニアが幸せと考えることは何か? という点を先に定義していきま しょう。今回は幸せをこのように定義します 「幸せとは、不安を取り除いた状態である」 不安は⼈間の⾏動を⼤きく制限します。 「こうなったらどうしよう」 「うまくいかなかっ たらどうしよう」などのリスクが発⽣したとき、正しい⼿段や最適な⼿法だとわかってい ても⾏動を起こす際の⾜枷となってしまいます。これはプロダクト開発に限ったことでは なく、⽇常のすべてに⾔えることです。 9
  13. 第 2 章 幸せな開発のために考えていること 2.2 コードの中の不安 2.2 コードの中の不安 コードの中には、さまざまな不安があります。というか不安だらけです。実装はエンジ ニアの肝でありながら、本当に動くのか?

    不具合はないのか? などの不安とずっと向き 合っていく必要があるからです。ソフトウェアの形態にも寄りますが、昨今は運⽤を前提 とした開発も多く、その場合は継続的に開発していけるか? という不安も積み重なってい きます。それらの不安に⽴ち向かう⽅法として、変更に耐えうるアーキテクチャを設計し たり、テストコードを書いたりなどがあります。 テストコードを書く 今⽇におけるソフトウェア開発では、テストコードを書くことはスタンダードな⼿法と して認知されてきたかと思います。しかし、次のような理由でテストをスキップするとい う話も多いです。 • リリースが近付いているのでテストコードを書く時間がない • UI 部分や設計の都合上テストコードを書くコストが⾼い これらは短期的な判断として選択せざるを得ないこともありますが、不安への対応を先 送りにしてしまうことになりがちです。 コードを書いていくうえで不安を覚えるのはどのタイミングでしょう。難易度の⾼い仕 様をきちんと実装できるかどうかでしょうか。もちろんそれもあるでしょう。瞬間的な不 安は⼤きいですが、 その実装が正しく動き続けているかというのも⼤きな不安となることが あります。業界によって⼤⼩はありますが、往々にしてプロダクトの要件は常に変化しま す。⼀ヵ⽉前の仕様が変わることも珍しいことではありません。仕様が変わればコードの 修正は必要です。つまり、コードは必ず変化するという前提で書く必要があります。コー ドが変化すると、その周辺のコードに少なからず影響を与えます。場合によっては全然関 係ないと思っていたところに⾶び⽕することもあります。それらをあらかじめ⼈間がすべ て把握するのは経験則からくる職⼈技のようなものであり、⾮常に難易度が⾼いです。 変化し続けるコードというのは、運⽤が発⽣するようなプロダクトを前提として考えら れる事が多いですが、⼀回リリースして終わりのパッケージ型であっても同様のことが⾔ えると筆者は考えています。実際に動くものを確認して、修正していくサイクルが発⽣す るのであればコードは変化し続けると⾔えるでしょう。 実装が既存のシステムに影響を与えてしまうかもしれないという不安が⽣じてしまうと、 極⼒影響を与えない⽅法を模索するでしょう。根本原因を解決すべきとわかっていても、 少しでもリスクを減らし最⼩⼯数で抑えるためにいったん場当たり的な対応を取りがちで 10
  14. 第 2 章 幸せな開発のために考えていること 2.2 コードの中の不安 す。不安な状態を減らすことができないままプロダクトが巨⼤なものに成⻑していくと、 開発効率は低下して、エンジニアのモチベーションを維持することが難しくなっていくで しょう。 テストコードを書くことのメリットは「コードの状態を把握」と「精神的障壁の排除」で

    す。テストそのものが品質を上げてくれるわけではありません。テストコードは現状のプ ロダクトの品質がどんなものなのかを可視化してくれます。この部分が適切に動いている のか、この部分に変更を加えるとどうなるのか、など。品質の可視化は機能追加、リファク タリングの判断材料となり、すばやい意思決定が⾏えます。テストコードがないというの は品質が悪いのではなく、品質がわからないということです。わからないという状態は不 安な気持ちを発⽣させます。 テストコードがあるコードというのは、動き続けているか? という不安をある程度取り 除く効果があります。テストが通っていれば⼤丈夫だろう、通っていないなら直そうとい う明確な判断を下せるからです。 また、テストコードは機能の改修に敏感です。既存に影響を与える実装を⾏ったとき、問 題があると真っ先にアラートを出してくれるのはテストです。実際に動かして⽬視確認す るより先にテストが落ちる、ということも発⽣しうるでしょう。こうなると、コードを書 く時の精神状態に変化が訪れます。筆者は良く実装⽅針を⼯数と難易度で「松⽵梅」と表 現することがあるのですが、 「影響範囲が分からないので、万が⼀を考慮して梅で最⼩限の 実装しよう」から「松で実装してみて、テストが落ちたらそこから影響範囲を出して考え よう」になるのです。これは実装に問題が出てもテストがあるから⼤丈夫だろうという安 ⼼感があるからこそできる⾏動です。なんだか⼤変そうというふわっとした精神的障壁を 取り除くことで、不具合を恐れた保守的な実装からあるべき実装へと持っていけるのはテ ストの⼤きな強みだと考えます。 しかしながら、テストコードがあれば不具合は出ないということはありません。書いて もバグは絶対に出ます。テストコードを話すとき「結局テストコードでは不具合検知をカ バーすることはできない、実際の動作確認が必要になるので⼆度⼿間ではないのか」とい う疑問が出ることは多いです。これは半分正解であり、間違いです。テストとは⼀つの軸 で考えるのではなく、さまざまな視点から考える必要があります。これらを整理したもの を「アジャイルテストの 4 象限」と呼びます。 11
  15. 第 2 章 幸せな開発のために考えていること 2.2 コードの中の不安 図: アジャイルテストの 4 象限

    本章で話しているテストとは、4象限の左下に相当する「チームを⽀援する技術⾯のテ スト」いわゆる TDD のことです。ユニットテストやコンポーネントテストコードを書い ていくことで、開発者の不安を取り除き開発を促進させるのが⽬的です。⼀⽅で不具合が 存在しないか? パフォーマンスに問題はないか? などのテストは右下の「技術⾯で製品を 批評するテスト」です。テストという名前でも「製品を批評するため」と「チームを⽀援す るため」と⽬的がそれぞれ違います。これらはどちらか⽚⽅あればよいというものではな く、両⽅プロダクトを⽀える技術です。よって、⼆度⼿間であるという考えは適切ではあ りません。テストという⾔葉で議論する場合はこの点を意識しておかないと認識に齟齬が 発⽣してしまうので⼗分に気を付けておきましょう。 では、どこまでテストを書くべきでしょうか。冒頭で記載した「UI 部分や設計の都合上 テストコードを書くコストが⾼い」というのは実際その通りで、特に UI 周りなどの画⾯出 ⼒に関する部分のテストは難易度が⾼いです。画⾯出⼒における「正しい状態」を定義す 12
  16. 第 2 章 幸せな開発のために考えていること 2.2 コードの中の不安 るのが難しいからです。たとえば画⾯にユーザー名を「◦◦さん」と表⽰するコードのテ ストを書くことを考えてみます。この場合、画⾯に「◦◦さん」と表⽰されることを確認 するテストを書くことになるでしょう。しかし、 「さん」付けは失礼だということで「◦◦

    様」に変わるとどうでしょうか。もちろんテストも「◦◦様」と出ているテストに改修しな ければなりません。この⽂⾔はこれからも簡単に変わる可能性がありそうですね。つまり、 UI というのは頻繁に変更されやすいのです。これがテストコードのコストが⾼いと⾔われ る⼀因です。どこまでテストを書くべきかというのはテストピラミッドという図で表され ることが多いです。 図: テストピラミッド 基本的にはユニットテスト、結合テスト、UI テストの順で厚めに書いていくことが推奨 されます。先ほどの⽂⾔変更もそうですが、UI 部分はサービスであれゲームであれ技術⾯ ではなくビジネス⾯の属性が強くなります。そのような部分はテストコードで頑張るので はなく、4象限で⾔う右上の「ビジネス⾯で製品を批評するテスト」で⾏うのがよいでしょ 13
  17. 第 2 章 幸せな開発のために考えていること 2.3 コードの外の不安 う。また、UI 部分が多すぎてテストが書けないという場合は、そもそもテストを書けるよ うな設計に寄せていくことが重要です。 2.3

    コードの外の不安 エンジニアが本当にコードだけを書いて終わる開発というのを⾒たことがありません。 きちんと職種間でコミュニケーションを取り、プロダクトが何をしたいのか、その実装が本 当に必要なのかを話し合うなども必ず⾏うことになるでしょう。むしろそれこそが正しい 流れだと筆者は考えています。その専⾨性を持って課題を定義し解決していく、また、専 ⾨性を持つからこそ、エンジニア以外の職種には⾒えない課題を⾒つけ出しチームに伝え ていくことがエンジニアリングです。 とはいえ、そこにはコーディングとはまた違う不安要素が存在します。対⼈リスクの問 題やチームとしての問題など⼀筋縄ではいかないものばかりです。コードの中の不安と向 き合いテストやアーキテクチャを考えたように、コードの外の不安とも向き合いどうすべ きか考えていく必要があります。 ⼼理的安全性の確保 プロダクト開発に限った話ではありませんが、コミュニケーションには対⼈リスクが発 ⽣することがあります。対⼈リスクとは相⼿との関係性に悪い影響を与える可能性のある ⾏動のことです。たとえば機能の内容や施策を考えるとき、ディレクターやプランナーの 発⾔が絶対に正しいということはないでしょう。エンジニア視点から⾒て懸念があったり 考慮漏れがあったりなどはよくあることです。そんなときに私たちは適切に指摘できてい るでしょうか。また、指摘した後も問題なくコミュニケーションを取れているでしょうか。 逆に、他⼈から⾃分の間違いを過剰に強く指摘されたときに、⾃分はその後何事もなく働 くことができるでしょうか。指摘するという⾏為は相⼿を攻撃してしまうこともあるので 対⼈リスクをはらんでいるデリケートな⾏動です。この指摘で相⼿が尻込みしてしまって 相談をしなくなったり、チームとして動きづらくなったりするリスクがあるということで す。仕事なので真摯に受け取り我慢すべきというのも⼀理ありますが、⼈間は理性と感情 を完璧に切り離すことはできません。つらい気持ちの原因となる相⼿との関係性を損なわ ずにチームとして仕事をするのはたいへんです。やがて、極⼒コミュニケーションを取ら ないなど対⼈リスクを避ける⾏動をとり始めてしまうでしょう。私たちがやるべきことは プロダクトの問題を⾒つけ出し解決していくことなのに、それより⼿前の段階で不安を覚 えて進めなくなってしまうのです。この状態のチームは⽣産性が低いと⾔えるでしょう。 チームの⽣産性と関係性の⾼い要因として、 「⼼理的安全性」というものがあります。こ れは⼀⾔で表すと「対⼈リスクを取って⾏動できるか」ということを表します。先ほどの 14
  18. 第 2 章 幸せな開発のために考えていること 2.3 コードの外の不安 ような対⼈リスクを取る選択ができない状態を「⼼理的安全性が低い」と考えます。逆に、 対⼈リスクを取れる状態のことを「⼼理的安全性が⾼い」と考えます。⼼理的安全性が⾼ いことは次のような影響をチームにもたらすと⾔われています。 •

    率直に話すようになる • 考えが明晰になる • 意義ある対⽴が後押しされる • 失敗が緩和される • イノベーションが促される • 組織内の障害でなく⽬標に集中できるようになる • 責任感が向上する では、対⼈リスクを取って⾏動できるとはどういう状態なのでしょう。これは、この⾏ 動で関係性が損なわれることはないだろうと確信が持てる状態だと⾔えるでしょう。仲の 良い友⼈を思い浮かべると良いかもしれません。⻑年付き合いのある友⼈に対して問題を 指摘しても、喧嘩になったりその後疎遠になったりと関係性が崩れてしまうという⼼配は あまりしないでしょう。同じように職場の⼈たちでも、⾃分の弱さを話したり他⼈に対し ての意⾒を素直に話せるような状態になれれば⼼理的安全性が⾼いと⾔えるでしょう。で はチームがそのような状態になるためには何を考えればよいでしょうか。良く上がるのは 「仲良くする・結束を深める」ことで⼼理的安全性を⾼めるという話です。 「飲み会でコミュ ニケーション」 「休⽇もみんなで遊ぶ」などイベントで時間を共有して仲良くするといった ⽅法ですが、それらは⼼理的安全性が⾼いことと直結はしません。相⼿を知るということ は重要ですし、もちろん仲良しに越したことはありませんがビジネスパートナーである以 上それらを強制することは難しいですし、個々⼈の好みもあります。イベント事で⼼理的 安全性を⾼めようとすると、参加したかしていないかが指標となりがちで、参加した⼈と それ以外という新たなグループを⽣むだけです。何より仲良しというのは測りにくいです。 「エンジニアリング組織論への招待*1」では⼼理的安全性と責任の関係を表すマトリック スを定義しています。チームが今どのエリアにいるのかが指標となります。 *1 広⽊⼤地(2018) 『エンジニアリング組織論への招待 〜不確実性に向き合う思考と組織のリファクタリン グ』技術評論社 15
  19. 第 2 章 幸せな開発のために考えていること 2.3 コードの外の不安 不安ゾーン 責任感はあるが⼼理的安全がない状態です。問題意識は強いが対⼈リスクを取れずに、 結果⼀⼈で抱え込んだりしがちです。 ラーニングゾーン

    ⼼理的安全性があるうえで、対⼈リスクを取って問題と向き合っている状態です。全員 がここに⼊ることが理想となります。 このマトリックスを基準として、⾃分たちがどの状態にいて、これからどう動いていくべ きかを議論する⾜がかりとできれば改善のきっかけが⾒えてくるのではないかと思います。 ふりかえることの意義 アジャイルな開発⼿法がかなり普及してきた昨今、 ふりかえりは頻繁に⾏われるようにな りました。継続的な改善をしていく以上重要な要素ではありますが、我々はなぜふりかえ りをしているのでしょう。スクラムというフレームワークに組み込まれているからでしょ うか。ここであらためてふりかえりの意義を考えてみましょう。 ふりかえりを⾏う意義は次の 3 つと考えています。 • ⽴ち⽌まって考えるため • チームを成⻑させるため • プロセスを改善するため ⽴ち⽌まって考えるため 開発においてゴールを定めることは何よりも重要です。何を作ろうとしているのか、誰 のために作ろうとしているのか、どのように作ろうとしているのか、明確になっているか らこそ⼿を進めることができます。では、私たちが正しくゴールに向かっているのかはど うやって確かめるのでしょうか。チームでの開発を⾏うとよくあるのですが、開発に集中 していればいるほど、⾃分たちがどこを⽬指していて今どんな状況なのか⾒えなくなった りするものです。外部から⾒れば⾮効率で、 「なぜそんなことをしているのだろう」と⾸を 傾げたくなることも当たり前のようにやっていたりします。しかしこれは個⼈の能⼒の話 ではなく、誰でも起きうる問題です。 ふりかえりは、⾃分たちがどこにいるのか、正しい⽅向を向いているのかを確認できる 場所でもあります。⾛りながらでは気付くことが難しいので定期的に⽌まって、⼀歩引い 17
  20. 第 2 章 幸せな開発のために考えていること 2.3 コードの外の不安 た視点で状態を⾒れるようにすることが重要です。時折、 「毎週ふりかえりをしても話題が ない」という話を聞きます。定期的なものにせず、問題が起きたらミーティングを開いて 解決していくという⼿段もたしかに可能で効率的に⾒えますが、そこには問題が何である

    か考える場所と時間が存在しません。⽤意されていない以上、問題提起は視野が広くて勘 の良い個⼈の裁量に委ねられてしまい、不定期で不安定なものとなってしまいます。です ので、全員で⽴ち⽌まって全員で考えることが重要です。 チームを成⻑させるため チームの成⻑とはなんでしょうか。個⼈の成⻑とはどう違うのでしょうか。チームの成 ⻑とは、部分最適化から全体最適化へとシフトすることが挙げられます。たとえば、企画 職の⼈がデータを打ち込むタスクを⾏っていたところ、ミスが多発して全体の作業が遅く なるという問題が発⽣したとします。これに対しての改善は、企画職の⼈がマニュアルを 徹底もしくは習熟度を上げてミスを防ぐなどがあります。この 2 つは個⼈の部分を改善す ることでほかの⼈たちには影響を与えずマイナスを補正する部分最適化と⾔えるでしょう。 しかし、この場合はエンジニアが協⼒してデータ⼊⼒を⾃動化するツールを作った⽅が楽 で安全でしょう。エンジニアの作業コストはかかりますが、仮に企画職の⼈が変わったと してもツールを覚えてもらうだけで作業に取り掛かれます。これを全体最適化と⾔います。 良い全体最適化を⾏うためにはチームの関係性の構築が必須です。お互いを気にかけれ ない状態では相⼿が抱える問題に気付けないし、⼼理的安全性が低い状態では問題として 提⽰することも難しいからです。ふりかえりはチームビルディングを⾏う時間として使う のも有効です。お互いが相⼿のやったことを理解し、ありがとうと感謝し尊重していくと ⼼理的安全性を⾼めていくのに役⽴ちます。感謝を⾔い合う時間がなければ、Keep で相⼿ の⾏ったことを書いていくのもよいでしょう。 プロセスを改善するため 開発のプロセスは常にアップデートが必要です。過去に成功したやり⽅が今適切かどう かはわからないので、常にそれがベターであるかを確認していかなければ形骸化していく でしょう。ふりかえりで KPT を出すというのは、現状のやり⽅がどのような価値をもた らしているのか⾒直すタイミングでもあります。すべてのプロセスには理由が存在します。 Keep や Problem を出していくことで、結果として起こったことの背景にどのような戦略、 考え⽅が存在するのか、それが価値を⽣み出していくのか問題となっていくのかという本 質的な部分への⾔及を可能とするのです。ふりかえり初期では Keep や Problem を出すと きに単発的なものばかり出てしまい継続的なことが考えられないという問題が発⽣しがち ですが、それは問題ではなくむしろ⼤事なことだと考えています。単発的な K や P は結 果を書き出していることが多く、その結果には必ず背景が存在するからです。その背景を 18
  21. 第 2 章 幸せな開発のために考えていること 2.4 まとめ たどる材料として単発的な Keep や Problem

    は有効であるのでどんどん出していくべきで す。最初から継続的なものを書き出そうとして詰まってしまう⽅が問題となりがちです。 2.4 まとめ 「幸せとは、不安を取り除いた状態である」 と最初に定義しましたが、開発において不 安がゼロになることはないでしょう。ソフトウェア開発はどこにも存在しないものを⽣み 出そうとしているのですから、未来がわからない不安や作っているものが良いものなのか という不安、チームでビジョンが共有されているかなどさまざまな不安が我々を襲います。 それらは避けようがないので、その不安を理解したうえで、どう減らしていくかという継 続的な改善を⾏うしかないのです。コードの中も外も共通しているのは、⾃分の領域から 少し⾜を延ばして周りを⾒渡すことで、問題解決の⽷⼝が⾒えるということです。この本 が幸せな開発への糧となれば幸いです。 19
  22. 第 3 章 GKE で private cluster 化する際に気を 付けたい点 皆さん

    2018 年 10 ⽉ 2 ⽇に GA された GKE private cluster*1って知っていますか? private cluster を利⽤する利点として以下の点が上げられます。 • Cloud NAT が利⽤可能 • セキュリティの向上 個⼈的には Cloud NAT が利⽤可能になるのが魅⼒的でこの機能について調べました (本 当はこの記事を書くころには⾃プロダクトに導⼊したかった)。Cloud NAT の導⼊を前提 とした調査、本番導⼊にするときに注意すべき点というのを洗い出したので導⼊を検討す る⽅々の⼿助けとなれば良いなと思っています。 3.1 private cluster とは private cluster を有効にするとノードに RFC 1918 の内部 IP が割り当てられます。外 部 IP は割り当てられないので public なインターネットから node への SSH が不可能にな ります。GKE は kube-apiserver などやりとりするためにマスタへの endpoint が存在し ます。ちなみに、下記のコマンドを打てば endpoint が出てきます。 kubectl cluster-info | grep master private cluster ではマスタへの endpoint が 2 つ作られます。⼀つは public なインター ネットからアクセスできるための endpoint (以下 public endpoint)。もう⼀つは VPC ネッ *1 release note: https://cloud.google.com/kubernetes-engine/docs/release-notes#new-features_40 20
  23. 第 3 章 GKE で private cluster 化する際に気を付けたい点 3.2 導⼊⽅法

    トワーク内からしかアクセスできない private な endpoint です (以下 private endpoint)。 public endpoint に対し以下の 3 種類*2の設定が選択できます。 1. public endpoint へのアクセスは不可 2. public endpoint へのアクセスは可能だがホワイトリストに含まれていない IP は 不可 3. public endpoint へのアクセスは可能 public endpoint にアクセスできなくなるとローカル環境から private cluster に対し kubectl が実⾏できなくなるので⾮常にやっかいです。private cluster の導⼊利⽤がセキュ リティの向上⽬的でない限りは 3 を選ぶのが無難です。1 を選ぶと public endpoint にア クセスできないので VPC 内に踏み台サーバを構築し、踏み台から private endpoint にア クセスする必要があります。2 を選ぶと CI/CD で利⽤しているサービスの IP や社内 IP を洗い出し、それをホワイトリストに追加する必要があります。 3.2 導⼊⽅法 執筆時点 (2019/03/08) では既存の Cluster を private cluster に変更することはできな いので新しく Cluster を作る必要があります。 Web コンソールの場合 詳細オプション→ネットワーキングに移動し図 3.1 のように • VPC ネイティブを有効 • 限定公開クラスタ にチェックを⼊れます。マスタ IP 範囲欄には RFC 1918 範囲でサブネットは/28 のも のを VPC 内のサブネットと重複しない IP アドレスを記⼊してください。 *2 Access to the cluster endpoints: https://cloud.google.com/kubernetes-engine/docs/how- to/private-clusters#access_to_the_cluster_endpoints 21
  24. 第 3 章 GKE で private cluster 化する際に気を付けたい点 3.2 導⼊⽅法

    図 3.1: console をキャプチャした図 gcloud コマンドの場合 gcloud container clusters create <NAME> \ (中略) --enable-private-nodes \ --master-ipv4-cidr <MASTER_IPV4_CIDR> \ --no-enable-master-authorized-networks \ --enable-ip-alias • --enable-private-nodes – クラスタ構築時、node に外部 IP は割り当てられません • --master-ipv4-cidr=MASTER_IPV4_CIDR – マスタネットワークに使⽤する IPv4 CIDR(RFC 1918) 範囲で        22
  25. 第 3 章 GKE で private cluster 化する際に気を付けたい点 3.3 導⼊時に気を付けたい点

    サブネットは/28 で指定する必要があります。この指定したサブネット範囲の IP が private endpoint に割り振られます。VPC 内のサブネットと重複しない ように気をつけてください。 • --no-enable-master-authorized-networks – public endpoint に対してアクセス制限を⾏いません • --enable-ip-alias – VPC ネイティブを有効にする 上記のような設定を⾏うことで private cluster を構築できます。なお、gcloud コマンド で"(中略)"と書かれている部分はリージョン・OS の種類・認証⽅法など設定が細かく変わ る部分なので中略しました。 3.3 導⼊時に気を付けたい点 継続的デリバリ (CD) に気を付ける すでに継続的デリバリを構築済みの場合、あなたの Project はどこかしらで kubectl apply -f hoge.yaml などのコマンドを使ってデプロイを⾏っているはずです。private cluster の設定を public endpoint へのアクセスが無効にする場合は kubectl のコマンドをたたく際必ず踏み 台を経由するような設定へ⾒直しが必要です。public endpoint へのアクセスが可能だがホ ワイトリスト外での IP アドレスはアクセスを無効にする場合は CD の IP アドレスをホ ワイトリストに追加しましょう。執筆時点 (2019/03/08) では Google Cloud Build(以下 GCB) も上記の⼿段を踏まなければいけません。GCB 経由で private cluster にアクセス できないという議論は stack overflow にも*3上がっています。 Docker Registry に気を付ける Google Container Registry(以下 GCR) 以外の Docker Registry を利⽤している場合 image pull で失敗する可能性があります。あなたがすでに Docker Hub を利⽤しており、 GCR への移⾏が難しい場合は GCR に Docker Hub のミラーを作りそこからフェッチし *3 https://stackoverflow.com/questions/51944817/google-cloud-build-deploy-to-gke-private- cluster 23
  26. 第 3 章 GKE で private cluster 化する際に気を付けたい点 3.4 最後に

    てくる⽅法*4があるので利⽤すべきです。 インターネットへの送信アクセスの許可 private cluster はインターネットへの送信アクセスは許可されていません。インター ネットへの送信アクセスを許可したい場合は Cloud NAT*5を導⼊するか GCE を利⽤して 独⾃に NAT ゲートウェイを作る*6必要があります。特別な理由がない限りは Cloud NAT の導⼊をお勧めします。Cloud NAT は network virtualization stack である Andromeda*7 が NAT を担っています。そのため、GKE~外部宛先への間に choke points が存在せず可 ⽤性、スループットなどで NAT ゲートウェイより勝っています。 cloud shell は特別ではない cloud shell から private endpoint をたたけないかなと思ったんですが、ムリでした。 ちなみに GKE のドキュメントには cloud shell の endpoint をホワイトリストに追加し、 public endpoint にアクセスする⽅法が書いてあります*8 3.4 最後に 導⼊したら動かなくなるのではないか? のような漠然とした不安は取り除けたら幸 いです。実際に運⽤しないと⾒えてこない課題というのはありそうですので、それらは Twitter(@soiya1919) のほうで発信できたらな... と思ってます。 *4 Fetch images from Container Registry’s mirror: https://cloud.google.com/container- registry/docs/using-dockerhub-mirroring#fetch_images_froms_mirror *5 https://cloud.google.com/nat/docs/gke-example *6 https://cloud.google.com/vpc/docs/special-configurations#multiple-natgateways *7 https://cloudplatform.googleblog.com/2014/04/enter-andromeda-zone-google-cloud-platforms- latest-networking-stack.html *8 Using Cloud Shell to access a private cluster: https://cloud.google.com/kubernetes- engine/docs/how-to/private-clusters#cloud_shell 24
  27. 第 4 章 The Fun of Extensible 本稿では筆者の推しプログラミング⾔語 Haskell*1 で愛⽤している

    extensible パッ ケージ*2の⼀⾵変わった使い⽅をまとめます。extensible パッケージを利⽤することで Haskell の 3 ⼤残念ポイント (要出典) のひとつ「レコード構⽂」をエレガントにしてくれ ます。ですので、まずはその残念なレコード構⽂について紹介し、そのあとに extensible パッケージの基本的な使い⽅を説明します。そして後半では、extensible パッケージの ⼀⾵変わった使い⽅ (?) を紹介します。また、本稿に載せているサンプルコードは GitHub の matsubara0507/fun-of-extensible リポジトリで管理しています*3。ちなみに、本 稿執筆時でのパッケージのバージョンは extensible-0.5 です。最後に注意事項として、 extensible パッケージを作ったのは筆者ではありません。 4.1 Haskell とレコード構⽂ Haskell では data 宣⾔を⽤いて独⾃の型を定義できます: -- 曜⽇を表す型 : -- ‘|‘ でいわゆる enum のように列挙できる data Weekday = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday *1 Haskell Language : https://www.haskell.org/ *2 extensible - Hackage : http://hackage.haskell.org/package/extensible *3 https://github.com/matsubara0507/fun-of-extensible/tree/techbookfest-6 25
  28. 第 4 章 The Fun of Extensible 4.1 Haskell とレコード構⽂

    -- 図形を表す型 : -- ‘Rect‘ などを値コンストラクタと呼ぶ -- その右側に構成要素となる他の型を列挙できる data Figure = Rect Double Double | Square Double | Circle Double -- Optional 型 : -- ‘a‘ はいわゆるジェネリクスの型パラメーター -- 型パラメーターを値コンストラクタの要素にできる data Optional a = Some a | None -- ⾃然数型 : -- 再帰的な定義も可能 data Nat = Zero | Succ Nat 上述で⽰した「書き⽅」を⽤いて、よくあるユーザーアカウントのような単純な型を考え てみます: data User = User Int Text Bool 1 つ⽬の Int がユーザー ID を、2 つ⽬の Text がユーザー名を、3 つ⽬の Bool が Admin ユーザーかどうかのフラグを表しているとします。お察しの通り、これだけではそ れぞれの型が何を表しているかわかりませんね。多くのプログラミング⾔語ではこのよう な型にはフィールド名を付けます。例にも漏れず Haskell にも同様な記法があります: data User = User { id :: Int , name :: Text , admin :: Bool } このフィールド名は関数として利⽤できます。Haskell のデファクトスタンダードな処 理系である GHC*4 の REPL である GHCi で実際に試してみます: *4 The Glasgow Haskell Compiler - Haskell.org : https://www.haskell.org/ghc/ 26
  29. 第 4 章 The Fun of Extensible 4.1 Haskell とレコード構⽂

    > user = User 123 "hoge" False > name user "hoge" > :t name name :: User -> Text Haskell の関数適⽤は空⽩区切で⾏います。また、GHCi で :t 特殊コマンドを使うこ とで、関数や値の型を出⼒します。name :: User -> Text という結果より、name は User 型をひとつ受け取り Text 型を返す関数ということがわかります。 さて、ここで Slack*5 のようなチャットツールを考えると仮定し、メッセージを表す型 を書いてみます: data Message = Message { id :: Int , body :: Text , author :: Int -- User ID } フィールド名 (とコメント) のおかげで特に説明はいりませんね。しかし、これらのコー ドのコンパイルは通りません: Sample.hs:12:5: error: Multiple declarations of ʞidʟ Declared at: src/Sample.hs:6:5 src/Sample.hs:12:5 | 12 | { id :: Int | ^^ このメッセージは id という名前の関数が⼆重で定義されているというものです。 Haskell で同名の関数をひとつのモジュール内に複数定義することはできません。そして、 フィールド名は関数として定義されてしますので、同名のフィールド名を定義できないの です。故に Haskell ではよく、フィールド名の接頭辞に型名を書いたりします: *5 Slack: Where work happens : https://slack.com 27
  30. 第 4 章 The Fun of Extensible 4.2 extensible パッケージ

    data User = User { userId :: Int , userName :: Text , userAdmin :: Bool } data Message = Message { messageId :: Int , messageBody :: Text , messageAuthor :: Int -- User ID } ダサいですね。このように、Haskell のレコード構⽂にはいくつか残念な点がありま す*6。 4.2 extensible パッケージ 前述した通り、Haskell の残念なレコード構⽂を解決するためにさまざまな⼿法が試され ています。その中で、個⼈的にかなり満⾜しているのが extensible パッケージを利⽤し た⽅法です。たとえば、前節で記述した User 型と Message 型を extensible を⽤いて 記述すると次のようになります: type User = Record ’[ "id" >: Int , "name" >: Text , "admin" >: Bool ] type Message = Record ’[ "id" >: Int , "body" >: Text , "author" >: Int ] type 宣⾔はいわゆる型エイリアスです。型に別名を与える時に使います。とりあえず雰 囲気で同じ「もの」を表現していることは伝わると思います。この型を使うには次のよう にします: *6 レ コ ー ド 構 ⽂ の 問 題 に 関 し て は 、す で に 議 論 さ れ て お り 対 策 を と り 始 め て は い ま す : https://gitlab.haskell.org/ghc/ghc/wikis/records 28
  31. 第 4 章 The Fun of Extensible 4.2 extensible パッケージ

    > import Data.Extensible (nil, (<:), (@=)) > import Lens.Micro ((^.)) > user = #id @= 123 <: #name @= "hoge" <: #admin @= False <: nil :: User > user ^. #id 123 > user ^. #name "hoge" Data.Extensible が extensible パ ッ ケ ー ジ の モ ジ ュ ー ル で 、Lens.Micro は microlens というパッケージのモジュールです。#id や #name のようなシャープを ⽤いた記法は、Haskell のデファクトスタンダードな処理系である GHC の拡張機能*7を利 ⽤しています。それは OverloadedLabels と⾔います。この拡張ラベルは関数や値とは 別のネームスペースであり、複数の型で利⽤しても問題ありません。 拡張可能レコード 前節により、 extensible パッケージを使うことで関数の名前空間を汚染しないレ コード型を記述・利⽤できることがわかりました。ただ、これだけでは extensible パッ ケージが extensible (拡張可能) という名前なのかがわからないと思います。本節では extensible パッケージの「拡張可能」な部分を紹介します。 前節と同様にチャットツールを考えましょう。メッセージを親にもつ「スレッド」の型 を定義してみます: type ThreadMessage = Record ’[ "parent" >: Maybe Int -- Message ID , "id" >: Int , "body" >: Text , "author" >: Int -- User ID ] GHCi を使って値を定義してみましょう: > user1 = #id @= 123 <: #name @= "hoge" <: #admin @= False <: nil :: User > :{ *7 GHC ⾔語拡張などと呼ばれており、OverloadedLabels のほかにもさまざまな種類があります : https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html 29
  32. 第 4 章 The Fun of Extensible 4.2 extensible パッケージ

    | message1 :: Message | message1 = | #id @= 12345 <: #body @= "hello" <: #author @= (user1 ^. #id) <: nil | | message1 :: ThreadMessage | message2 = | #parent @= Just (message1 ^. #id) <: #id @= 12346 | <: #body @= "world" <: #author @= (user1 ^. #id) <: nil | :} > message2 ^. #body "world" さて、ここから「拡張可能」の本領発揮です。Message 型の値である message1 を拡張 して、ThreadMessage 型の値にしてみます: >>> message1’ = #parent @= Nothing <: message1 :: ThreadMessage >>> :t message1’ message1’ :: ThreadMessage >>> message1’ ^. #parent Nothing 簡単ですね。しかしこれは、先頭に値を追加するときにしか使えません。たとえば、 フィールドを次のように並び替えましょう: type ThreadMessage = Record ’[ "id" >: Int , "body" >: Text , "author" >: Int -- User ID , "parent" >: Maybe Int -- Message ID ] ThreadMessage 型をこのように定義した場合、前述したように値を定義することはでき ません: > message1’ = #parent @= Nothing <: message1 :: ThreadMessage <interactive>:17:13: error: • Couldn’t match type ʞ’Data.Extensible.Internal.Missing "parent"ʟ with ʞ’Data.Extensible.Internal.Expecting 30
  33. 第 4 章 The Fun of Extensible 4.2 extensible パッケージ

    (n0 ’Data.Extensible.Internal.:> v20)ʟ arising from the overloaded label ʞ#parentʟ • In the first argument of ʞ(@=)ʟ, namely ʞ#parentʟ In the first argument of ʞ(<:)ʟ, namely ʞ#parent @= Nothingʟ In the expression: #parent @= Nothing <: message1 :: ThreadMessage なぜこうなるのかというと、それは extensible によるレコード型の内部実装に由来し ます。実は extensible のレコード型はしくみとして型レベルの線形リストを利⽤してい ます。そのため、リストの [1,2,3] と [2,1,3] が違うように、フィールドの順番を⼊れ 替えた型は別の型になってしまいます。試しに、Message 型の値をフィールドの順番を⼊ れ替えて定義してみましょう: > :{ | message1 :: Message | message1 = | #body @= "hello" <: #id @= 12345 <: #author @= (user1 ^. #id) <: nil | :} <interactive>:13:12: error: • Couldn’t match type ʞ’Data.Extensible.Internal.Missing "body"ʟ with ʞ’Data.Extensible.Internal.Expecting (n0 ’Data.Extensible.Internal.:> v20)ʟ arising from the overloaded label ʞ#bodyʟ • In the first argument of ʞ(@=)ʟ, namely ʞ#bodyʟ In the first argument of ʞ(<:)ʟ, namely ʞ#body @= "hello"ʟ In the expression: #body @= "hello" <: #id @= 12345 <: #author @= (user1 ^. #id) <: nil :: Message 型エラーとなりますね。そして、前述した message1 を拡張する⽅法はリスト操作の コンス (リストの先頭に要素を⼀つだけ⾜す操作) を⾏っています。ですので、追加した フィールドを後ろに持ってきた場合はうまくいかないのです。 では、先頭以外にフィールドを増やした場合は「拡張」する⽅法がないかというと、もち ろんそんなことはありません。shrinkAssoc という関数を使います: > import Data.Extensible (shrinkAssoc) > message1’ = shrinkAssoc $ #parent @= Nothing <: message1 :: ThreadMessage > :t message1’ message1’ :: ThreadMessage 31
  34. 第 4 章 The Fun of Extensible 4.2 extensible パッケージ

    > message1’ ^. #parent Nothing 簡単ですね。 拡張可能バリアント 代数的データ型を考えるとき、いわゆる enum 型のようなもの含まれます。extensible パッケージでは、この enum 型のような型に含まれる値を列挙する型も拡張可能にできま す。それを拡張可能バリアント*8と呼び、次のように定義します: type Figure = Variant ’[ "rect" >: (Double, Double) , "square" >: Double , "circle" >: Double ] これは最初の⽅に定義した図形を表した型です。図形ではなく、四⾓形だけの型を定義 してみます: type Tetragon = Variant ’[ "rect" >: (Double, Double) , "square" >: Double ] さぁ、この Tetragon 型の値を Figure 型の値に拡張してみましょう: > import Data.Extensible (embedAssoc, spreadAssoc) > fig1 = embedAssoc $ #rect @= (2.0, 3.0) :: Tetragon > :t fig1 fig1 :: Tetragon > fig1’ = spreadAssoc fig1 :: Figure > :t fig1’ fig1’ :: Figure *8 バリアントという⽤語は Haskell ではあまり使われず、OCaml のような ML 系⾔語で使われます 32
  35. 第 4 章 The Fun of Extensible 4.3 多相バリアント embedAssoc

    関数を⽤いることで extensible パッケージのバリアントを定義し、 spreadAssoc 関数により型の拡張を⾏っています。すごい簡単ですね。 4.3 多相バリアント 多相バリアント*9とは何かと⾔うと、たとえば次のようなパターンマッチがあったとし ます: countSameEdga fig = case fig of Rect _ _ -> Just 2 Square _ -> Just 1 _ -> Nothing さて、この countSameEdga 関数の型はなんでしょうか? 拡張可能なバリアントのない プレーンな Haskell であれば Figure -> Maybe Int と推論されるでしょう。しかし、こ のような関数であれば Tetragon 型のような型も引数として取れても不思議ではありませ ん。はい、extensible なら可能です。まずは extensible パッケージのバリアント⽤の パターンマッチを利⽤して、Tetragon 型にのみ対応した countSameEdga 関数を定義し てみましょう: countSameEdga :: Tetragon -> Int countSameEdga = matchField $ #rect @= const 2 <: #square @= const 1 <: nil extensible のバリアントに対するパターンマッチを記述するには matchField という 関数を使います。次にこれを Figure 型も受け取れるような多相な関数にしてみましょう: type PolygonFields = ’[ "rect" >: (Double, Double) , "square" >: Double ] countSameEdga :: *9 多相バリアントも同様に OCaml などで利⽤されている機能です 33
  36. 第 4 章 The Fun of Extensible 4.3 多相バリアント (Generate

    xs, PolygonFields ⊆ xs) => Variant xs -> Maybe Int countSameEdga = matchFieldWithMaybe $ #rect @= (const 2 :: (Double, Double) -> Int) <: #square @= (const 1 :: Double -> Int) <: nil matchFieldWithMaybe :: forall xs ys h r . (Generate ys, xs ⊆ ys) => RecordOf (Match h r) xs -> VariantOf h ys -> Maybe r matchFieldWithMaybe pat = matchWith func (wrench pat) where func :: forall x . Nullable (Field (Match h r)) x -> Field h x -> Maybe r func fx gx = (\x -> runMatch (getField x) $ getField gx) <$> getNullable fx はい、いきなり複雑怪奇な型が出てきましたね。まずは countSameEdga 関数の型を⾒ てみましょう。(=>) の左側は型クラスによる制約です。型クラスの役割は、ほかの⼀般 的なプログラミング⾔語における「インタフェース」に近いです*10。Generate xs をい わゆるインタフェースの⽂脈でいうと「型パラメータ xs は Generate インタフェースを 実装していないといけない」となります (Generate がなんなのかはここでは割愛)。また、 PolygonFields ⊆ xs は 2 引数 (ここでは PolygonFields と xs) をとる型クラスで、こ の制約も満たす必要があります。ここで xs は PolygonFields のようなフィールド (バ リアント) の型レベルリストです。⊆ は数学的な意味合いと同じで、これら 2 つのフィー ルドリストが包含関係にあることを制約として求めます。matchFieldWithMaybe 関数は matchField を多相バリアント⽤に実装し直したものです。フィールドがマッチしない場 合は Nothing を返します (matchFieldWithMaybe のしくみはまぁまぁ複雑なので割愛し ます)。最後に試してみます: > fig1 = embedAssoc $ #rect @= (2.0, 3.0) :: Tetragon > countSameEdga fig1 Just 2 > fig1’ = embedAssoc $ #rect @= (2.0, 3.0) :: Figure > countSameEdga fig1’ Just 2 > fig2 = embedAssoc $ #circle @= 3.0 :: Figure > countSameEdga fig2 Nothing *10 役割が近いというだけで、本質的なところは違うので注意してください 34
  37. 第 4 章 The Fun of Extensible 4.4 Do 記法レスプログラミング

    4.4 Do 記法レスプログラミング Haskell で外部⼊出⼒などを扱う時には次のように書きます: main :: IO () main = do putStrLn "please input your name: " name <- getLine putStrLn $ "Hi! " <> name <> "!" これは Do 記法と呼ばれる糖⾐構⽂です。putStrLn や getLine のような IO アクショ ンを上から順々に実⾏してくれます。ちなみに、これの実⾏結果は次のようになります: $ stack exec sample-app please input your name: nobutada Hi! nobutada! この main 関数は各アクションが⼀つ前のアクションを実⾏してあることを期待して います。この「どのアクションの前に何のアクションが実⾏してほしいか」というのを extensible パッケージの「タングル」と呼ばれるしくみを利⽤して記述します。そうす ることで、Do 記法を省いてみるので「Do 記法レスプログラミング」です (もちろん Do 記法は糖⾐構⽂ですので、デシュガーすればそれだけで Do 記法レスできますが)。ちなみ に、 「Do 記法レスプログラミング」というのは僕が勝⼿に呼び出した造語です。 まずはアクションの型を extensible パッケージのレコード型で定義します: type Main = Record MainFields type MainFields = ’[ "displayRequest" >: () , "readName" >: String , "displayName" >: () ] フィールドの⼀つ⼀つが main で実⾏したい各アクションになります。各アクションの 振る舞いは専⽤の型クラスを作って定義します: 35
  38. 第 4 章 The Fun of Extensible 4.4 Do 記法レスプログラミング

    class RunMain kv where run :: proxy kv -> TangleT (Field Identity) MainFields IO (AssocValue kv) instance RunMain ("displayRequest" >: ()) where run _ = lift $ putStrLn "please input your name: " run 関数の proxy kv という型の引数は型クラスメソッドの振る舞いをディスパッチす るためだけに使います。ディスパッチするのにしか使わないため具体的な値は必要ありませ ん。そのため proxy kv という型になっています (たとえば Nothing :: Maybe Int で も良い)。複雑なのは TangleT (Field Identity) MainFields IO (AssocValue kv) という型ですね。TangleT h xs m a というのは extensible パッケージに定義された 型です。h は普通に extensible パッケージのレコード型を使う場合は Field Identity で固定です。xs は依存したいレコードのフィールドリスト、m は内部で使いたい Monad m、a は最終的な結果です。今回は IO を実⾏したいので m は IO です。結果 a として取得 したいのは "displayRequest" >: () の右側ですので、AssocValue kv としています (たとえば AssocValue ("displayRequest" >: ()) は () となる)。また、インスタン スの定義のところで lift を使っているのは IO アクションである putStrLn を TangleT (Field Identity) MainFields IO () に持ち上げるためです。 さて、残りのインスタンスも定義しましょう。"readName" や "displayName" は "displayRequest" と異なり、依存関係があります。前に特定のフィールドを実⾏し てほしい場合には lasso という関数を使います: instance RunMain ("readName" >: String) where run _ = lasso #displayRequest >> lift getLine instance RunMain ("displayName" >: ()) where run _ = lasso #readName >>= \name -> lift (putStrLn $ "Hi! " <> name <> "!") lasso 関数にラベルを渡すことで、そのラベルに対応するフィールドのアクションを実 ⾏した結果を取得できます。これにより、"displayName" の前には "readName" の実⾏ が必要で、"readName" の前には "displayRequest" の実⾏が必要になります。これで、 main 関数全体のアクションの依存関係を、アクションごとに記述できました! おもしろ いですね。 ちなみに、(>>) や (>>=) は Monad ⽤の⾼階関数ですね。前者は前のアクションの実⾏ 結果を無視し、後者は結果を次のアクションの引数として渡します: 36
  39. 第 4 章 The Fun of Extensible 4.5 YAML 設定を使いこなす

    (>>) :: Monad m => m a -> m b -> m b (>>=) :: Monad m => m a -> (a -> m b) -> m b 最後にこれらを組み合わせて main 関数に渡してあげる必要があります: main :: IO () main = runTangles tangles (wrench emptyRecord) >> pure () tangles :: Comp (TangleT (Field Identity) MainFields IO) (Field Identity) :* MainFields tangles = htabulateFor (Proxy :: Proxy RunMain) $ \m -> Comp $ Field . pure <$> run m まぁここの説明は割愛します。ちなみに、レコードのフィールド順に実⾏しているわけ じゃないです。試しに、MainFields の "readName" と "displayName" を⼊れ替えても 同じように動作します。 何がうれしいのか 僕もあんまりよくわかってません (ぇ)。Haskell のアプリケーションコードを書いた場 合、main 関数が膨⼤になる傾向があり、各アクションの依存関係は平べったい⼿続き的 なものになってしまいます。これらを型クラスとして、アクション単位でコントロールで きると、何かおもしろいかもしれない。という程度です。これで何かおもしろいフレーム ワークとか DSL とか作れないかしら。 4.5 YAML 設定を使いこなす extensible パッケージのレコード型は aeson*11 の型クラスのインスタンスになって います。aeson は JSON のデコード・エンコードを⾏ってくれるデファクトスタンダード なパッケージです。すなわち、レコード型は簡単に JSON へのデコード・エンコードを⾏ うことができるのです: *11 aeson - Hackage : http://hackage.haskell.org/package/aeson 37
  40. 第 4 章 The Fun of Extensible 4.5 YAML 設定を使いこなす

    > import Data.Aeson (encode) > user1 = #id @= 123 <: #name @= "hoge" <: #admin @= False <: nil :: User > encode user1 "{\"admin\":false,\"name\":\"hoge\",\"id\":123}" YAML のデコード・エンコードには yaml*12 パッケージを使う。yaml パッケージでは aeson を利⽤しており、aeson による JSON のデコード・エンコードができれば YAML のデコード・エンコードもできるようになる。たとえば設定ファイルのような型を定義 する: type Config = Record ’[ "columns" >: Int , "languageExtensions" >: [String] ] 何かエディタの設定ファイルのようなものをイメージしました。さて、たとえば次のよ うな YAML を読み込むとしよう: # template/config.yaml columns: 80 languageExtensions: [] 試しに GHCi で読み込んでみよう: > import Data.Yaml > decodeFileThrow "./stack.yaml" :: IO Config resolver @= "lts-12.21" <: packages @= ["."] <: extra-deps @= Nothing <: nil 簡単ですね。 *12 yaml - Hackage : http://hackage.haskell.org/package/yaml 38
  41. 第 4 章 The Fun of Extensible 4.5 YAML 設定を使いこなす

    デフォルト値を埋め込む デフォルト値を定義してみましょう。ただし、 Haskell のコード上ではなく外部の YAML ファイルで記述し、コンパイル時に埋め込むようにしてみます。このようにコンパイル 時に外部ファイルを取り込んだりするにはメタプログラミングを⾏う必要があります ね。Haskell にも Template Haskell*13 と呼ばれるメタプログラミング機能があります。 Template Haskell を⽤いることで次のように書けます: {-# LANGUAGE TemplateHaskell #-} import Data.Yaml.TH (decodeFile) defaultConfig :: Config defaultConfig = $$(decodeFile "./template/.config.yaml") GHCi で試してみます: > defaultConfig columns @= 80 <: languageExtensions @= [] <: nil もちろん、埋め込む YAML ファイルが間違っていると、コンパイル時にエラーとなり ます: $ cat template/config.yaml column: 80 languageExtensions: [] rce-dirty specified) work-0.1.0.0: build (lib + exe) Preprocessing library for work-0.1.0.0.. Building library for work-0.1.0.0.. [1 of 6] Compiling Config ( src/Config.hs) [./template/config.yaml changed] /Users/nobutada.matsubara/git/haskell/work/src/Config.hs:22:20: error: • Exception when trying to run compile-time code: *13 https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#template- haskell 39
  42. 第 4 章 The Fun of Extensible 4.5 YAML 設定を使いこなす

    AesonException "Error in $: expected Int, encountered Null" Code: decodeFile "./template/config.yaml" • In the Template Haskell splice $$(decodeFile "./template/config.yaml") In the expression: $$(decodeFile "./template/config.yaml") In an equation for ʞdefaultConfigʟ: defaultConfig = $$(decodeFile "./template/config.yaml") | 22 | defaultConfig = $$(decodeFile "./template/config.yaml") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ すばらしい! 部分的にデフォルト値を選択 最後に⾜りない部分だけをデフォルト値に置き換えるような、簡易的な CLI ツールを実 装しようと思います。このツールは次のように動作するのを想定します: $ stack exec -- pconfig columns @= 80 <: languageExtensions @= [] <: nil $ cat config.yaml columns: 90 $ stack exec -- pconfig config.yaml columns @= 90 <: languageExtensions @= [] <: nil $ cat config.yaml $ stack exec -- pconfig config.yaml columns @= 80 <: languageExtensions @= [] <: nil さて、どうすれば良いだろうか。実は考え⽅はそこまで難しくはありません。たとえば 次のように、Config 型のすべてのフィールドに Maybe を付け⾜したような型を定義すれ ば、割と簡単にうまくいくはずです: type Config’ = Record ’[ "columns" >: Maybe Int , "languageExtensions" >: Maybe [String] ] 40
  43. 第 4 章 The Fun of Extensible 4.5 YAML 設定を使いこなす

    aeson の実装では、デコードする YAML (JSON) に対応する設定ラベルがない場合は、 もしそのラベルが Maybe 型なら Nothing となります。ですので、 Nothing のものだけ デフォルト値と置き換えればよいのです。しかし、これだとフィールドが多くなるにつれ て記述がたいへんになります。そこで extensible パッケージの Nullable を使います: import Data.Extensible import Data.Maybr (fromMaybe) fromNullable :: RecordOf h xs -> Nullable (Field h) :* xs -> RecordOf h xs fromNullable def = hmapWithIndex $ \m x -> fromMaybe (hlookup m def) (getNullable x) このコードを厳密に読むのは少し難しいです。感覚的な振る舞いとしては、前述した通 りです。すべてのフィールドに Maybe がついたような型 Nullable (Field h) :* xs の値の各フィールドをみて、Nothing だったらデフォルト値 def の値を返すというもの です。なんと、Nullable なレコードも aeson の型クラスのインスタンスになっているの で、特にインスタンスを定義することなく YAML のデコードができます。そのためあと は次のような main 関数を定義するだけです: import qualified Data.ByteString as BS import Data.Extensible import Data.Maybe (listToMaybe) import qualified Data.Yaml as Y import System.Environment (getArgs) main :: IO () main = do path <- listToMaybe <$> getArgs config <- case path of Just path’ -> readConfigWith defaultConfig path’ Nothing -> pure defaultConfig print config readConfigWith :: Config -> FilePath -> IO Config readConfigWith def path = do file <- BS.readFile path case Y.decodeEither’ file of Right Y.Null -> pure def _ -> fromNullable def <$> Y.decodeThrow file getArgs 関数はコマンドライン引数を [String] 型で返してくれます。listToMaybe :: [a] -> Maybe a 関数はリストが空なら Nothing を、そうでなければ先頭の要素を 41
  44. 第 4 章 The Fun of Extensible 4.6 終わりに Just

    に包んで返してくれます。コマンドライン引数がなければデフォルト値を返し、引数 があれば設定のファイルパスとして readConfigWith defaultConfig 関数に渡します。 この関数は ByteString という⽂字列型でファイルを読み取り、それをプレーンな YAML としてデコードします。もし、ファイルが空 (Y.Null) の場合はデフォルト値を返し、そう でなければ Nullable でラップされた Config 型にデコードし、前述した fromNullable でデフォルト値とマージします。もちろん、この main 関数のコンパイル結果は、最初に⽰ したコマンドの通りに動作します! 4.6 終わりに 本稿では extensible パッケージという Haskell のレコード問題に対して、Haskell (と いうよりは GHC) の機能をフルに使って解決しているバケモノパッケージを紹介しまし た。本稿で紹介した使い⽅は YAML の埋め込みのような実⽤的なものもあれば、Do 記法 レスプログラミングという謎な使い⽅があります。Haskell のおもしろさというのは、その ような両⾯を「型」の⼒を使って楽しめるところだと思います。ぜひ皆さんも Haskell の 魔術的な魅⼒に取り憑かれてみましょう。 More extensible 実は、本稿で紹介したトピックスは過去に筆者のブログにて記事を投稿してあります: • 拡張可能レコードでレコード型を拡縮する (Haskell)*14  • 多相バリアントを使いこなそう with Haskell*15 • 拡張可能タングルで Do 記法レスプログラミング♪ (Haskell)*16 • Haskell で型安全に YAML ファイルをビルド時に埋め込む*17 また、本稿で扱わなかった extensible についての話題も投稿しているので、筆者ブロ グの extensible-package タグを参照してみてください*18。 さらに extensible には攻略 wiki があります*19。攻略 wiki では extensible パッ ケージに含まれるあらゆる機能を可能な限り紹介しています。ですので、extensible を 使った特定の (不思議な) トピックスではなく、より内部的な話題を網羅的に知りたい場合 は攻略 wiki を参照するとよいでしょう。また、extensible パッケージの作者は⽇本⼈で *14 https://matsubara0507.github.io/posts/2017-11-28-fun-of-extensible-1.html *15 https://matsubara0507.github.io/posts/2018-03-24-poly-variant-with-haskell.html *16 https://matsubara0507.github.io/posts/2018-02-22-fun-of-extensible-3.html *17 https://matsubara0507.github.io/posts/2018-05-13-yaml-th.html *18 https://matsubara0507.github.io/tags/extensible-package.html *19 http://wiki.hask.moe/ 42
  45. 第 4 章 The Fun of Extensible 4.6 終わりに 「⽇本

    Haskell ユーザーグループ : Haskell-jp*20」にもいるので、もしわからないことがあ れば Haskell-jp の Slack などで質問してみるとよいでしょう (もちろん筆者もいます)。 *20 https://haskell.jp/ 43
  46. 第 5 章 オルタ 3 シミュレータのデバッグ機能 開発 弊社ではオルタ 3 プロジェクトの⼀環として,

    オルタ 3 シミュレータ (Alter3 Simulator) を開発しています。オルタ 3 シミュレータの開発にはゲームエンジン Unity を使⽤してい ますが,典型的なゲーム開発プロジェクトと異なり要件が特殊であり,システム的にも特 筆すべき点が多いものになっています。本章では,オルタ 3 シミュレータのデバッグ機能 について紹介し,異なる⼿法の⽐較を交えつつ実際の実装⽅法について解説します。 5.1 背景と導⼊ オルタ 3 シミュレータは,3D モデルの各関節の回転を制御することで実物のオルタ 3 の 動作を再現するようになっています。ここで,関節の回転の⽅向(回転軸) ,回転⾓度の範 囲はオルタ 3 の実物に合わせる必要がありますが,そのためには実物の測定や挙動の記録 などを⾏い,ソフトウェア側のパラメータを補正していく必要があります。 *1この作業を⾏ う上で,設定した数値データをシミュレータ上で視覚的に確認できると,設定の間違いの 発⾒や数値調整の助けになると感じたので,図 5.1 に⽰すような可動範囲表⽰の機能を実 装しました。 *1 ソフトウェア上ではクォータニオンを使って任意軸回転を表現できますが,実際のロボットの関節は複雑 なリンク機構の組み合わせであり,挙動も物理的なブレがあるので,実測の重要度は⾼いです。 44
  47. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.1 背景と導⼊ 図 5.1:

    オルタ 3 シミュレータにおける関節可動範囲表⽰機能 現状(執筆時点)のシミュレータでは実⾏中にデバッグ機能として表⽰できるようになっ ていますが,当初はギズモ(Gizmo)として実装していました。本章では,ギズモとゲー ムオブジェクトの双⽅の実装の解説を⾏います。扇形の範囲表⽰やクォータニオンの扱い, その他コンポーネントの扱い⽅など,ゲームに応⽤できる部分も多いかと思うので,ぜひ 参考にしてみてください。 対象読者 Unity の⼀通りの使い⽅やスクリプティングを経験していると理解が早いかと思います。 実際のプログラムコードを⽰すことを主眼に置いたため,気になる部分は実際に Unity 上 で試すことで理解の助けになるかと思います。実装ステップごとになるべく詳細に記述し たつもりなので,ぜひ追実装するような気持ちで読んでいただければと思います。 Unity のバージョンは 2018.3.7f1 を使⽤しています。 読者が得られる知⾒ • 基本的なギズモの実装⽅法 – OnDrawGizmos メソッドによるギズモの実装⽅法 – Gizmos.DrawRay メソッドの使い⽅ – Handles.DrawSolidArc メソッドの使い⽅ 45
  48. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.2 関節オブジェクトの作成 • 扇形の範囲表⽰オブジェクトの実装⽅法

    – Render Mode を World Space とした Canvas の使い⽅ – Image コンポーネントの Fill 機能の使い⽅ – GL クラスを⽤いたライン描画 • Transform クラスを使⽤しない直交ベクトルの求め⽅ • 任意軸回転の合成と,合成順序による違い 5.2 関節オブジェクトの作成 本章では,実際のロボットモデルの関節を簡易的に再現したモデル(図 5.2)を使って説 明します。 図 5.2: 本章で題材とする関節の簡易モデル この関節オブジェクトは,回転軸を表すベクトルと回転⾓度の下限・上限を持ちます。関 節の実際の回転⾓度は,外部から与えられた 0〜1 の割合によって決定され,回転軸周りの 回転が合成されることで関節が回転駆動します。回転⾓度の上限より下限の値の⽅が⼤き い場合は逆向きの回転駆動を表します。 以上の挙動を具体的に Unity 上で実装したものが以下の ArmController クラスになり ます。 using UnityEditor; public class ArmController : MonoBehaviour { [SerializeField] private Vector3 _rotationAxis = Vector3.up; // 回転軸 [SerializeField, Range(-180, 180)] 46
  49. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.2 関節オブジェクトの作成 private float

    _angleMin = 0; // 回転⾓度の下限 [SerializeField, Range(-180, 180)] private float _angleMax = 0; // 回転⾓度の上限 [SerializeField, Range(0, 1)] private float _angleRatio = 0.5f; private Quaternion _initialRotation; // 回転の初期値 private void Awake() { _initialRotation = transform.localRotation; } private void Update() { var angle = Mathf.Lerp(_angleMin, _angleMax, _angleRatio); var rotation = Quaternion.AngleAxis(angle, _rotationAxis); transform.localRotation = rotation * _initialRotation; } } 2 つの関節には,図 5.3 に⽰す通りのパラメータを設定しておきます。このインスペクタ に数値として表⽰されている内容を可視化することが本章の⽬的です。 図 5.3: 2 つの関節部分の駆動パラメータ 関節の回転⾃体は transform.localRotation で⾏っているため,親の Transform 47
  50. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.3 ギズモによる実装 による回転の影響は受けることになります。このことに関連して,回転軸ベクトル _rotationAxis

    はどの空間の扱いであるかというと,このオブジェクト⾃体の回転に 対しては不変であり親以上の回転に対しては追従することになるので, 「親の空間」におけ るベクトルとなります。 実際のプロジェクトでは 1 つの Transform に対して複数の回転が合成される部分もあり ますが,本章では上記の実装による挙動を前提とします。 5.3 ギズモによる実装 ギズモの実装は,アタッチする MonoBehaviour に OnDrawGizmos メソッドまたは OnDrawGizmosSelected メソッドを実装するのが簡単です。DrawGizmoAttribute を使 ⽤するとギズモの実装をエディタコードとして分離することもできますが,今回はシンプ ルに MonoBehaviour で実装してみましょう。 using UnityEngine; #if UNITY_EDITOR using UnityEditor; // Handles クラスを使うため #endif public class ArmController : MonoBehaviour { // ... #if UNITY_EDITOR private void OnDrawGizmos() { // ここでギズモの実装をします } #endif } 関節の可動部分にアタッチしている ArmController に OnDrawGizmos メソッドを 追加しました。まずは,回転軸を表⽰してみましょう。ギズモで線を描画するには, Gizmos.DrawRay メソッド*2を使⽤します。 *2 Gizmos.DrawLine メソッドが始点と終点を指定するのに対し,Gizmos.DrawRay は始点と⽅向を指定し ます。 48
  51. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.3 ギズモによる実装 var center

    = transform.position; var normalizedAxis = _rotationAxis.normalized; Gizmos.color = Color.red; Gizmos.DrawRay(center, normalizedAxis); コンパイルが終了すると,Scene ビュー上のオブジェクトの中⼼から回転軸の⽅向に⾚ い線が描画されると思います。インスペクタ上で Rotation Axis の値を変えるとギズモ の表⽰もそれに合わせて更新されます。 次に本題ともいうべき扇形の表⽰部分ですが,Handles.DrawSolidArc メソッドを使⽤ することで簡単に描画できます。Gizmos クラスではなく Handles クラスなところがポイ ントです。Handles クラスは UnityEditor 名前空間に属するので,先に掲載したソース のように適宜 UNITY_EDITOR シンボルによる条件付きコンパイルの対応が必要です。 *3 Handles.DrawSolidArc メソッドの引数を⾒てみましょう。 public static void DrawSolidArc( Vector3 center, // 中⼼点 Vector3 normal, // 扇形の法線 Vector3 from, // 扇形の始線の⽅向 float angle, // 中⼼⾓(度) float radius // 半径 ) 引数がいろいろありますが,それぞれ図 5.4 に⽰すように対応しています。 図 5.4: Handles.DrawSolidArc メソッドによる扇形の描画 *3 Unity エディタ上では普通に実⾏できるので,いざビルドするというタイミングでエラーになって気付く ことも多いです。 49
  52. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.3 ギズモによる実装 ここで from

    というベクトルですが,今回の⽤途では回転軸に垂直な⽅向のベクトルとす る必要があります。回転軸⽅向が transform.up となるようにギズモを表⽰しているオブ ジェクト⾃体を回転させてもよいのですが,回転させてしまうと困ることもあります。今 回は直交ベクトルを⾃分で求めてみましょう。 v を回転軸⽅向のベクトル, e をそれとは別の適当なベクトル*4とします。図 5.5 に⽰ すような,ベクトル v に垂直なベクトル u を求めましょう。 図 5.5: ベクトル v に直交するベクトル u ベクトル v, e の内積は,2 つのベクトルのなす⾓を θ とすると v · e = ∥v∥∥e∥ cos θ ベクトル v に対するベクトル e の正射影ベクトル v′ を考えると v′ = ∥e∥ cos θ v ∥v∥ = ∥e∥ v · e ∥v∥∥e∥ v ∥v∥ = v · e ∥v∥2 v ここで,ベクトル v, e がともに単位ベクトルであるなら, v′ = (v · e) v (∥v∥ = 1, ∥e∥ = 1) この正射影ベクトルを使って, u = e − v′ というベクトルを作ると,これは v に垂直 になります。ただし,与えられたベクトル v がたまたま e に平⾏( |v · e| = 1 )だった場 合は,最終的な結果も e に平⾏になってしまい, v に垂直な⽅向が決定できません。この 場合は内積などの計算をするまでもなく,たとえば e = (0, 1, 0) なら (1, 0, 0) などを解と すればよいでしょう。 以上を Unity 上で実装すると,以下のようになります。 *4 (0, 1, 0) などを適当に選びます。Unity では Vector3.up などを使うとよいでしょう。 50
  53. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.3 ギズモによる実装 private Vector3

    GetTangentDir(Vector3 v) { var d = Vector3.Dot(v, Vector3.up); if (Mathf.Abs(d) == 1) { return Vector3.right; } return (Vector3.up - d * v).normalized; } さて,これでようやく扇形を描画する準備が整いました。さっそく DrawSolidArc を 使ってみましょう。 var center = transform.position; var normalizedAxis = _rotationAxis.normalized; var angleRange = _angleMax - _angleMin; var tangentDir = GetTangentDir(normalizedAxis); Handles.DrawSolidArc(center, normalizedAxis, tangentDir, angleRange, 0.5f); インスペクタ上でフィールドの値を変えると,範囲を表す扇形も変化します。これで ⾓度の範囲が分かる表⽰にはなりましたが,Angle Min を変化させたときは扇形の始線 の⽅を動かしたいところです。そのためには,tangentDir を normalizedAxis を軸に _angleMin だけ回転させれば良さそうです。 var angleRange = _angleMax - _angleMin; var tangentDir = GetTangentDir(normalizedAxis); // 扇形の始線⽅向を回転 var fromDir = Quaternion.AngleAxis(_angleMin, normalizedAxis) * tangentDir; Handles.DrawSolidArc(center, normalizedAxis, fromDir, angleRange, 0.5f); 最終的に,以下のような実装になりました。各部の⾊はお好みで指定すると良いで しょう。 private void OnDrawGizmos() 51
  54. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.3 ギズモによる実装 { var

    center = transform.position; var normalizedAxis = _rotationAxis.normalized; var angleRange = _angleMax - _angleMin; var tangentDir = GetTangentDir(normalizedAxis); var fromDir = Quaternion.AngleAxis(_angleMin, normalizedAxis) * tangentDir; Gizmos.color = Color.red; Gizmos.DrawRay(center, normalizedAxis); Handles.color = new Color(0, 0.3f, 1, 0.5f); Handles.DrawSolidArc(center, normalizedAxis, fromDir, angleRange, 0.5f); } private Vector3 GetTangentDir(Vector3 v) { var d = Vector3.Dot(v, Vector3.up); if (Mathf.Abs(d) == 1) { return Vector3.right; } return (Vector3.up - d * v).normalized; } 描画結果は図 5.6 のようになります。インスペクタ上で各パラメータを変更した際に表 ⽰も適切に更新されます。 52
  55. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 図 5.6:

    ギズモによる実装 5.4 ゲームオブジェクトによる実装 ギズモによる実装は,既存のコンポーネントに対してちょっとしたデバッグ情報を表⽰ したい場合などは少ない⼯数で実現可能です。Gizmos や Handles といったクラスには, そうした表⽰や操作*5を実装するためのさまざまなメソッドが⽤意されており,描画や⼊ ⼒ハンドリングを簡単に実現できるようになっています。 可動範囲のギズモを作り込むうちに,アプリケーションの実⾏時にもこの表⽰で確認し たいと思うようになりました。しかしながら,ギズモはあくまで Unity エディタ向けの機 能であり,実際のビルドに組み込むことはできません(Unity エディタのゲームビューに表 ⽰させることはできます) 。そこで,この範囲表⽰をアプリケーションの実⾏時にも使⽤可 能なデバッグ機能として組み込むべく,ゲームオブジェクトとしてあらためて実装するこ とにしました。 *5 Handles にはその名の通りハンドルを扱う機能もあり,シーンエディタ上で操作可能なカスタムハンドル を実装することもできます。 53
  56. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 実装⽅針 ギズモによる実装時は扇形の描画に

    Handles.DrawSolidArc という便利なメソッドを 利⽤できましたが,ゲームオブジェクトとして作る場合には別の⽅法を考える必要があり ます。普段 uGUI をよく使っている⽅にとってはこちらの⽅が馴染み深いと思いますが, Image コンポーネントの Fill 機能を使うことで簡単に扇形を作ることができます。Image コンポーネントは Canvas コンポーネントとともに使⽤する必要があるので,扇形を描画 する平⾯は Render Mode を World Space に設定した Canvas を使うと良さそうです。 ゲームオブジェクトの構成 まず,扇形表⽰そのものとなるゲームオブジェクトを作成します。構造はシンプル*6で, 空の GameObject に Canvas と Image を付けただけです。 *7 Canvas の Render Mode を World Space に,Rect Transform*8の Width と Height を 1 に設定しておきます。次に扇形の描画部分となる Image ですが,以下のように設定し ます: • Image Type を Filled に設定します。 • Fill Method は Radial 360 に設定します。 • Fill Origin は Right に設定します。 x 軸正⽅向を扇形の始線とするためです。 Source Image は,⾃分で描くか,Unity のビルトインの Knob というスプライトなど, 何かしら円形の画像を指定します。筆者は図 5.7 のようなイメージを作成しました(格⼦ 模様は透明部分を表します) 。 *6 実は開発時点では,後述する 2 種類の回転のためにいくつかの階層からなる構造でした。その後,回転を クォータニオンの合成で実現するように変更し,本章の構成にまとまりました。 *7 Image によって要求される Canvas Renderer も⾃動的に付加されます。 *8 Canvas を追加すると Transform が⾃動的に Rect Transform に変化します。 54
  57. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 図 5.7:

    扇形の描画に使⽤するイメージ 以上で,図 5.8 に⽰すようなゲームオブジェクトができました。 図 5.8: 扇形オブジェクトのコンポーネント構成 スクリプトの準備 ギズモによる実装では関節のコントローラである ArmController クラスに直接実装を 追加できましたが,今回は独⽴のゲームオブジェクトとして作成するため,専⽤の制御ス クリプトを準備します。ここでは,以下のような AxisRangeView クラスを作成しました。 55
  58. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 using UnityEngine;

    public class AxisRangeView : MonoBehaviour { [SerializeField] private Vector3 _rotationAxis = Vector3.up; [SerializeField, Range(-180, 180)] private float _angleMin = 0; [SerializeField, Range(-180, 180)] private float _angleMax = 0; [SerializeField] private Image _arcImage = null; private void Update() { // ここで扇形の更新処理を実装します } } 回転軸や可動範囲といったパラメータは本来は外部(本章では ArmController クラス) から与えられるものですが,ここでは単純化のためインスペクタ上で直接変更できるよう にしています。 扇形の中⼼⾓の更新処理 まず,fillAmount は 0〜1 の割合なので,扇形の中⼼⾓の絶対値から次のようになり ます。 _arcImage.fillAmount = Mathf.Abs(_angleMax - _angleMin) / 360.0f; これだけだと_angleMin と_angleMax の⼤⼩関係が逆転,すなわち回転の⽅向が逆の場 合が区別できないので,範囲を動的に変更する場合は表⽰が不⾃然になってしまいます。 Unity の Quaternion における回転の正の⽅向は時計回りなので,fillClockwise もそれ に合わせましょう。 まとめると以下のようになります。 56
  59. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 _arcImage.fillClockwise =

    _angleMin < _angleMax; _arcImage.fillAmount = Mathf.Abs(_angleMax - _angleMin) / 360.0f; これを試してみると,_angleMax を動かしても_angleMin を動かしても扇形の始線は固 定されたままで,動径の⽅しか動きません。ギズモによる実装と同様に,_angleMin を動 かしたときは始線の⽅を動かしたい(図 5.9)ですが,Image の Fill 機能のみでは開始位置 を細かく制御できません。したがって,オブジェクト⾃体を_rotationAxis を軸として回 転させる必要があります。 図 5.9: ⾓度範囲を変更したときに期待される扇形の挙動 扇形の向きの更新処理 ギズモによる実装と違い,扇形を適切な向きで配置するためにはこのオブジェクト⾃体 を回転させる必要があります。この回転は以下の 2 つの回転の合成になります。 • 回転軸⽅向にオブジェクトの正⾯を向けるための回転 • _angleMin に扇形の始線を合わせるための回転 それぞれは簡単でも,回転(クォータニオン)の合成を考えると,合成順序と座標系の関 係を抑えておく必要があります。 クォータニオンによる回転の合成 複数の回転の合成は,それぞれの回転を表すクォータニオンを乗算することで得られま す。しかし,クォータニオンの乗算は⼀般に可換ではなく,左右どちらから乗算するかに よって結果が異なります。 各軸に対して 30 度ずつ回転するクォータニオンを,右に乗算していった場合と左に乗算 57
  60. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 していった場合の様⼦は図 5.10

    のようになります。ここで,たとえば Rx (30) は x 軸を 回転軸に 30 度回転するクォータニオンを表します。 図 5.10: 回転順序による違い ある回転を右に乗算する場合,それまでの回転を適⽤したローカル空間の軸を基準に新 たな回転が適⽤されていることが分かります(図 5.10 上段) 。⼀⽅,左に乗算していく場 合は何が基準になっているかというと,実はワールド(もしくは親の)空間の軸になって います(図 5.10 下段) 。 どちらが正しいということはないですが,⼤事なのは左右どちらから乗算するかによっ て,たとえば回転軸(Quaternion.AngleAxis の第 2 引数)をワールド座標で与えるべき かローカル座標で与えるべきかが変わるということです。Unity ではクォータニオンを直 接扱う機会は少ないかもしれませんが,覚えておくと役⽴つことがあるかもしれません。 オブジェクトの回転 まず, このオブジェクト⾃体の向きを回転軸に合わせる回転を考えます。どの⽅向を回転 軸に向けるかですが,これはローカル座標における Vector3.back,すなわち (0, 0, -1) の⽅向を回転軸の⽅向に向くように回転させると良いです。というのも,Canvas は z 軸 の負の⽅向に向いている⾯が「表」となっています。これを考慮しておくと,この Canvas 上に付加情報としてテキストを置いた場合に⾃然な⾒た⽬になります。 58
  61. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 図 5.11:

    Image を使⽤した扇形オブジェクト この回転は,Vector3.back から_rotationAxis へ回転することにほかなりません。 Unity では Quaternion.FromToRotation メソッドによってこの回転のクォータニオンを 得ることができます。 var facing = Quaternion.FromToRotation(Vector3.back, _rotationAxis); 図 5.12: Quaternion.FromToRotation による回転 次に,扇形の始線を動かすための回転ですが,これは_rotationAxis を回転軸に _angleMin だけ回転させるので Quaternion.AngleAxis メソッドを使うと良さそうで す。ただし,前節で説明した通り,この回転を左右どちらから乗算するかによって回転軸 59
  62. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 の扱いが変わってしまうので,そこだけ注意が必要です。 右から乗算する場合は,ローカル空間における_rotationAxis

    を回転軸とします。 _rotationAxis はローカル空間ではどうなっているかというと,前段の回転(facing)に よって Vector3.back を向いているはずです。よって, この場合の回転軸は Vector3.back となります。 var offset = Quaternion.AngleAxis(_angleMin, Vector3.back); 左から乗算する場合は,親空間における_rotationAxis を回転軸とします。この場合は 回転軸は_rotationAxis そのものとなります。 var offset = Quaternion.AngleAxis(_angleMin, _rotationAxis); 今回は,回転ごとのローカル空間で考えられる⽅が回転の流れが分かりやすいと思った ので,右に乗算するようにしました。 var facing = Quaternion.FromToRotation(Vector3.back, _rotationAxis); var offset = Quaternion.AngleAxis(_angleMin, Vector3.back); transform.localRotation = facing * offset; オイラー⾓を使った回転 実のところ, Quaternion.AngleAxis(_angleMin, Vector3.back) という回転は z 軸 周りの回転にほかならないので, クォータニオンの乗算よりもオイラー⾓を使った⽅が分か りやすいかもしれません。回転軸の⽅向に向ける回転をクォータニオンで⾏い,始線の回 転を transform.localEulerAngles を使って⾏うようにすると,以下のようになります。 var facing = Quaternion.FromToRotation(Vector3.back, _rotationAxis); transform.localRotation = facing; var angles = transform.localEulerAngles; angles.z = -_angleMin; transform.localEulerAngles = angles; 60
  63. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 z 軸の負⽅向を軸とした回転のため符号に注意が必要です。どのような回転なのかは分

    かりやすいですが,少しコードが冗⻑に感じられるかもしれませんね。 回転軸の表⽰ ギズモのときと同様に,回転軸を表すベクトルを線で表⽰したいところです。今回は 2 つの⽅法を試してみました。 適当な⼦オブジェクトを置く ギズモによる実装と違い,表⽰⽤オブジェクトは-transform.forward ⽅向を回転軸 ⽅向に合わせているので,この⽅向に向いた細いシリンダーなどを⼦オブジェクトとし ておけば回転軸の表⽰としてこと⾜ります。⼦オブジェクトを置くだけなので,特に追 加の実装は不要です。図 5.13 は,ビルトインの Cylinder メッシュのスケールを調整し, Unlit/Color マテリアルを付けたものを回転軸として置いたものです。 図 5.13: 回転軸を表すシリンダーを追加 これはこれで⾮常に簡単ではあるのですが,普通の 3D オブジェクトなのでズームイン・ アウトによって軸の⾒た⽬の太さが変化して⾒えます。デバッグ表⽰としてはむしろ距離 61
  64. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 に関係なく細い線で表⽰される⽅が⾒やすい*9こともあり,今回は次節のラインを描画す る⽅法にしました。

    ラインを描画する Unity で純粋に線を引くには,MeshTopology.Lines 等を指定した Mesh を描画する*10 か,GL クラスを使って描画するなどの⽅法があります。GL クラスを使う⽅が簡単なので, こちらでやってみましょう。 GL クラスを使った描画においても何かしらのマテリアルが必要になるので,あらかじめ 作っておきます。線を描画するのに適した Hidden/Internal-Colored というビルトイン シェーダーがあるので,こちらを使⽤すると良いでしょう。 private Material _material; private void Awake() { _material = new Material(Shader.Find("Hidden/Internal-Colored")); } いくつか補助的なメソッドを作っておきます。 // オブジェクトの中⼼から dir ⽅向に線を引く private void DrawRadialRay(Vector3 dir, Color color1, Color color2) { DrawRay(transform.position, dir, color1, color2); } // start を始点に dir ⽅向に線を引く private void DrawRay(Vector3 start, Vector3 dir, Color color1, Color color2) { GL.Color(color1); GL.Vertex(start); GL.Color(color2); GL.Vertex(start + dir); } *9 実際のモデルはもう少し込み⼊った構造をしているので,デバッグ表⽰は意図的に浮いた表現にしたかっ たのと,表⽰物の密度を上げたくなかったという理由があります。 *10 Mesh を描画する⽅法も,通常の MeshRenderer のほか,Graphics.DrawMesh メソッドといったより低レ ベルなものもあるようです。 62
  65. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 これらを使って,OnRenderObject メソッド*11の中で描画処理を実装します。

    private void OnRenderObject() { var axis = -transform.forward; _material.SetPass(0); GL.PushMatrix(); GL.Begin(GL.LINES); DrawRadialRay(axis, Color.red, new Color(1, 0, 0, 0)); GL.End(); GL.PopMatrix(); } ⾒た⽬はギズモに似ていますが,こちらは実⾏時にも描画されます。ギズモに⽐べると 記述量が多いですが,デバッグ情報としてちょっと線を描画したいといった時には使える と思います。 *12 *11 必ずしも OnRenderObject である必要はないですが,描画順などの関係で典型的なタイミングかと思われ ます。ちなみに OnRenderObject という名前ながらタイミングは「すべての描画の後」であり,まさに描 画されようとするタイミングでは OnWillRenderObject が呼ばれます。 *12 パフォーマンスは良くないので,あくまでデバッグ⽤途として使⽤すると良いと思います。 63
  66. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.4 ゲームオブジェクトによる実装 図 5.14:

    GL クラスによるライン描画を追加 最終的な実装 ゲームオブジェクト版の実装は最終的に以下のようになりました。せっかくなので,扇 形の始線・動径を強調するためにここにもラインを描画してみました(図 5.14 でも描画さ れています) 。 [SerializeField] private Image _arcImage = null; private Material _material; private void Awake() { _material = new Material(Shader.Find("Hidden/Internal-Colored")); } private void Update() { var facing = Quaternion.FromToRotation(Vector3.back, _rotationAxis); var offset = Quaternion.AngleAxis(_angleMin, Vector3.back); transform.localRotation = facing * offset; _arcImage.fillClockwise = _angleMin < _angleMax; 64
  67. 第 5 章 オルタ 3 シミュレータのデバッグ機能開発 5.5 最後に _arcImage.fillAmount =

    Mathf.Abs(_angleMax - _angleMin) / 360.0f; } private void OnRenderObject() { var axis = -transform.forward; var angle = _angleMax - _angleMin; var radialMin = transform.right; var radialMax = Quaternion.AngleAxis(angle, axis) * radialMin; _material.SetPass(0); GL.PushMatrix(); GL.Begin(GL.LINES); DrawRadialRay(axis, Color.red, new Color(1, 0, 0, 0)); DrawRadialRay(radialMin, Color.green, new Color(0, 1, 0, 0)); DrawRadialRay(radialMax, Color.green, new Color(0, 1, 0, 0)); GL.End(); GL.PopMatrix(); } private void DrawRadialRay(Vector3 dir, Color color1, Color color2) { DrawRay(transform.position, dir, color1, color2); } private void DrawRay(Vector3 start, Vector3 dir, Color color1, Color color2) { GL.Color(color1); GL.Vertex(start); GL.Color(color2); GL.Vertex(start + dir); } 5.5 最後に 本章では,オルタ 3 シミュレータにおけるデバッグ表⽰機能について,実装⽅法の⽐較 や解説を⾏いました。技術的にはマニュアル等でカバーできる内容ではありますが,具体 的な要件を想定した⼿法⽐較や個々の要素の組み合わせといった応⽤例として参考にして もらえればと思います。紹介した内容⾃体はゲームではないものでしたが,そこに含まれ るいくつかの内容はゲームにも活⽤できるかと思います。また,オルタ 3 シミュレータに ついては冒頭で触れるのみでしたが,こちらのシステムについてもいつかどこかで紹介で きたらと思います。 本章の執筆にあたり題材となる部分の実装コードの⾒直しとまとめを⾏う過程で,実装 をより改善できたり⾃分⾃⾝の理解を深めることができたりと,執筆駆動開発の可能性を 65
  68. 著者紹介 萩原 涼介 (第 1 章担当, Twitter: @raryosu) 2018 年

    4 ⽉にミクシィに新卒⼊社したエンジニア。津⼭⾼専卒の 21 歳。minimo 事業部の遊撃チームでプロダクトに関わるコードを書いたり、事業の成⻑を思い描 きながら数字を出したり読んだりしています。Perl を主に書いていますが、Go や Python も書きます。なんでもやります。   井本 ⼤登 (第 2 章担当, Twitter: @adarpata) XFLAG で Unity クライアントエンジニアをしています。ゲームを作るのが好きで す。ゲーム開発とともに、チームビルディングや設計などプロダクトを⻑⽣きさせ る⽅法に関⼼があります。ガルパンにも関⼼があります。   栗原 尚弘 (第 3 章担当, Twitter: @soiya1919) 18 新卒で XFLAG でインフラ兼サーバサイドエンジニアをやっております。学⽣時 代はアプリとかいろんな物作ってました。会社では、インフラ (GKE) 運⽤とサーバ サイドコード書いてます。最近は Istio に注⽬しています。   松原 信忠 (第 4 章担当, GitHub: matsubara0507) 所属はモンストサーバチームで Ruby を書いてる。プログラミングが好きで、普段 は推し⾔語の Haskell で遊んだり、新しいプログラミング⾔語を勉強したりしてい る。Haskell-jp や Elm-jp でちょこちょこ活動もしている。   YuukiARIA (第 5 章担当) 2014 年ミクシィ⼊社。プログラミングちょっとできる。   67
  69. 付録 著者紹介 ⼟屋 雅(ミクシィ本のデザイナー, Twitter:@miyabt_, note:miyabt) 前回の本の失敗をリベンジすべく、舞い戻ったデザイナー。最近はアプリをモリモ リ作ったりユーザーインタビューしたり Daily UI

    やったりプログラミング勉強した り。note 書く書く詐欺が得意。ハッカソン呼んでください!   杉⽥ 絵美 (プロジェクト推進・制作進⾏等の庶務, GitHub: esugita, Twitter: @semiemi7) 元エンジニアで、ミクシィでは、Perl を触ったり、アプリを触ったり、新規事業の PM をしたりして、今は、DevRel チームで各種イベントを企画・運営したり、技術 的な知⾒のアウトプット活動をサポートしたりしています。   喜多 功次 (制作アシスト, GitHub: kojikita, Twitter: @kojikita) DevRel チームで勉強会やイベントの運営などをしています。元エンジニアで以前は web フロント側のサービス開発やアドテクなどを担当していました。JavaScript が 好きで、最近は PWA に注⽬しています。 68
  70. mixi tech note #01 2019 年 4 ⽉ 14 ⽇ 初版第

    1 刷 発⾏ 著 者 株式会社ミクシィ 有志 発⾏所 株式会社ミクシィ 印刷所 ⽇光企画   © mixi, Inc.