こまぶろ

技術のこととか仕事のこととか。

ポッドキャスト出演の収穫: #成し遂げたいam ep.7 に出演した

先日、ポッドキャスト「#成し遂げたいam」の収録にゲストとして参加してきた。そして、収録した前・後編のうち、前編がすでに公開されている。

anchor.fm

内容については、上のツイートのようなことを話している。この記事では、

を書く。リンク集が話の内容に沿ったものであるのに対して、それ以下の文章はメタ的なものになっている。ぜひ、本編と併せて読んでいただき、できればフィードバックをお寄せいただきたい。

補足リンク集

本編のShow Notesには載せられなかったリンクを掲載する。

ブログを書くことになったきっかけ

このブログの最初の記事。

ky-yk-d.hatenablog.com

write-blog-every-week

Slackグループを立ち上げたkojirock(@kojirock5260)さんの記事。

kojirooooocks.hatenablog.com

昨年末に立ち上げられ、無事完走したAdvent Calendar。

adventar.org

カカカカックさん関連

ブログ書かなきゃと思わされたカカカカックさんの一枚のスライド。ここから始まった。

「ブログ欲」の話がされているポッドキャスト(fukabori.fm)。エラーでつまずいたときにブログを書くことの意義についてもこちらのエピソードで語られている。

fukabori.fm

『自省録』関連

『自省録』(岩波文庫)。

自省録 (岩波文庫)

自省録 (岩波文庫)

『自省録』を引用しているブログ記事。

ky-yk-d.hatenablog.com

アジャイルに対する欲望

アジャイルに強い思いを持ちすぎていた頃の記事。良くも悪くも我ながらエモい。

ky-yk-d.hatenablog.com

宣伝:DevLOVE

3月に開催する2つのイベント。

devlove.doorkeeper.jp

devlove.doorkeeper.jp

なぜポッドキャストに出ることになったのか

なぜポッドキャストに出ることになったか。きっかけは、今回出演させていただいた「#成し遂げたいam」の過去の回を聴いて思ったことを書いた以下のツイートだ。当然、こういうことをツイートすれば、パーソナリティの一人であり、他にも多くのポッドキャストを配信されている「ポッドキャスト生やすおじさんお兄さん」ことKANE(@higuyume)さんから声が掛かることは予想できた。果たして、出演の依頼をいただくことになり、今に至る。

事前に「自分を知ってもらうメモ」を書いた

僕は、技術やその他の何らかの領域において、特段人に話すべき知見を持ち合わせているわけではない。このブログにしても、自分の有している知識を誰かに伝えるというよりは、書こうとすることによって考えるということに重きを置いている。したがって、ポッドキャストに出るといっても、たとえば和田卓人(@t_wada)さんのように、確固たる実績に基づいて知見を語るというわけにはいかないし、TDDやペアプロといった「持ちネタ」があるわけでもない。パーソナリティのKANEさんとなべくら(@nabe__kurage)さんにしても、それほどお互いをよく知っている間柄でもないから*1、僕が何を喋るのか、喋ることができるのか、というのは未知数だったはずだ。

そこで、事前に頼まれてもいないのに僕について知ってもらうためのメモを作成して、お二人に目を通していただくことにした*2。そのメモには「話せないけどお二人に参考にしてほしいこと」も載せていたので、公開することはしないけれども、これを作成したのは有意義だった。

メモには主に、下記の事柄を書いた。

  • 自分の来歴
  • 最近考えていること、悩んでいること
  • ポッドキャストで話せそうな話題

1点目の、自分の来歴については、主に就職をしてから、どの時期にどういう人・物事との出会いがあって、自分がどう考え、行動してきたのか、を書いた。書くにあたっては、ConnpassやDoorkeeperのイベント参加履歴、読書履歴、Twitterやブログの過去ログ、カレンダーの過去の予定、毎日つけている日記など、様々な情報ソースに目を通した。その過程で、忘れかけていた初心や、消化不良のままになっている書籍などを思い出すことができた。また、2年弱という期間をふりかえることによって、自分の思考や行動を貫いているものや、逆に一貫していないもの、それと過去の自分からの差分などが見えてきた。

2点目の、最近考えていること、悩んでいることについては、普段からノートやTwitterに書き出しているから、新しいものが生まれたわけではないけれども、人に伝えることを意図してまとめてみると、いくつかの主題が見えてきた。色々なことを考えているようで、実は少ない数の中心的なテーマに発しているということがわかったので、掘り下げるべき方向性を考えるのに役立った。

3点目の、ポッドキャストで話せそうな話題を書いてみたところ、冒頭の言葉を覆すようではあるが、「意外と人に話せることがあるんじゃないか」という感想を持った。話せそうな事柄は、必ずしも自分のオリジナルの内容ではなく、書籍やインターネットの記事、Twitterポッドキャストなどの様々な媒体から入手した他人の知識や意見が多くを占めているのだけれども、それでも自分なりの情報の集め方、消化の仕方をしたものを表現するということは意味のないことではないと思うようになった。

以上のいずれも、普段は自分のその時々の関心に導かれて断片的に想起・表現している事柄を、まとめて人に対して表現しようとすることによって得られた効果だ。特に、今回のメモはパーソナリティのお二人に対してのみ見せるためのものであり、ブログなどでは公開することを躊躇う事柄も排除していないのが有効だった。これが、不特定多数に公開する媒体や、あるいは人事・採用上の評価を受ける可能性のある媒体であったとすれば、また違った結果になっただろう。

