若くない何かの悩み

何かをdisっているときは、たいていツンデレですので大目に見てやってください。

5年前に私の中で起きた理解しやすさのブレークスルーを振り返る

10年前から文章の理解しやすさの認識がだいぶ変わった。昔は論理式やそれに相当する固い説明がどっさり書かれている文章を理解しやすいとは思わなかったが、今ではコンテキストさえ掴んでいれば論理式やそれに相当する固い説明で書かれた文章の方が理解しやすいと感じる。たとえば論理式やそれに相当する固い説明がどっさり(本当にどっさり)書いてある ソフトウェア科学基礎型システム入門 を理解しやすいと今は思う。たとえば、ソフトウェア科学基礎には以下の記述がある:

s が無限回含まれるとは,任意のi ≥0 について,s = s j となるあるj ≥ i が存在することである.

── ソフトウェア科学基礎 p.198

昔だったら、この記述を理解しやすいとはきっと思わなかっただろう。「s が無限回含まれる」なんて論理式の説明がなくともわかるし固い説明なんてしなくていいのではと思っていははずだ。しかし今ではこの記述を理解しやすいと感じ、さらにこのような記述はあるべきだとまで思っている。

この認識の変化はなぜ起きたのだろうか。振り返ると、数学基礎論の初歩を理解したことが鍵だったように思う。

数学基礎論 の初歩を理解すると、論理式の意味の理解や、形式的な証明をできるようになる。私はここに とてつもない価値 を感じている。しかし世間を見渡してみればこの要因の価値がわかる人とわからない人の真っ二つに分かれるのではないか。そこで価値がわからない人向けに説明を試みる。この価値のわからない人は「論理式の意味がわからなくとも、形式的な証明ができなくとも、何か物事を理解することはできる」と考えていると思う。そこで思考実験をしてみよう。ある難しい理論を易しく説明すると謳う文献が理論の当てはまる例を1つ2つだけ紹介している。この文献を読めばその理論を精密に理解できるだろうか?私は例をいくつか知っているだけでは深い理解にはならないと思う。深い理解とはその理論の対象について多くの主張の帰結を短時間に回答できることだと考えているからである。この定義の上で考えると、例を2つ3つ紹介するような易しい文献を読むだけでは多くの主張の帰結を回答できるようにはならない。ただ短時間のうちに紹介されている例については主張の帰結を回答できるようになる効果はあると言えるだろう。ただ、具体例はわかりやすいがそれだけの説明だと他にどんな例があるかの推測を正確はできない。一方、論理式やそれに相当する固い説明で書かれた文書は親切にも例が書かれていない限り短時間に主張の帰結を回答できるようにはならない。しかし、ひとたび時間をかければ急激に帰結を回答できる主張が増加していき、易しい文献の理解度をいつか追い越す。これを図にすると次のようになる:

易しい文献と固い文献の理解度の時間的変化

この差は思考実験のしやすさに由来している。論理式やそれに相当する固い説明は具体的な対象を割り当ててその真偽を厳密に判断できるため、読者側で実験しすぐに結果が得られる。しかし例を2つ3つ紹介されただけでは、正確に他の例を思いつくことには手間がかかる。似ている例を直感的に挙げることはできるだろうが、それが本当に説明されている理論に当てはまるのかどうかの検証に時間がかかるからである。余談であるが、よく練られた文献は論理式や固い説明を書きつつ例を添えることでそれぞれのいいとこどりをしている(代わりに文量が増えるため鈍器と化す)。このことを一般化すると、個別の例の説明は素早く理解度を上げられるが深い理解には至れず、膨大な例を端的に記述できる説明は深い理解に至れるが抽象的なため理解に時間を要するということだろう。そして深い理解に至るためには抽象的な説明を精密に記述できる言語が必要なのである。そしてそのような言語として論理式が知られている。だから論理式の意味を知ることが何かを深く理解する上で鍵になってくるのである。

ただ論理式を知るだけでは片手落ちである。論理式の意味がわかるだけでは思考実験をしにくいからである。論理式は、決まった手順で論証することで前提が正しければ帰結もまた正しいと言える性質を備えている1。ここでこのような論証のことを 形式的証明 という。数学の証明もそれが正しければ形式的に証明できる2。ただし義務教育で習う数学のほとんどの証明は非形式的な証明であることから形式的な証明に馴染みのある人は多くない。形式的な証明の最大のメリットは機械による支援が受けられることである。定理証明支援系という形式的な証明を支援するツールがある。IsabelleRocqLean などさまざまなものが知られている。これらを使うと、主張が形式的な論証に沿っているかの確認だけでなく、主張の自動証明や反例探索ができる。これによって、論理式を使って機械に支援されて主張を実験できるのである。

もうひとつ、言語化できないものを人は理解できないという点も指摘しておきたい。思考は言語に制約されることを痛感したできごとを2つ紹介しておく。

10年前の私にはさっぱり証明の思いつかなかった Promise.all と race の組み合わせによる待ち合わせの表現力の証明を、その5年後にできるようになった。私が10年前に証明を思いつかなかった理由はあり得るケースを網羅的に表現する言語を知らなかったからである。その5年後には 操作的意味論をかじり、また状態遷移モデルを扱えるようになったため、前者なら「どのように評価されても」、後者なら「どんな遷移経路でも」と表現できることを知っていた。 この知っている言語の差が 10 年前の私に証明できなかった主張をその 5 年後には(否定的に)証明できるようになったという違いを生んだ。

また 10 年前の私はGUIアプリケーションの仕様とは何かを言語化することができなかった。10年経った今では画面仕様書への静的検査器 を開発できるほど、GUIアプリケーションの仕様とは何かを説明できる。10年前と今の違いは2つある。1つは仕様とは何かという理解の深さの違い、もう1つは膨大な遷移経路のあるモデルを端的に表現するために知っている語彙の多さの違いである。10年前の私は仕様が何かをはっきりと説明することはできず、これまで読んできた「仕様書」と呼ばれるものの共通項を挙げるぐらいしかできなかった。今の私は仕様がどんな実装についてもその正誤を機械的に判定する基準であると知っている。また、GUI アプリケーションは画面を行って戻ってをいくらでも繰り返せることが多く、その遷移経路の数は無限大であることが多い。すると仕様で許容したい遷移経路の集合や、実装で生じる遷移経路の集合を現実的な記述量で表現する言語が必要になる。10年前の私はそれを知らなかった。10年経った今の私は 時相論理CSP による表現を知っている。そのため、与えられた実装が仕様を満たすかどうか機械的に判断する基準を端的に記述できる。

前者も後者も、言語を知ることでできなかったことができることに変わっている。言語がこんなにも私のできることを広げてくれたのである。それにともなって理解しやすさの感覚が変わったのだろう。

まとめ

文献の理解しやすさの感覚の変化を振り返った。今の私は次のように考えている:

  • 深い理解は個々の事例からではなく網羅的な思考実験によりもたらされる
  • 効率的な思考実験の鍵は論理式と形式的証明である
  • 人間の思考は言語に制約される。できないことをできることに変えるため言語を知ろう

  1. 健全な論理体系であれば。
  2. 完全な論理体系であれば。

仕様の静的検査器のブログに対する匿名ダイアリーへの回答