自分のポッドキャストでの喋りを聴く

公開前に、事前確認ということで、全編を通しで聴き直させてもらった。プレゼンテーションの練習をするときに、自分の喋りを録音して聴いてみたことはあった。しかし、ポッドキャストのように、事前のスクリプトなしの、人との対話を録音したものを聴くというのは初めてだった。

まず、一番懸念していたスピードについては、速すぎるということがなくて安心した。僕は普段、話すスピードが速いという自覚があるので、今回はかなり意識をしてスピードを抑えていた。意識しても速くなってしまうものなので、うまくいったのはよかった*3

話の内容についても、思っていたよりはしっかり喋ることができていたと思う。欲望についての考えは、抽象的にすぎるところがあり、リスナーの方を置いてけぼりにしているのではないかという懸念もあるが、話として聴きづらいということはそこまでないのではないかと自分では評価している。

一方で、気づくことがあった。それは、以下に記すようにネガティブな形で現れているのだが、ポッドキャストという媒体で自分の語りを聴くことによって見えてきたものであり、大きな収穫でもある。特に気になった以下の2点について言及しておきたい。

  • パーソナリティの質問に答えていない/応答していない
  • 話している中で主張が一貫していない

1点目は、ポッドキャストという媒体ゆえの難しさと、自分自身の元来持っている悪い癖とが現れたものだと思う。今回、話せることがあるかが不安だったため、上に書いたように事前にかなり話す内容を考えておいた。これは、良い面もあったと思うけれど、「時間内に話さなきゃ」「放送事故っぽくなったらいやだ」と、あらかじめ考えてきたことを喋る方向に傾いてしまったと思う。

特に、なべくらさんからは自分の意見に対して反対意見、少なくとも違和感を表明されていたのに、正面から対話をするという方向にいけなかったのは、とても勿体なかったと思う。これは、「あらかじめ持っている意見に疑問を呈されるとその場では防衛的になってしまう」という自分の弱いところが出た結果でもあり、反省が大きい。

2点目は、逆に話していることに一貫性がなくなっていたという点だ。具体的には本編を聴いて探していただければと思うが、語りの中で、矛盾した2つの主張をしている場面や、同じ言葉を別の意味で使うことで話をすり替える場面があったように自分では感じている。これは、「その場しのぎ」で答えてしまったがゆえだと思われ、自分の普段の思考の盲点があぶり出されるものであったように思う。

ブログや発表原稿の場合には、ある程度筋を作ってから書くので、ある部分と別の部分とで矛盾した内容を書き、しかもそれに自ら気づくようなことはあまりない。ポッドキャストは、自分の思考の流れに他者の意見や問いかけが挿し込まれるので、上に書いたようなその場しのぎの発言が出やすい状態になる。今回は聴き直したから気づくことができたが、普段の会話でも恐らくその種の発言をしているのだろう。これに気づくことができた(あるいは、向き合わざるを得なくなった)のはポッドキャスト出演の貴重な収穫だと思う。

終わりに

収録当日はとても楽しく、終わってからの反省の多いポッドキャスト出演だった。当初の「ポッドキャストに出演しても喋ることがないんじゃないか、リスナーのためになるのだろうか」というのは烏滸がましい悩みであり、むしろ自分にとって大きな気づきのある体験になった。このような機会を設けてくださったKANEさんとなべくらさんに感謝したい。反省を今後に生かすこととともに、本編でも言及があった「欲望との折り合いのつけ方」の記事を書くこととで報いたい。

なお、後半では、何度かTwitterでは言及している「言葉」についての思いを話させてもらっている。2本どり2本目ということもあり、緊張もほぐれてきて、個人的にはかなり手応えを感じている。まだ編集作業中で、僕の方にも音源が回ってきていないので、聴き直したら大いに反省することになるかもしれないが、公開されたらぜひ聴いてみていただきたいと思う。

*1:なべくらさんに至っては収録日が初対面だった。

*2:これは最終的に、なべくらさんが用意してくださった収録用のメモに統合された。

*3:ポッドキャストの中で言及したカカカカックさんからも「1.2倍速で聞くぐらいがちょうど良かった」というコメントをいただいた。

ソフトウェア開発者がテスト技術の資格「JSTQB」のテキストを読んで感じたこと

テスト技術についての資格である「JSTQB認定テスト技術者資格」のテキストを読んだ。読もうと思った理由は後述するが、ソフトウェア開発についての考えに別の光を当ててくれるいい体験になった。

ソフトウェアテスト教科書 JSTQB Foundation 第3版

ソフトウェアテスト教科書 JSTQB Foundation 第3版

  • 作者: 大西建児,勝亦匡秀,佐々木方規,鈴木三紀夫,中野直樹,町田欣史,湯本剛,吉澤智美
  • 出版社/メーカー: 翔泳社
  • 発売日: 2011/11/12
  • メディア: 単行本(ソフトカバー)
  • 購入: 5人 クリック: 85回
  • この商品を含むブログ (12件) を見る

なぜテストを学ぼうと思ったのか

僕は開発者、プログラマという自己認識をしており、現在の業務の中心はソースコードを書くことだ。また、普段勉強している事柄も、コードの書き方やプログラミング言語についてのものが中心になっている。

しかしながら、今回、テストの勉強をざっとでいいのでしておくべきだと思い立ち、冒頭に掲げた書籍を手に取った。その理由は以下の通りである。

  • 現職にはテスト専門の部隊がおらず、開発チームがテストまで行うため、テストの技法を知る必要がある。
  • プロジェクトを通じてどのように品質を高めていくを織り込んだ計画を作成する必要があり、テスト計画の考え方を知りたい。
  • テスト経験のないメンバーが漏れの多いテスト項目書を作成していたため、共通の理解の基盤が必要と考えた。

上記は、現在のプロジェクトというコンテキストに依存する理由であるが、一般論としても、ソフトウェア開発者として、品質に関わる重要な営みであるテストを学ぶ意味はあると思う。どれだけスピーディに開発をしても、品質が悪くテストで差し戻されるようであれば、結局リードタイムは長くなってしまう。製造業的な言い方をすれば、「歩どまりの悪い」開発となってしまう。

SHIROBAKO』で井口さんが、「リテイクが見えればいい原画が見える、悪い原画もわかる。」と言っているのと同じで、テストにおいてどのような観点でソフトウェアが見られるかを知っておくことは開発者にとって意味のあることだろう。そのような言説にもちょうど先日接する機会があり、いいタイミングだと思って学ぶことにした。

以下、本を一冊読んで感じたことを述べていく。

制約の中でテストをするということ

この書籍の冒頭の方で、「テストの7原則」というものが挙げられている*1

  • 原則1:テストは「欠陥がある」ことしか示せない
  • 原則2:全数テストは不可能
  • 原則3:初期テスト
  • 原則4:欠陥の偏在
  • 原則5:殺虫剤のパラドックス
  • 原則6:テストは条件次第
  • 原則7:「バグゼロ」の落とし穴

このうち、ハッとさせられたのは、原則2の「全数テストは不可能」である。これは、「ソフトウェアに入力する可能性のある、すべてのパターンをテストする」*2ことが不可能であると主張するものであり、当たり前といえば当たり前の、何ら衝撃を受けるようなものではない。

僕がこの原則に強い印象を受けたのは、この原則に端を発して、書籍全体を貫いているテストというものへの考え方が、自分のテストに対するイメージとは異なるものだったからだ。僕のテストに対するイメージは、バグを無くすためにしらみつぶしで実施するものというものだった。もちろん、全数テストを行うものだ、というまでの極端なイメージではなかったが、決められた件数を実施するまでひたすらやる退屈なものというイメージがあった。

しかしながら、この書籍では、時間やコストの観点から、どのようにテストを効率的に設計・実施するかという観点から書かれている記述が非常に多かった。どのようなテストを実施するのかについても、テストの目的、対象となるソフトウェアの種類に応じて合理的なものを選択するという姿勢が一貫している。「全数テストは不可能」という原則には、テストがそもそも限られた資源の中で実施されるものであり、「制約の中でどのようにテストを行うか」こそがテスト技術の本質なのだということが表現されているように感じた。現実を見据えることなくしてテストを考えることはできないのだ。

「品質」の多様性とOOP

テストは品質と深い関係を有している。僕も、品質という言葉を普段口にすることもあるが、実際のところ「品質」が何を指しているのかは深く考えたことがなかった。「バグがある」や「パフォーマンスが悪い」というのが品質の悪い例であることはわかるから、コードを書く際も左の2点については考えることは多いが、この書籍を読むことで、考慮すべき品質が思っていた以上に多様であることがわかった。

それを教えてくれるのは、書籍中で紹介されているISO/IEC 9126の品質モデルである。これは、非機能テストを実施する際に参照することのできるモデルとして紹介されているものであり、以下のように整理されている*3

品質特性 品質副特性
機能性 合目的性・正確性・相互運用性・セキュリティ・適合性
信頼性 成熟性・障害許容性・回復性・適合性
使用性 理解性・習得性・運用性・魅力性・適合性
効率性 時間効率性・資源効率性・適合性
保守性 解析性・変更性・安定性・試験性・適合性
移植性 環境適応性・設置性・共存性・置換性・適合性

これを見たときに、「自分は今まで品質というものの一部しか見られていなかった」という思いを持つとともに、ここしばらく興味を持って勉強している事柄の意義を再確認できた気がした。「興味を持って勉強している事柄」というのは、オブジェクト指向のことである。

オブジェクト指向は、拡張性や保守性を持ったソフトウェアを開発するための技術、考え方だと思っている。オブジェクト指向と品質の関係については、オブジェクト指向の泰斗であるバートランド・メイヤーの言葉を引いておきたい。鈍器大著『オブジェクト指向入門』の第1章は「ソフトウェアの品質」であり、以下のような端的な言明で説き起こされている。

工学の目的は品質である。したがって、ソフトウェア工学は、品質の高いソフトウェアの生産を意味する。本書ではソフトウェア製品の品質を飛躍的に高める可能性を持った一連の技法を紹介する。*4

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

メイヤーは、ソフトウェアの品質の「外的品質要因」と「内的品質要因」のうち、オブジェクト指向の技法が内的品質要因を実現するとした上で、あくまで後者は前者の達成のための手段であると述べる。これは忘れてはならない事柄だと思う。拡張性や保守性というのは、後々の開発者にとって問題になるものであり、直接的に顧客に価値をもたらすものではない。それ自体が価値なのではないという意味では、アジャイルもこれに似ており、ややもするとソフトウェア開発者の自己満足の対象となりかねないという危惧を持っている。