仕様の静的検査器のブログ記事について質問や添削を匿名ダイアリーでいただいたので私の意見を回答します。

anond.hatelabo.jp

前書き

まず上記の匿名ダイアリー記事を書いてくださった方へ。記事を閲覧しさらにご意見を書いてくださったことに感謝いたします。 おそらく著者様は検証に携わる立場の方なのかなと推察します。いただいたご意見は大変参考になりました。 ここに私からの回答を付け加えることで、記事の訂正及び補足ができると嬉しいなと思っています。

本文

「実装のふるまいを誤っていると判断する」のは本当に仕様の欠陥といえるのか?

→ テストで気づくことを想定しているんだろうけど、テストは仕様書から作られるわけで、テスト設計ミスでは?

「本来正しいと意図した実装の振る舞いを誤っていると判断したり、その逆に誤っていると意図した実装を正しいと判断する仕様には欠陥があります」の読点前の部分に対する疑問でしょうか。以下はそうだと仮定して話を進めます。もしそうでなければ読み飛ばしてください。

まず元記事の文を図解し、元記事の意図していた仕様の定義を確認します。 というのも仕様には複数の定義があり、この記事ではそのどれを意図しているのかを明らかにしなければよい議論にならないからです。 なお元記事で採用している仕様の定義は、数理論理学に立脚した仕様の定義で形式仕様と呼ばれるものです。

図1の矩形は過去・現在・未来にわたってありえるすべての実装の集合を意味しています。

元記事における仕様は、図2のようにこの実装の集合を正誤判定によって2つの部分集合へと分割する役目をもちます。

仕様記述をする際、仕様記述者の脳内には意図した仕様があるはずです。 これが図3の左上です。 実際に記述された仕様は図3の右上です。 この左上と右上が一致しなかったとき仕様に欠陥があります(図3の②と④の部分)。

元記事の読点前の部分は図3の②を指し、読点後の部分は図3の④を指します。

図3の②の典型的な例は記事中に示している矛盾した記述です。 他にもパスワード入力画面にあるはずのパスワード入力フォームの配置の指示を書き忘れてしまってパスワード記入フォームが存在しないことが正解という仕様書になってしまっていたケースもこの例に当てはまります。 このいずれの例も「テスト設計ミス」と呼ばれることに違和感があります。

「誤っていると意図した実装を正しいと判断する仕様」は、通常の開発において起こり得るのか?

要求定義〜仕様作成〜テスト設計の流れを踏む以上、このような逆転は起こりにくいのでは?

この話は入力によって出力が一意に決まるシステム、すなわち関数的なシステムの仕様である、事前条件・事後条件の組を表で表したときに起こりうる例がわかりやすいと思います。 事前条件・事後条件については「契約による設計」に解説があります。 大雑把にいうと、事前条件はシステムに入力してよい入力を決める条件です。 事後条件は、事前条件を満たす入力について、どのような出力を返さないといけないかを決める条件です。この事前条件・事後条件の表し方はいくつかあります。 ここでは入出力の表として表現したとし、事前条件を満たさない入力は行を書かかないこととします。 これには2つの理由があります。

  1. 事前条件を満たさない入力は無数にあり、その全てを表に書ききると膨大になってしまい読みづらくなるため
  2. 事前条件を満たさない入力について実装はどのように振る舞ってもよいため

例えば FizzBuzz 関数の仕様をこの規則にしたがって表にすると次のようになります:

入力 出力
1 "1"
2 "2"
3 "Fizz"
4 "4"
5 "Buzz"
... ...

注目して欲しいのは、0 以下の整数や浮動小数点数、文字列などは事前条件を満たさないことから表に書いていないところです。 このときFizzBuzz 関数の仕様の表に 100 のときの入力だけ書き忘れてしまったとしましょう。 すると100 の入力は事前条件を満たさないということになり、本来実装に100を入力すると"Buzz"が出るべきなのにどのように振る舞っても正しいと判定されてしまいます。 つまり100以外の入力についてはすべて仕様を満たすが100の入力に対する出力を"100"とする実装や"FizzBuzz"とする実装が正しいと判定されてしまいます。 これはまさに誤っている意図した実装を正しいと判定してしまう仕様を書いてしまった例になっています。

実装が顧客の要求にたまたま合っていたとしても、それが仕様書に沿っていないなら「正しい実装」とはいえないのでは?

→ この場合、実装がたまたまあっていただけで、テストもされていない状況。仕様も実装も両方が誤っていたというべきでは?

仕様と同じように要求にも複数の定義があるため用語を整理してから解釈を説明します。 私は Michel Jackson の要求の定義を採用しています。 この定義では、問題領域に属する事柄が要求であり、機械領域に属する事柄が仕様であるとされます。 また、仕様が要求を満たしかつ実装が仕様を満たすならば自動的に実装は要求を満たす、というふうに要求と仕様、実装の間の関係が満たすべき性質が与えられています。

ここで、要求と仕様、要求と実装のそれぞれの間で満たすべき性質を妥当性(Validation)といい、仕様と実装の間で満たすべき性質を正当性(Verification)といいます。 この定義では、おっしゃる状況は実装が妥当であるが正当ではない状況ということになります。

そして元記事では意図しない妥当性違反のみをスコープとし、意図した妥当性違反と正当性違反についてはスコープ外1とするよう構成しました。 この立場からおっしゃる状況を解釈すると、仕様書に沿っていない実装は正当でない、つまりおっしゃる通り正しくない、となります。 元記事はこの解釈で一貫して書いているつもりです。

「さて」が不要

接続語としての意味が弱く、文の格調を下げている印象。

参考になりました。ありがとうございます。

仕様や満たすという言葉の用法の定義直後に「仕様の欠陥」の話が始まり、論点が拡散する

仕様の性質や運用について触れた後に、欠陥の話題を出した方が構成として自然。

「運用」を仕様という成果物を取り巻くプロセスというふうに解釈して回答します。 もしそうでない場合は読み飛ばしてください。

仕様がどう使われるかによって仕様の備えるべき性質が決まります。 したがって仕様という成果物を取り巻くプロセスが重要であるというご指摘はごもっともです。 ただそれが元記事において説明する優先度は低いと判断しここに関する説明をしていません。

今回の構成は、仕様の欠陥を見つける静的検査器を実装した成果を説明するために以下の構成をとっています:

  1. 用語定義

    用語の混乱を避けるために元記事で用いる「仕様」および「仕様を満たす」、「仕様の欠陥」の意味を定義した

  2. 問題提起

    仕様の欠陥はあるべきではないが現実にはあるというギャップを問題として提起した

  3. 解決方法

    仕様の欠陥を防ぐアプローチとして仕様書の形式を整え静的検査器を実装し解決を頃みた

  4. 今後の展望

論点が拡散するように見えたのは1の文量が多く、そこが主題だとご認識いただいたからかもしれません。 ただ私は3を主題として設定しているつもりです。 そのため仕様という成果物を取り巻くプロセスについてはあまり言及していません。 1が長くなってしまったのは「仕様」という複数の定義をもつ語を混乱しないように解釈してもらうための苦肉の措置でした。

仕様における「正しさ」の基準と、実装やテストの誤りの区別が曖昧

仕様の欠陥と、テスト設計・実装のミスは分けて扱うべき。

元記事では次のようにそれぞれの欠陥を定義しました:

仕様の欠陥
意図と表現物の相違による妥当性違反、意図と表現物の相違によらない妥当性違反(元記事のスコープ外)
実装の欠陥
正当性違反(元記事のスコープ外)、妥当性違反(元記事のスコープ外)

これによればいずれも厳密に区別される認識です。 そうでない場合、ぜひご指摘をいただきたいです。

例示が適切でない

 → 「仕様と実装がどちらも間違っていたが、結果的に要求に近かっただけ」のケースであり、 「正しい実装を誤っていると判断した仕様」ではない。

言及先の文を見つけられませんでした。 よろしければ言及先の文をご教示願います。

「無価値」という語の使用が不用意に攻撃的

仕様のすべてが誤っているわけではない状況であるので、特定の部分が誤っている・矛盾しているという事実の指摘にとどめるべきで、感情的な評価語は避けた方が望ましい。

「無価値」という言葉が感情的な評価語であるという認識を持っていませんでした。 そのような解釈があることに気づけていなかったため、ご指摘に感謝いたします。

私の解釈では、ソフトウェア開発プロセスに登場するすべての成果物はそれに誤りがなければ(客観的な)価値があるというという認識を持っています。 ここから仮に(客観的に)無価値なものがあるとしたらその成果物に誤りがある、というふうに背理法で導くことが本記事の意図でした。 客観的な価値評価は合理的なソフトウェア開発プロセスにおいて極めて重要な概念であると認識しています。 「無価値」を感情的な評価語であるとするのはおそらく主観による評価であるからだと思っていますが元記事ではそれを意図していません。

ていうか、仕様矛盾の例示いらなくない?

単なる文言の誤りをここまで冗長に書く必要ないし、無用に攻撃的だしで、なんだかなという感じ。ここ以外もなんだけど、全体として実装やってるやつは悪くないんだ!って気持ちがあふれてる感じがする。気持ちはわかるが書きたいことと関係なくない?

これはのちに登場する静的解析期の検査ルール12に対する事前の説明を意図して書いたものです。 特定の誰かを悪くいう意図はありませんでした。 そのような意図に取られる表現であったことをお詫びいたします。

添削後( 「仕様に欠陥があるとどうなるか」も別に言いたいことと直接関係ないだろうし軽く触れる程度でいいでしょってことでマージした場合)

仕様の定義にはいくつかの解釈がありますが、ここでは「仕様」を、要件定義に基づいて作成され、実装の正しい振る舞いを定める基準と定義します。実装が正しいと判定される場合、それは実装が仕様を満たしていることを意味します。

要件定義を元に作成された仕様に誤りがあった場合、実装の段階でその誤りに気づくことは難しいことが多いです。このような誤りは、通常、顧客レビュー(受け入れテストやUAT)で判明します。しかし、顧客とのコミュニケーションコストや調整が必要になるため、テスト段階で問題を発見するよりも、対応に要する工数が多くなりがちです。

この添削は私の意図した構成から大きく逸れたものになっています。 前述の通り、ここは用語を厳密に定義することでその後の説明の意味解釈の混乱を避けるために用意したセクションです。 「要件定義」や「要件定義に基づく」といった複数の定義があり混乱している用語がその定義の説明なしに登場することは望ましくありません。 また「仕様」の定義の説明がほとんどなくなってしまったことも残念に感じます。

後書き

上記の匿名ダイアリー記事を書いてくださった方へ。記事を閲覧しさらにご意見を書いてくださったことに感謝いたします。 私の見解や意図を説明してみましたのでもしよろしければまたご意見・ご指摘をいただけると嬉しく思います。


  1. 元記事の該当部分「別の欠陥の例として、仕様が意図どおりだったとしても、その仕様を満たした実装が解決したかった問題を解決できなかった(e.g. 売り上げが目標に至らなかった)というものもあり得ます。 ただし本記事ではこの種類の欠陥はスコープ外としています」
  2. 元記事の該当部分「PlantUMLで記述された状態遷移図と各画面それぞれのUI要素表のインタラクション列に出現する画面IDを付き合わせた辺の整合性の検査5。画面遷移図内のすべての遷移がUI要素表にあり、かつUI要素表にあるすべての遷移が画面遷移図にあればOK、それ以外はNGと判定」

画面仕様書への静的検査器を実装したらたくさんの欠陥を発見できた話

社のブログで最近の成果を公開しました。ぜひご覧ください。

swet.dena.com