上の表に保守性や移植性というものが挙がっていることを、自己満足の免罪符に使うつもりはない。品質特性に含まれているということは、いつでもこれらが重要であるということを意味しない。どのような品質特性を重んじるべきかは、対象となるソフトウェアの性格によって異なるからだ。自分にとって「品質モデル」が啓発的だったのは、普段自分が「開発者」として興味を持っている事柄と、(今までは「開発」ではない営みとして見がちであった)テストで扱われている事柄が地続きであることを確認できたからである。

後工程を考えて開発するということ

当たり前のことではあるが、テストをする際には、要件がわかっていることが重要である。要件を窺い知るための資料は、書籍では「テストベース」という言葉で指示されている。外部のソフトウェアテスト会社にテストを依頼したところ、「仕様がないからテストできません、うちは設計する会社じゃないんですよ」と言われたという笑い話を耳にしたことがあるが、有効なテストを実施するためにはこのテストベースの充実が不可欠である。

テストベースの分析はテスト設計のための重要な作業であるわけだが、書籍では、実際のプロジェクトにおいてはこのテストベースが十分に揃っていない場合が多いから、収集と分析にかかるオーバーヘッドを考慮に入れて計画せよ、と指摘されている*5。ドキュメントが揃っていないと、テストにおいても遅延を引き起こすことになるというわけだ。

これは、現在のプロジェクトにおいても実体験していることであるが、仕様が開発者の頭の中だけにあるような状態では、テスト実施は覚束ない。「テストで見つかればいいや」と考えずに、品質を意識してコードを書くのは当然なされるべきことであるが、「どのような資料があればテストしやすいだろうか」と考えて適切な資料を残すということも重要なのである。後工程のことを考えて開発をするというのは、コードを書くことだけではないのだ。

おわりに:開発者がテストを学ぶということ

以前のブログ記事で、『エクストリームプログラミング』の一節を紹介した。以下に再び引用する。

ky-yk-d.hatenablog.com

品質部門を別に設置すれば、エンジニアリングにおける品質の重要性が、マーケティングや営業における品質と同等だというメッセージを送ることになる。エンジニアリングで品質に責任を持つ人がいなくなる。すべては他の誰かの責任だ。開発組織のなかにQA部門を設置したとしても、エンジニアリングと品質保証が別々の並行した活動であるというメッセージを送ることになる。品質とエンジニアリングを組織的に分離すれば、品質部門の仕事は建設的なものではなく、懲罰的なものとなってしまう。*6

現在属している会社にも、独立した「品質部門」が存在しており、プロダクトの品質を改善するためのマニュアルやドキュメントのフォーマットを「開発部門」に提供したり、レビューに入ったりしている。自分も「開発部門」の一角に属する身であるわけだが、他の「開発部門」の諸先輩に話を聞いてみると、「品質部門」の取り組みに対して懐疑的な声も聞こえてくる。だからこそ、『エクストリームプログラミング』の上の記述を読んだときは笑ってしまった。

今回、JSTQBのテキストを読んでみて、テストという営みがソフトウェア開発プロジェクトの「テスト工程」以外の部分にも深く関わっているものだということを確認することができた。上では書かなかったが、開発者とのコミュニケーションの重要性や、プロセス全体においてテストチームがボトルネックとならないようにする配慮、テストに関わるリスクに対する考え方など、テストという分野が視野に収めるべき事柄は非常に広い。これは、専門職としてのテスト技術者の重要性を語るものであると同時に、「視野に収められる側」である開発者もまたテストを学ばねばならないということも意味しているのだろう。

エクストリームプログラミング

エクストリームプログラミング

*1:27頁以下。

*2:28頁。

*3:108頁。

*4:バートランド・メイヤー『オブジェクト指向入門 第2版 原則・コンセプト』2頁。

*5:40頁。

*6:ケント・ベックエクストリームプログラミング』128頁。

社外の人にしてもらった1on1を振り返る

随分と日が空いてしまったが、2月の中頃、ブログ仲間(?)のnitt-san(@nitt_san)さん*1に、1on1の機会を設けていただいた。nitt-sanさんによる記録はこちら。

nitt-san.hatenablog.com

社外の人と「いきなり1on1」?

今回、1on1をしてもらったnitt-sanさんは、僕の会社の上司でも何でもない。繋がりは、「write-blog-every-week」*2と、「エンジニアの登壇を応援する会」という2つの社外コミュニティであり、リアルでお会いしたのは今回の1on1が3回目だった。

nitt-sanさんは、先月から「いきなり1on1」という企画を実施されていて、僕の前にすでに6人の方と1on1をされている*3。今回の僕の1on1も、その企画の一環として行われた。nitt-sanさんが「いきなり1on1」を行うに至った経緯については、下に掲げた記事を参照していただきたい。

nitt-san.hatenablog.com

この記事では、nitt-sanさんとの1on1を、2週間が経とうとしている時点から改めて振り返る。

「30歳のときにどうしていたい?」

冒頭の方で受けた質問だ。この質問が出たのは、当日の最初に記入したヒアリングシートに僕が「目標がない」と記入し、しかもそれを最高の優先度としたからだろう。目標がうまく持てていないことが一番の関心事であり、この日の1on1に対してもこの辺りのモヤモヤが少しでも晴れればと思って臨んでいた。

そんな僕であったが、上に掲げた質問を投げかけられて、固まってしまった。ここで固まってしまうようであるからこそうまく目標設定ができないでいると言えばそれまでなのだが、一発の質問で何も考えられていなかったことに気づかせられた。この質問になぜ自分が固まってしまったのかを考えてみると、そこに「30歳」という具体的な数字が入っていることが大きいのではないかと思う。

以前、このブログで、プログラマとして生き、技術の進歩にキャッチアップしていくことへの不安を前面に出した記事を書いた。

ky-yk-d.hatenablog.com

このような記事を書くのであるから、ある程度長い期間、たとえば10年くらいは、プログラマとして生きていくことを意識していたはずである。そうであるとすれば、たかだか4年後である30歳のときのことはある程度具体的に考えていても良さそうなものである。しかしながら、僕は20年や30年、下手したら40年後のことについて不安を覚えてばかりだった。

長いタイムスパンでものを考えるのは時として必要なことだが、それが意味を持つのは、より近い将来や現在の行動を考えるのに役立ててこそだろう。将来への不安で現在が埋め尽くされ、近い将来を具体的に考えることから目を背けるようでは、せっかくの「長期的視野」も害あって利がない

今回、「30歳」という具体的な数字を伴った質問をされたことで、あちこちに散逸しがちな自分の意識を、特定の一点に集中させるきっかけを得ることができた。まだ、問いに対する答えを練り切れてはいないが、貴重な問いかけだったと感じている。

「どうすれば経験から学んだことになる?」

これは、僕が「経験から学ぶのが苦手なんです」というようなことを言ったことを承けての質問だ。僕は、本をよく読む方で、そこから得た学びを活かすこともそれなりにできていると思っている。しかし一方で、自分が実際に見た、体験した事柄から教訓や認識を抽出し、定式化する、あるいは別の機会に活かす、ということにはあまり自信がない。

ブログや日常の会話といった場面においても、書籍や人の話からの引用がソースであることが多い。他の人のブログや勉強会での発表に、現場でその人が経験した事柄に根ざした知見が表現されていると、「自分にはこれは無理だ・・・」と勝手に落ち込んでしまうこともある。いわば「経験からの学習コンプレックス」である。

以上のような自己認識を持っていた僕に、上掲の質問が持った意味は何だっただろうか。この質問は、「自分は経験から学べない」という僕が持っている信念に対して、「それを反証するような事象はどのようなものか」、と問うものだ。自分についての信念というのは、それが自分のことであるがゆえに、他者による検証を受け付けない独断的なものになりがちだ。

「自分は〇〇なんです」というネガティブな言及に対して、「そんなことないよ!」と否定するのは簡単だが、あまり有効でない場合がある。その信念が、自分の行動にロックを掛けてコンフォートゾーンに留まることを許すような働きを持っていたり、セルフ・ハンディキャッピングのために作り出されていることがあるからだ。

「経験から学べない」という自分の発言も、経験から学ぶために必要な事柄ーーとりわけ、不都合な現実に向き合うことーーへの億劫さに由来しているもののように思う。「本を読んで得た知識を行動に写したり表現したりできる」というポジティブな面ではなくあえてネガティブな面を訴えるのには、自分でもまだよくわからない心理が働いているのだろう。

自己についての信念が、歪んでいたり、有害であったりする場合に、それを矯正するのには、当人の口から語らせることが有効なのだと思う。今回のnitt-sanさんの質問は、僕の提示した信念が反証される条件を僕自身に語らせることで、その条件を満たす事例を見つけたときに信念を修正せざるを得ない状況を作り出す効果があった。

自らの口で語った反証の条件を満たす場面に直面してなお、その信念を守ろうとするのであれば、その信念はその事実性ゆえにではなく有用性において指示されているものだということが明らかになるだろう。

感想:「社外」であることの意味

冒頭に書いたとおり、nitt-sanさんと僕は同じ会社に勤めているわけではない。会社を異にする者が1on1を行うことには、それゆえの制約がある。コンプライアンス的に話せない事柄があったり、お互いに仕事ぶりを含む普段の振る舞いを知らないためにコミュニケーションに齟齬を生じやすかったりというのは、十分に想定されることだろう。

しかし、いい面もある。まず、利害関係がないので、自己防衛的な語りをせずに済む。そのため、素直に発言できるというだけでなく、会社では取り繕っている(つもりの)自分に都合の悪い部分も他人の前に晒すことになり、フィードバックが受けやすい。上司からであればムッとさせられるようなアドバイスも、素直に受け入れやすい。また、社内ではありがちな「フィードバックと評価が密着してしまう問題」の懸念もない。そして、転職という話題も躊躇なく持ち込める。

今回の1on1は、自分にとって気づきの多い時間だった。以上に感想を記したのは、そのごくわずかな一部分である。言及したところ以外にも、ハッとさせられたやり取りが多くあったことのみ記述しておきたい。これからゆっくりと消化し、今後の自分の行動に活かしていきたい。読者の方におかれては、くれぐれにここに書いた事柄をもって、nitt-sanさんの1on1の全体を評価されることがないようにお願いしたい。

nitt-sanさん、本当にありがとうございました。

この1冊ですべてわかる 新版 コーチングの基本

この1冊ですべてわかる 新版 コーチングの基本

*1:これが「さかなクンさん」と同等の表現になるのかどうかはよくわからない。