画面仕様書への静的検査器を実装したらたくさんの欠陥を発見できた話 --- SWET第二グループの[Kuniwak](https://kuniwak.com/)です。本記事では画面仕様(後述)の仕様書に対する静的検査器を開発した事例について紹介します。 ## 伝えたいこと 1. 画面表示と画面遷移を記述する仕様書は機械可読にできる 2. 仕様書が機械可読であれば仕様の静的検査ができる 3. 静的検査によって自身の担当範囲の15%の画面から計40件弱の欠陥を発見した 4. 機械可読な仕様書にはさらなる応用が見込める ## おさらい:仕様とは 仕様の定義はいくつかあります。 ここでは仕様とは実装の正しい振る舞いを定める基準とします。 ある実装が正しいと判定されることを、実装が仕様を満たしたといいます。 誰による判定でも実装が仕様を満たしたかどうかの判定結果は一致すべきです。 さて実装の欠陥と同様に、仕様にも欠陥が生じえます。 本来正しいと意図した実装の振る舞いを誤っていると判断したり、その逆に誤っていると意図した実装を正しいと判断する仕様には欠陥があります。 仕様の欠陥の典型的な例の1つに矛盾した記述の存在があります。 矛盾した記述を含む仕様はどんな実装でも満たせません。 たとえば、ある箇所でログイン画面のボタンのテキストが「ログイン」と指示されているのに、別の箇所で「サインイン」と指示されている場合、どんな実装でもこの両方を同時に満たすことはできません。 すると、仕様を満たせる実装が1つもないということになります。 仕様を満たせる実装が1つもない仕様は無価値ですから、実際には正しいとしたかった実装はあったはずでしょう。 まさにこの例は正しいとしたかった実装を誤っていると判断する仕様になっています。 別の欠陥の例として、仕様が意図どおりだったとしても、その仕様を満たした実装が解決したかった問題を解決できなかった(e.g. 売り上げが目標に至らなかった)というものもあり得ます。 ただし本記事ではこの種類の欠陥はスコープ外としています。 ## 仕様に欠陥があるとどうなるか さて、仕様に欠陥があるとどうなるのでしょうか。 開発プロセスにおける仕様の利用者は主に2つです: * 実装者:実装者の役割は仕様を満たす実装を提供すること * 検証者:検証者の役割は実装が仕様を満たしていることを確認すること 仕様に欠陥があると、それぞれの利用者に次の悪影響があります: 1. 本来不要だったコミュニケーションコストの発生 - 実装者または検証者が仕様を不審に思えれば実装前に仕様への質問でこれを発見できます。ただし本来不要であったコミュニケーションです 2. 無駄な実装・検証コストの発生 - 検証者が不審に思えなければそのとおりに実装されてしまい、仕様策定者がその実装を触るまで意図どおりでないことに気づけません。運よく出荷前に気づけた場合でも一部または全部の実装が無駄になります 3. 信頼の失墜 - 運悪く出荷前に気付けなければ、エンドユーザーからのお問い合わせ等で発覚することになります。その場合は実装が無駄になるうえ、エンドユーザーからの信頼を損ないかねません ここまでのまとめです: 1. 仕様とはシステムの正しい振る舞いを定める基準です 2. 実装と同様に仕様にも欠陥を考えられます 3. 仕様に欠陥があると余計なコストの発生や信頼の失墜が発生しえます ## 画面仕様とは 本記事で扱う仕様は、GUIアプリケーションの画面の見た目と画面間の遷移にまつわるものです。 この仕様を**画面仕様**と呼び、今回は画面表示仕様(後述)と画面遷移仕様(後述)の2つの組であるとします。 ここで**画面**とは主にGUIの見た目によってグルーピングされたアプリケーションの状態の集合です[^1]。 たとえば一般的なログイン画面を例に画面について考えてみましょう。 ログインという操作には入力フォームの入力状態や認証サーバーとの通信状態が関与します。 これらのありえる組み合わせからなる状態の集合がログイン画面です。 [^1]: 実際には見た目が似ていても別の画面として扱った方がよいこともあります。たとえば一般ユーザーと特権ユーザーが別れているサービスがあったとして、それぞれのログイン画面の見た目を似せることはできますが、そこに到達するまでの経路およびそこから遷移する先の画面が大きく異なる場合、別の画面として扱った方がわかりやすくできるでしょう。 また画面内の状態遷移や画面間の遷移は、UI要素への操作(例:クリックやホバー、スクロール)やサーバーとの通信、時刻などを引き金として起こります。 この引き金のことを**イベント**と呼びます。 ### 画面表示仕様とは 多くの画面は入力フォームやボタンなどのUI要素を決まった位置に配置されています。 画面内のそれぞれの状態ごとに、UI要素をどんな見た目でどの位置に配置するかを指示する仕様が**画面表示仕様**です。 なお個々のUI要素のとりうる状態や見た目、受け付けるイベントについて画面表示仕様とは別にあらかじめ**UI要素仕様**として定めるのが一般的です。 そうすることで複数の画面で共通するUI要素の仕様をそれぞれの画面表示仕様内に重複して記述しなくともよくなります。 本記事ではUI要素の表示や状態遷移にまつわる仕様をUI要素仕様で別に定めているとし、画面表示仕様では(1)UI要素の配置と、(2)画面内のUI要素間の相互作用による振る舞いを記述することとします。 ここで**UI要素間の相互作用**とは、イベントによって複数のUI要素の状態が連動することをいいます[^2]。 たとえば画面に2つのUI要素として入力フォームとテキストが配置されているとします。 そして入力フォームへキーボード入力というUI操作をおこなった結果、その入力が入力規則に違反していればテキストにエラーメッセージが表示され、違反していなければテキストを非表示にされる、という振る舞いはUI要素間の相互作用による典型的な振る舞いです。 [^2]: CSPにおける並行合成を意図しています。 ここで画面表示仕様の例を、UI要素の配置とUI要素間の相互作用による振る舞いの順でみていきましょう。 まずUI要素の配置は、状態によってUI要素の配置が変わらなければ代表的な状態におけるUI要素の配置を示せば十分です。 たとえば次のログイン画面が状態ごとにUI要素の配置が変わらないとすると、このスクリーンショットで十分です。 !(https://cdn-ak.f.st-hatena.com/images/fotolife/s/swet-blog/20250416/20250416150013.png) 状態によってUI要素の配置が異なればそれらの配置の代表的な状態の配置を示せば十分です。 次にUI要素の間の相互作用は、UI要素の配置図に加えて何らかの表現でそれを記述する必要があります。 本事例ではこれを自然言語によって記述しました。 前述の入力フォームとテキストの振る舞いの説明のように記述しています。 なお実装がこの画面表示仕様を満たしたと判断する基準は、大雑把にいうと仕様と実装に同じイベントを与えた結果の見た目が一致することです[^3]。 この画面表示仕様の指示する見た目の集合はUI要素の配置とUI要素ごとの状態から計算できます。 [^3]: 仕様と実装の両方が決定的かつ内部イベントによる遷移を含まない場合です。非決定的な場合はトレース実行後の仕様と実装の両方の見た目は集合となります。このときは実装の見た目の集合が仕様の見た目の集合に包含されるか否かで判定します。内部イベントによる遷移を含む場合は不安定な状態を比較対象から取り除くとよいでしょう。このときクライアントとサーバー間の通信中の画面表示の指示もしたいことがほとんどですから通信によるイベントを隠蔽しない工夫が必要になるでしょう。 ### 画面遷移仕様とは ほとんどのGUIアプリケーションは、ユーザーが画面を遷移しながら操作していくことを意図されています。 このような画面内の状態を点とし、イベントを辺としたラベル付き有向グラフを本記事では**画面遷移仕様**と呼びます。 たとえばログイン画面内のユーザー名・パスワードが未入力な状態`S_0`に正しいユーザー名`Taro`とそのパスワードの組を入力すると`S_Taro`へと遷移する場合、`S_0`と`S_Taro`の間をユーザー`Taro`の正しい認証情報の入力という辺で結びます[^4]。 [^4]: この図はわかりやすさのために素朴に状態とイベントを描いています。実用的には状態変数やガード、事後条件を使ってより見通しのよい図にする方がよいでしょう。 !(https://cdn-ak.f.st-hatena.com/images/fotolife/s/swet-blog/20250414/20250414134448.png) この状態遷移グラフの表現方法はいくつかあります。 上に示した状態遷移図や状態遷移表がその代表的な候補です。 なお実装がこの画面遷移仕様を満たしたと判断する方法については説明が長くなるため割愛します。 本事例では[Communicating Sequential Process (CSP)知りたい方は](https://ja.wikipedia.org/wiki/Communicating_Sequential_Processes)という理論を背景にしているため、気になる方はCSPにおける「詳細化」という概念を調べてください。 CSPおよび詳細化については「[並行システムの検証と実装(磯部 祥尚 著、近代科学社)](https://www.kindaikagaku.co.jp/book_list/detail/9784764904354/)」がわかりやすいです。 ## 画面仕様を機械可読にする方法 今回の取り組みは[Confluence Wikiマークアップ](https://ja.confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html)で記述された既存の仕様書が画面仕様として十分な情報を持っていないところからスタートしました。 そこで既存の仕様書が画面仕様として十分な情報を持てるように画面ごとにそれぞれを次のようなUI要素の配置図(画像左)とUI要素表(画像右)の組を書くようにしました。 ![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/swet-blog/20250414/20250414134453.png) この`Scr.001`は画面IDで`ログイン画面`は画面名です。 本事例の仕様書ではすべての画面に画面名だけでなくIDをつけました。 画面はさまざまな箇所で言及され(例:UI要素表内の画面遷移)、その際に言及先の画面を一意に特定するためのIDが必要になるためです。 画像左はUI要素の配置図で画面表示仕様の一部です。 画像右はUI要素ごとにIDや種類、表示条件、表示内容、インタラクションを記述します。 表示条件や表示内容は画面表示仕様の一部です。 インタラクションは画面遷移仕様の一部です。 このようにUI要素表を組むことで、Confluence Wikiマークアップを解析すれば画面遷移仕様を読み取れるようになります。 また状態遷移図は画像ではなくPlantUMLマクロで描画することによって機械可読にしています。 ## 機械可読な画面仕様への静的検査 機械可読な画面仕様に対して静的な検査が可能です。 今回の事例ではGo言語で6000行弱の静的検査器を実装しました。 この静的検査器は23の検査ルールを持っています。 この検査ルールは、エンジニアの間で事前に洗い出しておいた仕様インスペクション観点がもとになっています。 この事前に洗い出された仕様インスペクション観点は20ほどあり、いくつかを抜粋します: 1. 画面遷移図とUI要素表が整合すること 2. インタラクション可能なUI要素(ボタン・チェックボックス等)にインタラクションの記載があること 3. UI要素の種類がリストのものについてはリスト内のUI要素の並び順の指示が明確であること 4. 動的画像は表示範囲に対して画像の大きさが異なるとき拡大縮小の指示が明確であること 5. ... 今回はこれらの観点のうち8つを自動化できました。 上の例であれば次のような基準で実装されています: 1. PlantUMLで記述された状態遷移図と各画面それぞれのUI要素表のインタラクション列に出現する画面IDを付き合わせた辺の整合性の検査[^5]。画面遷移図内のすべての遷移がUI要素表にあり、かつUI要素表にあるすべての遷移が画面遷移図にあればOK、それ以外はNGと判定 2. 種類がボタンなどの場合にインタラクションの列に記載があればOK、なければNGと判定 3. 種類が動的または静的リストの場合、「順」という文字が表示内容に出現すればOK、なければNGと判定 4. 種類が動的画像の場合、「拡大」「縮小」「見切れ」という文字列のいずれかが出現すればOK、それ以外はNGと判定 5. ... [^5]: 本来は画面間の大雑把な遷移関係ではなく、個々の状態間の遷移関係を特定できるように状態変数やガードや事後条件を機械可読な形式で記述すべきです。しかし機械可読な形式が人間にとって読みやすいとは限らないことから今回の事例でそこまでは踏み込めませんでした。自然言語での説明にとどまっています。 また記載ミスや記載漏れによって欠陥が見逃されないように、補助的な仕様インスペクション観点を追加で15設けています。 たとえば: 1. 重複したIDがなければOK、あればNGと判定 2. TODOという文字列を含まなければOK、含めばNGと判定 3. テキストや画像はそれが静的なのか動的なのかが指示されていればOK、なければNGと判定 4. ... 余談ですが、静的検査器の実装は検査ルール1つにつき、UI要素レイアウト、UI要素表、画面遷移図のいずれかを入力とし、発見された欠陥のリストを返す関数として実装しています。 このようにすることで検査ルール間の独立性が高まり、検査ルールの追加や削除、変更を容易にできます。 また検査対象外の要素は無視するようにした方がよいです。 こうすることで仕様に対して画一的な表現を強制されなくなり、より適した表現(例:画面内の複雑な状態遷移を状態遷移図で表現する、複雑な条件判定をフローチャートで表現する、など)を仕様書へ埋め込めるようになります。 その表現についても検査対象にしたければ、その表現だけを検査する追加のルールをあとで実装すればよいのです。 ## 画面仕様の静的検査を導入した結果 この静的検査器を使うことで、プログラマーに仕様が渡ってくる前に15%の画面から計40の欠陥を発見できました。 また検査器によって欠陥が摘出された後の仕様書への質問は、静的検査器を使っていない他事例の平均的な質問数より少ない傾向にあることがわかっています。 ## 機械可読な画面仕様の課題 非プログラマーによって機械可読な仕様書を保守する場合、機械可読に保つハードルが高いとわかりました。 そのため残念ながら今回実装した静的検査器は継続的な運用には至れませんでした。 プログラマーが仕様書を保守するように役割を変更すれば解決できるかもしれません。 そのように役割を変更する場合、仕様書の保守にかかるプログラマーの工数の捻出が課題になるでしょう。 後述するような機械可読な仕様書による実装の自動生成分でまかなえるかどうかがポイントになりそうです。 ## 機械可読な仕様の応用 画面仕様やその周辺を機械可読にすることで仕様策定を取り巻くプロセスの一部を自動化できることがわかっています。 たとえば本事例では[仕様策定プロセス](https://cdn-ak.f.st-hatena.com/images/fotolife/s/swet-blog/20250416/20250416171648.png)のP3とP4が自動化されました。 P3についてはUI要素の配置図はFigma上のオブジェクトと紐づけることでFigma APIを使い機械的な画像の更新を実現しています[^6]。 [^6]: ConfluenceにFigmaを埋め込めるFigma Widgetが利用可能でしたが動作が重すぎるため利用しませんでした。 また仕様を機械可読にすれば実装の自動生成や検証項目の自動生成をできるかもしれません。 完全な自動生成は難しいかもしれませんが、部分的な自動生成であれば実現しやすいと推測しています。 たとえば典型的なE2Eテストは画面遷移仕様に基づく検証です。 今回紹介した画面遷移仕様にいくつかUI要素のID等のヒントを与えればE2Eテストの自動生成が可能に思えます。 また機械可読な仕様の検索や解釈をLLM向けに支援するMCPサーバーを考えられます。 これは実装や検証項目、テストの自動生成や、それらのプロセスの支援の役に立つかもしれません。 ## まとめ 1. 画面表示と画面遷移を記述する仕様書は機械可読にできる 2. 仕様書が機械可読であれば仕様の静的検査ができる 3. 静的検査によって自身の担当範囲の15%の画面から計40件弱の欠陥を発見した 4. 機械可読な仕様書にはさらなる応用が見込める

子供の命名のために名前を探索するツールを作った

子供が産まれるのに備え子供の名前を探索するツールを作りました。Linux、macOS、Windows で次のように名前の候補を列挙してくれます:

$ name search --space full 山田 --max-length 2 < ./filter.json | tee result.tsv
評点    画数    名前    読み    性別    天格    地格    人格    外格    総格
14      16      丈辞    ジョウジ        男性    吉      大吉    吉      大吉    大大吉
13      21      丈騎    タケキ  男性    吉      大吉    吉      大吉    大吉
...

github.com

名前探索器を開発した背景

12月に第二子が産まれました。第一子は人力で名前を探索したところあまり良い名前が思い浮かばず苦労した経験があります。最終的には友人の手を借りて命名したのですが、今回もまたお世話になるのは申し訳なかったため、システム的な解決を図りました。

開発した名前探索器の紹介

kuniwak/name は、常用漢字と人名用漢字、ひらがな、カタカナからなる文字列の空間から条件に当てはまる名前を探索します。探索の条件として JSON 形式でフィルタを指定します。フィルタによって空間内の文字列それぞれが判定され、フィルタの結果が真なら結果に残り、偽なら結果から取り除かれます。フィルタには次の要素を使えます:

説明 構文
{"true": {}} {"true": {}}
{"false": {}} {"false": {}}
論理積 {"and": [filter...]} {"and": [{"yomiCount": {"rune": "ア", "count": {"equal": 1}}}, {"yomiCount": {"rune": "イ", "count": {"equal": 1}}}]}
論理和 {"or": [filter...]} {"or": [{"yomiCount": {"rune": "ア", "count": {"equal": 1}}}, {"yomiCount": {"rune": "イ", "count": {"equal": 1}}}]}
否定論理 {"not": filter} {"not": {"yomiCount": {"rune": "ア", "count": {"equal": 1}}}}
性別 {"sex": sex} {"sex": "asexual"}
長さ {"length": count} {"length": 3}
読み仮名のモーラ数 {"mora": count} {"mora": {"equal": 3}}
よくある読み仮名 {"commonYomi": {}} {"commonYomi": {}}
画数 {"strokes": count} {"strokes": {"lessThan": 25}}
五格それぞれの最小値 {"minRank": 0-4}4=大大吉, 3=大吉, 2=吉, 1=凶, 0=大凶) {"minRank": 3}
五格の合計値の最小値 {"minTotalRank": byte} {"minTotalRank": 11}
指定した読み仮名の数 {"yomiCount": {"rune": string, "count": count}} {"yomiCount": {"rune": "ア", "count": {"equal": 1}}}
読み仮名のマッチ {"yomi": match} {"yomi": {"equal": "タロウ"}}
漢字のマッチ {"kanji": match} {"kanji": {"equal": "タロウ"}}
指定した漢字の数 {"kanjiCount": {"rune": string, "count": count}} {"kanjiCount": {"rune": "漢", equal": 1}}
count {"equal": byte} or {"lessThan": byte} or {"greaterThan": byte} {"lessThan": 1}
match {"equal": string} or {"startWith": string} or {"endWith": string} {"startWith": "タロ"}
sex "asexual" or "male" or "female" {"sex": "asexual"}

ちなみに私の使ったフィルタは次のとおりです:

{
  "and": [
    {"sex": "male"},
    {"mora": {"equal": 3}},
    {"minRank": 2},
    {"minTotalRank": 11},
    {"commonYomi": {}},
    {"length": {"equal": 2}},
    {
      "or": [
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 1}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"lessThan": 2}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}}
          ]
        },
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 1}}},
            {"yomiCount": {"rune": "", "count": {"lessThan": 2}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}}
          ]
        },
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 1}}}
          ]
        }
      ]
    },
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}}
  ]
}