*2:週一回ブログを書く会。

*3:本記事の執筆時点で、12人の方と実施されている。

StreamとOptionalに共通して存在するメソッド(その3・完結篇):map / flatMap

StreamとOptionalに共通して存在するメソッドの第三弾。 第一弾・第二弾はこちら。

ky-yk-d.hatenablog.com

ky-yk-d.hatenablog.com

これまで、下記のメソッドについて書いてきた。

  • filter()
  • of()
  • ofNullable()
  • empty()

完結篇となる今回は、map()flatMap()について。これまでに出てきたメソッドとも関連させながら書いていきたい。

マッピング関数を引数にとるメソッド

メソッド Stream Optional
map() Stream map​(Function<? super T,​? extends R> mapper) public Optional map​(Function<? super T,​? extends U> mapper)
flatMap() Stream flatMap​(Function<? super T,​? extends Stream<? extends R>> mapper) public Optional flatMap​(Function<? super T,​? extends Optional<? extends U>> mapper)

map()flatMap()は、いずれも「マッピング関数」を引数にとるメソッドだ。マッピング関数とは、一定の入力を一定の出力に対応づけする関数で、関数型インタフェースFunction<T, R>という型で指定されていることから明らかなように、ラムダ式やメソッド参照を代入することができる。

map()flatMap()のいずれも、元のStream/Optionalの要素を取り出し、マッピング関数で変換し、その返り値を要素として含む新たなStream/Optionalを返すメソッドだ。異なるのは、マッピング関数が返す値の型と、それをどのように新たなStream/Optionalに流すかだ。それぞれ見ていこう。

map()のマッピング関数は「要素そのもの」を返す

まず、わかりやすいmap()から。このメソッドは、Stream/Optionalの要素を取り出し、引数で指定されたマッピング関数を適用し、その返り値を要素とする新たなStream/Optionalを返す。挙動としては非常にわかりやすい。少し注意が必要なのは、マッピング関数の返り値がnullだった場合だ。

Streamのmap():マッピング関数の結果のnullも1つの要素として扱う

Streamの場合は、nullも一つの要素として新たなStreamに含められるのに対して、Optionalの場合は、nullをラップしたOptionalではなく、空のOptionalが出力される。コードを示す。

/* 動作確認用のstaticメソッド定義 */
class Sample {
    static Integer stringToInteger(String str){
        try {
            return Integer.valueOf(str);
        } catch (NumberFormatException e) {
            return null;
        }
    }
}
/* JUnit5を用いた動作確認 */
@Test
void Streamのmap() {
    List<String> src = List.of("123", "456", "変換できない文字列");
    List<Integer> list = src.stream().map(Sample::stringToInteger).collect(Collectors.toList());
    assertEquals(3, list.size()); // テストはグリーン(nullも要素の一つとして含んでいる)
    list.forEach(System.out::print); // 123456null と出力される
}

Streamのmap()は、マッピング関数の返り値がnullだった場合、それをそのまま新しいStreamの1つの要素として挿入する。map()の前後でStreamの要素の数は増減していない。

Optionalのmap():マッピング関数の返り値がnullの場合は空のOptionalを返す

これに対して、Optionalのmap()で、マッピング関数の返り値がnullだった場合は、(元のOptionalが空だった場合と同じく)空のOptionalを返す。

@Test
void Optionalのmap() {
    Optional<String> opt = Optional.of("123");
    Optional<Integer> result = opt.map(Sample::stringToInteger);
    System.out.println(result); // Optional[123] と出力
    opt = Optional.of("変換できない文字列");
    result = opt.map(Sample::stringToInteger);
    System.out.println(result); // Optional.empty と出力(Optional[null]とはならない)
}

これは、map()の実装において、マッピング関数の返り値をOptionalに包む際に、Optional.ofNullable()を利用しているためだ。実装は以下のようになっている。

public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();  
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

map()のまとめ

まとめると、以下のことが言える。

  • StreamとOptionalのmap()は、元のStreamないしOptionalの要素を置き換えた新しいStreamないしOptionalを返す
  • ただし、マッピング関数の結果がnullの場合に、Streamはnullをそのまま要素に含めるが、Optionalは空のOptionalを返す(nullをラップしたOptionalが返ることはない)

flatMap()のマッピング関数は「要素の入れ物」を返す

map()に渡すマッピング関数の返り値の型は? extends R(U)であるのに対し、flatMap()に渡すマッピング関数の返り値の型はStream(Optional)<? extends R(U)>である。map()マッピング関数が「要素そのもの」を返したのに対し、flatMap()マッピング関数はStream/Optionalであり、いわば「要素の入れ物」を返す。マッピング関数の入力がそもそも、(map()と同様に)元のStream/Optionalという「入れ物」の中身である要素だから、「入れ物の要素1つから新しい入れ物を作る」というのがマッピング関数の役割になる。

そして、flatMap()全体としては、マッピング関数の出力であるそれぞれの「入れ物」から要素を取り出し、新しいStream/Optionalに送り込む。マッピング関数の出力をそのままStream/Optionalに送り込むのではなく、「入れ物」から要素を取り出すことにより、flatMap()の戻り値はネストしたStream/Optionalにはならない(これがフラット化の意味だ)。

StreamのflatMap():「1対多の変換」はミスリーディング