このフィルタでは最低限の姓名判断のほか、性別の指定、モーラ数の制限、長さの制限、よくある名前の読みとの一致、親と子の名前の同一性の制限(第一子との公平性のため)、望ましくない漢字の除去をしています。姓名判断を条件に加えた理由は、自分の名前を姓名判断にかけてみるときちんと良いものであることがわかり親はちゃんと考えてつけたんだなあということがわかるからです。

このフィルタによって、私の苗字では全空間探索によりおよそ 4500 件ほどの名前の候補が得られます。あとはこれを Google Sheets 等にまとめ、好ましい名前を抽出するとよいでしょう。5000件未満であれば 1h 未満で目視による選別を終えられます。

仕組み

このツールは全空間探索と頻出空間探索の2つの探索モードを持っています:

  • 全空間探索
    • 常用漢字と人名用漢字、ひらがな、カタカナからなる文字列の空間から名前を探索します。低速ですが候補数は多くなります。探索する名前の長さの上限値に2より大きい数を指定すると候補数が爆発的に増えるため非常に低速になります。名前の読みは MeCab によって推定します。
  • 頻出空間探索
    • s1r-J/jinmei-dict を使い、よくある名前とその読みのペアからなる空間から探索します。高速ですが候補数は少なくなります。

この探索範囲から得られた名前の候補について性別やモーラ数を推定します。性別の推定には ENAMDICT/JMnedict を使っています。そうして得られた名前の候補と付加情報をもとにフィルタの条件を満たすもののみを表示します。