Streamの場合、Streamという「入れ物」には複数の要素を入れることができるから、flatMap()の典型的な利用例として、入力のStreamの各々から複数の出力を取り出してそれを新しいStreamの要素とする、という「数を増やす」形の変換に利用される。実際、StreamのflatMap()APIドキュメントの「APIの注」という項目に、「1対多の変換」という表現がある。

flatMap()操作には、ストリームの要素に対して1対多の変換を適用した後、結果の要素を新しいストリーム内にフラット化する効果があります。

しかし、ここでいう「1対多の変換」が、必ずしも「数を増やす」ものではないということには注意が必要だ。例を示そう。StreamのflatMap()が使われるシンプルなケースの1つとして、以下のように、ネストしたコレクションからそれぞれの要素を取り出して処理の対象とする場合がある。

List<List<String>> src = List.of(List.of("123","456"), List.of("789"), List.of());
List<String> dest = src.stream().flatMap(List::stream).collect(Collectors.toList());
dest.forEach(System.out::print); // 123456789 と出力

上の処理において、flatMap()に渡されているマッピング関数List::stream()は、ラムダ式では list -> list.stream()と書くこともでき、List型の入力を受け付けて、Stream型の出力を返す関数となっている。このStreamの要素であるString型の値を、flatMap()が(List::streamではなくflatMap()自体の)戻り値であるStreamに流しこむ。

ここで、srcの要素であるList型のオブジェクトのうち、最初のものであるList.of("123","456")からは"123""456"という2つの文字列を取り出す1対2の変換を行なっているが、第三要素であるList.of()からは要素を取り出していないため1対0の変換になっている。「1対多の変換」の「多」というのは「0以上」という意味だということがわかる。この意味で「多」を利用することはプログラミングの世界では珍しくないが、ミスリーディングな説明だと思う。

1対0の変換:Java9で導入されたStream.ofNullable()とOptional.stream()

上で殊更に「1対0」の場合があると書いたのには意味がある。その意味は、Java 9 で導入されたメソッドである、StreamのofNullable()とOptionalのstream()を見てみるとわかる。

前回の記事で紹介したofNullable()は、引数がnullであれば空のStreamを返すファクトリメソッドだ。一方、Optionalのstream()は、Optionalが空であれば空のStreamを、空でなければその要素を含むStreamを返すメソッドであり、以下のような実装を持つ。

public Stream<T> stream() {
    if (!isPresent()) {
        return Stream.empty();
    } else {
        return Stream.of(value);
    }
}

これら2つのメソッドは、似たような場面で用いることができる。具体的には、下記のコードにおいて、2つのテストケースは同じ動きを示す。SampleクラスのstringToOptionalInteger()メソッドは、先に登場したstringToInteger()メソッドとほぼ同じで、異なるのは変換が失敗したときに、stringToInteger()がnullを返すのに対して、stringToOptionalInteger()は空のOptionalを返すという点だ(したがって、後者は返り値の型がOptional<Integer>になっている。

class Sample {
    /* 変換失敗時に空のOptionalを返すstaticメソッド */
    static Optional<Integer> stringToOptionalInteger(String str){
        try {
            return Optional.of(Integer.valueOf(str));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }
}

動作確認用のコードは以下の通り。

@Test
void StreamのofNullableの利用() {
    List.of("123", "456", "変換できない文字列").stream()
            .flatMap(str -> Stream.ofNullable(Sample.stringToInteger(str)))
            .forEach(System.out::print); // 123456 と出力
}
        
@Test
void Optionalのstreamの利用() {
    List.of("123", "456", "変換できない文字列").stream()
            .flatMap(str -> Sample.stringToOptionalInteger(str).stream())
            .forEach(System.out::print); // 123456 と出力
}

上記のテストメソッド、StreamのofNullableの利用()Optionalのstreamの利用()は同じ動きをする。前者ではnullを返すメソッドを呼び出し、それをStream.ofNullable()に渡すことでStreamを作っているのに対し、後者では空のOptionalを返すメソッドを呼び出し、それをstream()によってStream化している。いずれもマッピング関数の返り値はStream型になっていて、その要素がflatMap()により新しいStreamに投入されている。

ここでは、flatMap()は、(先に見た例のように)入力のStreamの各要素から複数の要素を生み出す(数を増やす)ために用いられているのではなく、むしろ数を減らすために用いられている。この例で用いているStream.ofNullable()Optional.stream()の両方がJava9で導入されたメソッドなのは、利用方法から考えると納得がいくことではないだろうか。

OptionalのflatMap():1対0 or 1対1の変換

さて、以上のように考えると、OptionalのflatMap()も理解しやすい。これもまた、StreamのflatMap()同様に「1対多の変換」を行うマッピング関数を受け取るものであり、たまたまOptionalというものの性格上、数を増やすためには用いることができないのだ、という理解の仕方もできるだろう。flatMap()は、以下のように実装されている。

public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        @SuppressWarnings("unchecked")
        Optional<U> r = (Optional<U>) mapper.apply(value);
        return Objects.requireNonNull(r);
    }
}

map()マッピング関数の返り値がnullだった場合の挙動と、flatMap()マッピング関数の返り値が空のOptionalだった場合の挙動は同じで、いずれも空のOptionalが返る。「要素ありのOptional→空のOptional」という置き換えで行われている変換を「1対0の変換」と呼ぶならば、Optionalのmap()flatMap()はいずれも1対0の変換を行うことができるものだと言える。どちらを使うかは、マッピング関数で利用したい関数がnullを返すのか空のOptionalを返すかによって決めればいい。これは、Streamのmap()がnullを要素として扱い、要素の数を減らすような動きはしない(nullを消すためには先述のようにflatMap()を利用する)のと異なっている。