読みの推定方法の模索

前述の通り、読みの推定には MeCab を使っています。辞書として NEologd を使うと現代的な読みを出力してくれます。なお MeCab を使う前には常用漢字 + 人名用漢字の漢字ごとに標準的な読みのリストのデカルト積を取っていました。ただあまりにも精度が悪かったために別の方法を模索し neologd/namelti にいきつきました(Namelti の紹介記事)。Namelti は NEologd の辞書を使った MeCab により人名の読みを推定するツールです。

ちなみに Namelti の精度の評価には苦労しました。そのままではビルドできないためパッチをあてる必要がありました。最終的に Docker 上でビルドに成功し(kuniwak/debian-namelti として公開)、その精度がかなりよいことがわかりました。ただ Namelti そのままを使おうとするとプロセスが別になるため IPC が必要となり、この IPC は名前の候補数だけ呼び出されるため呼び出しのオーバーヘッドが大きいです。Namelti の実装を読んだところ、MeCab を素朴に使っているだけということがわかったため MeCab の Go 言語バインディングである shogo82148/go-mecab を使い、IPC を使わず動作するように Go 言語で Namelti を再実装しました。

Windows から cgo で MeCab を呼び出す

一番苦労したのは Windows 環境の cgo 経由で MeCab を呼び出すことです。shogo82148/go-mecab では CGO_LDFLAGSCGO_CFLAGSmecab-config が指示する値で設定するよう指示しています。ただ Windows のコマンドプロンプトや PowerShell から、Bash で実装されている mecab-config そのままを呼び出すことができないこと、また C:\Program Files\MeCab のような空白を含むパス配下にあるヘッダやオブジェクトファイルがあると cgo で意図しない位置でパラメータの区切りと判定されることがハードルとなりました。そこで C# で mecab-config を再実装し、これらの問題を解決しました(kuniwak/mecab-config-windows)。これは dotnet tool install -g MecabConfig でインストールできます。この実装では次のように空白を含むパスでも cgo に渡るパラメータが正しく解釈されるようにエスケープしています:

$ mecab-config --libs
"-LC:\Program Files\MeCab\lib" -lmecab -lstdc++

ただぶっちゃけ、WSL2 で起動する方が百万倍楽だと思います。WSL2 で使ってください。

終わりに

こうして作った名前の探索器から得られた候補から子供に名前をつけられました。

あまりに BLE マクロを Nature Remo 上でデバッグするのが苦行だったので Swift 用の開発環境を整えた

TL;DR

Android nRF Connect の BLE マクロのサブセットを macOS/iOS/... 上で開発する環境を用意しました。

github.com

import Foundation
import BLEMacroEasy

// You can find your iPhone's UUID by running the following command in Terminal:
// $ git clone https://github.com/Kuniwak/swift-ble-macro
// $ cd swift-ble-macro
// $ swift run ble discover
let myIPhoneUUID = UUID(uuidString: "********-****-****-****-************")!
let myMacro = try String(contentsOf: URL(string: "https://ble-macro.kuniwak.com/iphone/battery-level.xml")!)

try await run(macroXMLString: myMacro, on: myIPhoneUUID) { data in
    // This handler is called when every value read from the peripheral.
    let batteryLevel = Int(data[0])
    print("\(batteryLevel)%")
}

macOS/iOS/... 上で BLE マクロを実行する機能のほかマクロの開発を助ける CLI を提供しています。BLE マクロ実行機能については README を見てください。CLI は BLE デバイスのスキャン、マクロのバリデーション、マクロの実行、対話的なマクロ実行をサポートしています:

$ # BLE デバイスをスキャンする
$ ble discover
00000000-0000-0000-0000-000000000000    Example Device 1    -78
11111111-1111-1111-1111-111111111111    Example Device 2    -47
22222222-2222-2222-2222-222222222222    Example Device 3    -54
...

$ # Ctrl+C でスキャンを中断する

$ # BLE マクロを実行する
$ ble run path/to/your/ble-macro.xml --uuid 00000000-0000-0000-0000-000000000000

$ # BLE マクロを対話的に実行する
$ ble repl --uuid 00000000-0000-0000-0000-000000000000
connecting...
connected

(ble) ?
write-command, w, wc    Write to a characteristic without a response
write-descriptor, wd    Write to a descriptor
write-request, req      Write to a characteristic with a response
read, r Read from a characteristic
discovery-service, ds   Discover services
discovery-characteristics, dc   Discover characteristics
discovery-descriptor, dd        Discover descriptors
q, quit Quit the REPL

(ble) dc
180A 2A29 read
180A 2A24 read
D0611E78-BBB4-4591-A5F8-487910AE4366 8667556C-9A37-4C91-84ED-54EE27D90049 write/write/notify/extendedProperties
9FA480E0-4967-4542-9390-D343DC5D04AE AF0BADB1-5B99-43CD-917A-A77BC549E3CC write/write/notify/extendedProperties
180F 2A19 read/notify
1805 2A2B read/notify
1805 2A0F readk

(ble) r 180F 2A19
58

なお開発した BLE マクロは Kuniwak/ble-macro で公開しています。

背景

Nature Remo に BLE(Bluetooth Low Energy)マクロ機能が搭載されました(公式アナウンス)。対応機種(Nature Remo3、Nature Remo mini2 ファミリー)の Remo をお使いなら BLE デバイスを Remo から操作できます。そこで BLE で操作できる我が家の Philips Hue のフルカラー電球x3を Remo から操作すべく BLE マクロの開発を始めました(無駄にフルカラーなのはこの動画を見て淡い憧れがあったため)。

我が家の Philips Hue

Remo のサポートしている BLE マクロは Android nRF Connect のサブセットです。マクロは XML で記述します。例えば Hue を白昼色で点灯させるには次のように記述します:

<macro name="hue-daylight-white" icon="BRIGHTNESS_HIGH">
    <assert-service description="Hue" uuid="932c32bd-0000-47a2-835a-a8d455b859dd">
        <assert-characteristic description="Combined" uuid="932c32bd-0007-47a2-835a-a8d455b859dd">
            <property name="WRITE" requirement="MANDATORY"/>
        </assert-characteristic>
    </assert-service>
    <write description="Set to Daylight White" characteristic-uuid="932c32bd-0007-47a2-835a-a8d455b859dd" service-uuid="932c32bd-0000-47a2-835a-a8d455b859dd" value="0101010201fe0302fa0005020100" type="WRITE_REQUEST" />
</macro>

BLE マクロでは service や characteristic という操作窓口で値の読み書きを指示できます。有名なデバイスならばこれらは 第三者による仕様の推測 である程度詳細がわかります。ただマクロを記述して Remo で動作する状態に持っていくまでにはそれなりに試行錯誤が必要になります。この試行錯誤がとにかく苦行でした

公式曰く Android なら nRF Connect for Mobile アプリで BLE マクロの記録から実行までをサポートしているらしく楽にマクロを開発できるようです。しかし手元には Android 端末がありませんでした。iOS 用に同じ開発者の 同名のアプリ がありますがこれにはマクロ機能がついていません。そのため最初の頃は Remo に BLE マクロを食べさせてみて理由のほとんどわからないエラーに直面して、BLE マクロを勘で書き換えて…というサイクルを回していました。

エラーに詳細な情報がない様子

このサイクルがあまりに不毛であったため Android の nRF Connect for Mobile と同様の機能をもつ環境をなんとか整えられないかと思い Kuniwak/swift-ble-macro を開発しました。nRF Connect for Mobile よりも大幅に機能は少ないですが、Remo で動作する BLE マクロを開発するには十分な機能を持っています。ぜひご活用ください。

なおBLE マクロを開発するには既存のアプリがどんな通信を BLE デバイスとしているかをキャプチャすることも重要です。詳しくは次のブログを読むとやり方がわかるでしょう:

harumi.sakura.ne.jp

BLE マクロを開発した際の苦労

Hue や Switchbot でさまざまな苦労をしたのでここで供養しておきます。

Hue から insufficient encryption エラーが返ってくる

Hue はペアリングしないと操作できないようです。ペアリングしていない central から read/write しようとすると insufficient encryption エラーが発生します。なおペアリングされるのは最初にペアリングされたデバイスのみです(後述する方法でこれを回避できる)。例えば初回に Hue のアプリで接続してしまうとそれ以降のデバイス(e.g. Remo、Google Nest mini)からは操作を受け付けてくれなくなってしまいます。これを回避するにペアリングしてある Hue の公式アプリ > 設定 > 音声アシスタント > Google Home > 検出可能にする、の操作が必要です。この操作によってごく短時間(1minほど?)ペアリングを受け付けてくれるようになります。これは service 0000fe0f-0000-1000-8000-00805f9b34fb の characteristic 97fe6561-2004-4f62-86e9-b71ee2da3d220x01 を書き込むことで再現できます(検出可能にする BLE マクロ)。

ペアリングし損ねた Hue のファクトリリセット方法がわからない

公式アプリや Mac などにペアリングする前に Google Nest mini などにペアリングされてしまうと前述の insufficient encryption 問題によって公式アプリや Mac などから操作ができなくなります。ファクトリリセットは BLE による操作で実現されているため公式アプリがペアリングされていない状況ではファクトリリセットが必要になります。

Hue をファクトリリセットする方法として点灯と消灯のサイクルを回す方法が紹介されています。しかし私の Hue(モデル LCA009、ファームウェアバージョン v1.116.3)ではうまく動きませんでした。代わりに、Hue の電源を壁面のスイッチ等から落としてから再び電源を入れると、ごく短時間 & かなりの近距離であれば Hue の公式アプリで検出可能になることを発見しました。そこからファクトリリセットができました。

Switchbot を操作しようとしても反応しない

Switchbot も BLE によって操作ができるデバイスです。ただこいつもなかなかの曲者です。v6.6 なら Discord の開発者コミュニティに紹介されているマクロ で操作ができるのですが v6.3 では操作を受け付けてくれません。試行錯誤しているうち v6.3 でも characteristic cba20002-224d-11e6-9fb8-0002a5d5c51b に 0x01 を書き込む前に cba20003-224d-11e6-9fb8-0002a5d5c51b で通知の購読をすると爪が動くことを発見しました。ということで v6.3 で動くマクロは以下になります:

<macro name="switchbot-push" icon="PLAY">
    <assert-service description="Switchbot" uuid="cba20d00-224d-11e6-9fb8-0002a5d5c51b">
        <assert-characteristic description="Push" uuid="cba20002-224d-11e6-9fb8-0002a5d5c51b">
            <property name="WRITE" requirement="MANDATORY"/>
        </assert-characteristic>
        <assert-characteristic description="Configuration" uuid="cba20003-224d-11e6-9fb8-0002a5d5c51b">
            <property name="NOTIFY" requirement="MANDATORY"/>
            <assert-cccd />
        </assert-characteristic>
    </assert-service>
    <write-descriptor description="Enable notifications" characteristic-uuid="cba20003-224d-11e6-9fb8-0002a5d5c51b" service-uuid="cba20d00-224d-11e6-9fb8-0002a5d5c51b" uuid="00002902-0000-1000-8000-00805f9b34fb" value="0100" />
    <write description="Write 0x570100" characteristic-uuid="cba20002-224d-11e6-9fb8-0002a5d5c51b" service-uuid="cba20d00-224d-11e6-9fb8-0002a5d5c51b" value="570100" type="WRITE_REQUEST" />
    <wait-for-notification description="Wait for notification" characteristic-uuid="cba20003-224d-11e6-9fb8-0002a5d5c51b" service-uuid="cba20d00-224d-11e6-9fb8-0002a5d5c51b" timeout="5000" />
</macro>

お世話になった製品・文献

最後にお世話になった製品・文献を紹介します:

github.com

github.com

テスト技法「同値分割」を信頼していいのかわからなくなった

これまで同値分割を信頼できる手法だと信じてきました。最近になってどうして同値分割が信頼できる方法なのかその理由を私が説明できないことに気づきました。この原因は2つあります:

  1. 同値分割の分割の基準が不明確であること
  2. 後述するいくつかの仮定を満たさない場合、ある同値パーティションの代表値の出力が正しければその同値パーティションの他の値の出力も正しいといえる根拠に乏しいこと

この2つから、不明確な基準の同値分割はその信頼性の説明ができないこと、同値テストは後述するいくつかの仮定が満たされたときのみ有効な手段でありいずれかの仮定が満たされない場合はさして信頼できないことが導かれます。

この記事ではこの結論に至るまでの過程について詳しく説明していきます。なお誤りのご指摘は大歓迎です。ぜひ皆さんで議論しましょう。

続きを読む

私の TDD の理解と Kent Beck による TDD の解説の比較

TDD(テスト駆動開発)の提唱者 Kent Beck による TDD の定義の解説を @t_wada さんが翻訳したブログが公開されました。

t-wada.hatenablog.jp

ここで解説されている TDD と私のこれまで理解していた TDD(後述)を比較します。 みなさんの TDD の理解もぜひ知りたいので自身の思う TDD のプロセスを教えてください。

TL;DR

概ね一致していたようです。細かい差異としては Kent Beck は後述する PFD の「脳内の SUT 仕様(D10)」を生成するアクティビティも TDD に含まれていると主張しているようなので、仕様を分析してテストシナリオを洗いだすアクティビティを D10 の前に追加するとよさそうですね。

私の理解していた TDD

PFD(Process Flow Diagram) で表現します。

PFD

TDD のプロセスの PFD 表現

緑色の成果物はテストが成功する成果物であることを意味し、赤色の成果物はテストが成功しない成果物であることを意味します。

要素表

成果物要素表

アクティビティID アクティビティの説明
A1 脳内のテスト対象(SUT)の仕様から最も実装の容易な入出力の組を選び、これを最初のテストケースとしてテストに追加する。
A2 空のSUTのままテストを実行し失敗することを確認する。これには失敗すべきときに実際にテストが失敗することを確認する意図がある。
A3 D6のテストを成功させる最も単純な実装をSUTに実装する。期待される出力をそのままreturnするので構わない。この作業はFakeItと呼ばれる。テストが成功すべきときに実際に成功することを確認する意図がある。
A4 テストが失敗であればD7へのテストが通るようにテストを修正する。もし修正しなくともテストが成功するようであれば何もしない。
A5 脳内仕様から同値パーティションの代表値や境界値となる入力を選んでテストケースとして実装しテストに追加する。同値パーティションの代表値を入力に選ぶ場合はテストケースの名前に同値パーティションの名前を設定する。境界値を選ぶ場合はその境界値を選んだことがわかるような名前をテストケースの名前として設定する。この時点ではまだSUTを編集してはいけない。
A6 テストを実行する。
A7 テストが成功するまで最も素早く実装できる方法でSUTを編集する。もし最初からテストが成功している場合は何もしない。この時点ではコードの簡潔さを気にしない。
A8 SUTまたはテストのいずれか一方(Xとする)だけを簡潔する。もしテストが失敗する場合はテストが成功するようになるまでXを修正する。これをリファクタリングという。

プロセス要素表

成果物ID 成果物の説明 実行可能なリソース
D0.0 仕様のサンプルである全てのテストケースにおいて実際の出力が期待する出力と一致するテスト対象(SUT)。SUTの実装は簡潔であり保守性が高い。サンプル数が十分であれば仕様を満たしたテスト対象の実装と見なしてよい。 実装者
D0.1 仕様のサンプルである入出力の組のすべてが実際にSUTから得られるか確かめるコード。実装が簡潔であり保守性が高い。もし1つでもSUTからの出力が仕様から期待される出力と異なるならば全体として失敗を返す。そうでなければ全体として成功を返す。 実装者
D1 仕様のサンプルである全てのテストケースにおいて実際の出力が期待する出力と一致するSUT。SUTの実装は簡潔であるとは限らない。サンプル数が十分であれば仕様を満たしたテスト対象の実装と見なしてよい。 実装者
D2 追加されたテストケース以外は成功するテスト。追加されたテストケースだけは成功・失敗のどちらでもよい。 実装者
D3 仕様のサンプルである入出力の組のすべてが実際にSUTから得られるか確かめるコード。テストの実装が簡潔であるとは限らない。もし1つでもSUTからの出力が仕様から期待される出力と異なるならば全体として失敗を返す。そうでなければ全体として成功を返す。 実装者
D4 仕様のサンプルであるテストケースにおいて実際の出力が期待する出力と一致するとは限らないSUT。SUTの実装は簡潔であるとは限らない。 実装者
D5 仕様のサンプルとして選ばれた1つの入出力の組が実際にSUTから得られるか確かめるコード。もしSUTからの出力が仕様から期待される出力と異なるならば全体として失敗を返す。そうでなければ全体として成功を返す。 実装者
D6 仕様のサンプルとして最初に選ばれた入出力の組が実際にSUTから得られるか確かめるコード。もしSUTからの出力が仕様から期待される出力と異なるならば全体として失敗を返す。そうでなければ全体として成功を返すべきだがそうなっているとは限らない。 実装者
D7 仕様のサンプルとして最初に選ばれた入出力の組が実際に帰ってくるSUT。SUTの実装は期待する出力をreturnで返す程度に極めて単純であるべきである。 実装者
D8 何も実装されていないテスト。 実装者
D9 何も実装されていないSUT。 実装者
D10 実装者の脳内にあるこれから実装したいコンポーネントの仕様。テストケースを追加するうちに浮き彫りにできるので明確である必要はこの時点ではない。 実装者

終わりに

みなさんの TDD の理解もぜひ知りたいので自身の思う TDD のプロセスを教えてください!