まとめとモナドに関する参考記事

3回に渡って、StreamとOptionalに共通して存在するメソッドについて書いた。自分自身の理解を作るためという趣旨で書き始めた記事だったが、今回記事にするために挙動を一つずつ確認していくなかで、StreamとOptionalの同一名称のメソッドが似ているようで少しずつ違うのがわかった。同じ名称だと仕様の違いに鈍感なまま雰囲気で使ってしまいがちだが、今後は細かい挙動の違いも意識してコードを書くことができると思う。

なお、今回の記事を書いている過程で、モナドというキーワードに接した。Haskellを学ぶと出会うことになる概念という印象を持っていたが、OptionalとStreamというJava関数型プログラミング的な側面においてはこの概念が顔を出すようだ。詳しくは下記に列挙した記事をご覧いただきたい。

d.hatena.ne.jp

qiita.com

StreamとOptionalに共通して存在するメソッド(その2):of / ofNullable / empty

StreamとOptionalに共通して存在するメソッドの第二弾。 第一弾はこちら。

ky-yk-d.hatenablog.com

前回の記事では、filter()について書いた。今回は、of()ofNullable()について書……こうと思ったら実はempty()も共通して存在するメソッドだったことに気づいたので、これも併せて3つのメソッドについて書く。

ファクトリメソッド: of()ofNullableempty()

メソッド Stream Optional
of() static Stream of​(T t) public static Optional of​(T value))
ofNullable() static Stream ofNullable​(T t) public static Optional ofNullable​(T value)
empty() static Stream empty() public static Optional empty()

of()ofNullable()empty()は、StreamとOptionalのいずれにおいてもstaticのファクトリメソッドだ。Streamはクラスではなくインタフェースだが、Java 8からはインタフェースにstaticメソッドが書けるようになっているため*1、ファクトリメソッドを定義することができる。実際には、下記のように定義されている。

public static<T> Stream<T> of(T t) {
    return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

public static<T> Stream<T> ofNullable(T t) {
    return t == null ? Stream.empty()
                     : StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

public static<T> Stream<T> empty() {
    return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
}

対して、Optionalのそれぞれのメソッドの定義は下記の通りだ。

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

こうして並べてみると、StreamとOptionalのそれぞれにおいて、of()ofNullable()という2つのメソッドの関係が同じであることがよくわかる。of()が引数で受け取った何らかの値をそのままStreamないしOptionalに包んで「返そうとする」のに対して、ofNullable()は引数がnullだった場合は空のインスタンス(それぞれのempty()メソッドで取得する)を返し、nullでない場合はof()と同じ結果を返すようになっている。空のインスタンスについては、その挙動を含めて次回に回す。

of()にnullを渡した場合の挙動の差異

それでは、of()の引数にnullを渡した場合の挙動はどうなるのか。これはStreamとOptionalで異なっており、Streamのof()が、単に(nullが実体を持ったオブジェクトであるかのような表現には語弊があるが)「null 1つ」を要素とするStreamを返すのに対し、Optionalのof()NullPointerExceptionをスローする*2。下記のコードで実際に動作を確認できる。

/* JUnit5を利用 */
@Test
void Streamのofはnullを含むStreamを返す() {
    List<Object> list = Stream.of((Object)null).collect(Collectors.toList());
    assertEquals(1, list.size()); // 要素数は1
    assertNull(list.get(0)); // その要素はnull
}
@Test
void Optionalのofは例外をスローする() {
    final String helloWorld = "Hello World";
    Optional<Object> opt = Optional.of(helloWorld);
    Exception exception = null; // スローされたら格納しておく
    try {
        opt = Optional.of(null);
        fail("例外がスローされるので到達したらいけないコード");
    } catch (Exception e) {
        exception = e;
        assertEquals(NullPointerException.class, e.getClass());
    }
    assertEquals(helloWorld, opt.get()); // 当初の値のままであるはず
    assertNotNull(exception); // 例外がスローされていることを確認
}

その他の違い

以上のように、of()にnullを渡した際の挙動はStreamとOptionalとで異なっている。その他に、異なる点としては、

  • Optionalのof()には(Optionalの性質上)単一の引数のものしか存在しないのに対して、Streamのof()にはof​(T... values)という可変長引数のものも存在している。
  • これら6つのメソッドのうち、StreamのofNullable()だけは導入されたバージョンがJava 9であり、一世代新しい。

という点が挙げられる。

感想と次回予告

じっくりと仕様と実装を見てみると、普段いかに雰囲気で使ってしまっているかに気づくことができた。とはいえ、ファクトリはファクトリでしかないので、それぞれの生成の仕方にどのような意味があるのかは、生成が利用される場面とセットでなければわかりにくい。次の記事では、map()flatMap()を紹介しながら、今回の記事で扱ったファクトリをどのように利用するかにも言及したい。

*1:また、インスタンスメソッドについてはdefaultキーワードを用いてデフォルト実装を定義することもできるようになっている。

*2:スローするのはof()の中で呼び出しているOptionalのコンストラクタ、更に言えばそこで呼び出しているObjects.requireNonNull()だ。