株式会社クリアコード > ククログ

ククログ


フリーソフトウェアの法人向けサポートの一環で行った開発元へのフィードバックの事例紹介

当社の法人向けサポートサービスでは、企業のお客様がフリーソフトウェアを使われていて遭遇されたトラブルの解決や原因究明、文書化されていない技術情報の調査などを有償にて行っています。「特殊な労働力を提供してお客様の役に立ち、その対価としてお金を頂く」ということで、ビジネスモデルとしてはシンプルです。

ところで、当社の理念は「自由と稼ぐの両立」です。前述のビジネスモデルから「稼ぐ」は容易に読み取れますが、これがどのように「自由」に繋がっているかは見えにくいかもしれません。

まず単純に、「ユーザーが増える事が自由なソフトウェアの推進になる」という事が言えます。それは「そのソフトウェアを使うと開発チームに広告収入が入る」といった直接的な還元に限りません。例えば、そのソフトウェアのユーザーが増えると、業界内でのそのソフトウェアの影響力が増すので、プロプライエタリな製品にとってはそのデータ形式やプロトコルを無視しにくくなり、ユーザーが特定の製品に囲い込まれるリスクが低減する、という効果が期待できるでしょう。

また、こちらがこの記事の本題なのですが、このビジネスの中で実際の使用環境で見つかった不具合を報告したり、実際の使用環境でのニーズを汲み取った提案をしたりという形で、そのソフトウェアの改善に寄与できます

ということで、以下、Mozilla製品の法人向けサポート由来のフィードバックの事例を4つご紹介します。

Bug 1540943 - Broken message body on forwarding a mail including invalid character(Mozilla Thunderbird)

これは、メールの本文にエンコーディング上不正な文字が含まれていると転送メールの本文が文字化けしてしまうという問題です。法人利用では様々なメールが頻繁に・大量にやり取りされるため、このような「普通は起こらないエッジケース」が起こる事があり、また、それがビジネス上の支障になってしまう場合もあります。この現象に遭遇されたお客さまの場合、取引先からのメールを転送しようとすると文字化けしてしまうという事でお困りでした。

原因と回避策を調査した結果、本文として転送する場合には発生を避けられない問題である事が分かったため、お客さまには「添付として転送」などの別の操作を使って回避して頂く事をお薦めし、開発元には上記のBugの通り、その時点で判明していた発生条件などの情報を報告しました。

その後、開発者の方の目に留めて頂き調査が進んで、最終的にはソースコード中の1行をピンポイントで修正(呼び出す関数を変更)して問題を解消するパッチが投入されました。変更点が少なかった事から、この修正はThunderbird 60のメンテナンスリリースにも反映される結果となっています。

Bug 1563665 - Replied mails are not marked as "replied" if the folder has a comma (,) in its substance file name(Mozilla Thunderbird)

これは、メールフォルダに対応するローカルディスク上のファイルの名前に , (半角コンマ)が含まれているとそのフォルダ内のメールに返信などの操作をしても「返信済み」のようなマークが付かないという問題です。Thunderbirdではメールフォルダ1つがディスク上の1ファイルとなるmbox形式が初期設定となっており、ファイル名はフォルダ名そのままではなくMUTF-7という特殊なエンコーディング方式で英数字に変換されているのですが、台北 のような特定の文字列で変換結果が &U,BTFw- となり , が含まれる事になって、全く予想もしていなかった所でこの不具合に遭遇してしまう場合があるという状況でした。

お客さまには、MUTF-7でのエンコーディング結果に , が含まれないようにフォルダ名を工夫するという回避策をご案内していますが、どんな文字列がエンコーディング後にこのパターンになるのかについては事前の予想が難しいため、残念ながら万全とは言えない回答となってしまいました。

この問題は原因そのものは判明したものの、古くからある設計上の判断に起因する問題ということで、どのような方向で対処するのが妥当かという技術的判断の落とし所が見つかるまで、修正にはまだしばらく時間がかかりそうです。

Bug 1569089 - The "-version" ("-v") command line option doesn't report version information when using cmd.exe(Mozilla Firefox)

これは、Firefoxの起動オプションの一部が特定の状況下で動作しなくなっているという問題です。Firefoxは実は firefox.exe -v という風に -v オプションを指定して起動すると一般的なコマンドと同様にバージョン情報を文字列として出力するようになっており、これを使って現在インストール済みのFirefoxのバージョンを確認するという運用を取られているお客さまがいらっしゃったのですが、FirefoxをESR68に更新するとバージョン情報が出力されなくなったために運用に支障が生じている、という事でお問い合わせを頂きました。

調査の結果、Bugに記載しているとおりこの問題そのものは回避が不可能という事が分かったため、Firefoxのバージョンを調べるには何かしら別の方法*1を使わなくてはならない旨をお客さまにはお伝えしました。

-v はあまり使われる事の無さそうなオプションで、我々も正直な所、今回の調査を行うまでオプションの存在自体を把握していませんでした。しかし、報告したBugは優先度P2 (クリティカルな問題に対して付けられる P1 の次に高い優先度)と設定されています。本稿執筆時点では未解決ですが、もしかしたら案外早く修正される事になるかもしれません。

Add managed storage support (Duplicate Tabs Closer)

こちらはFirefox本体ではなくアドオンへの改善提案です。「Duplicate Tabs Closer」は同じURLのタブを重複して開く事を制限するアドオンで、重複の検出基準や検出時の挙動について様々な設定ができるようになっています。しかしながら、それらの設定値はローカルストレージ領域を使って保存されており、システム管理者の側では設定を制御できません。ですので、法人利用であらかじめ動作を決めておきたい場合、アドオンのソースコードを編集して組み込みの初期設定を変更する必要があります。

お問い合わせを頂いたお客さまの導入環境向けにそのように改造する事自体は容易ですが、提供開始後にアドオンが更新されたらそれへの追従の必要が生じます。また、その後同様のニーズをお持ちのお客さまから相談を頂く度に改造版を作る必要もあります。

その一方、Firefoxのアドオン向けAPIにはManaged Storageという機能があり、専用のファイルもしくはポリシー設定用の policies.json に書いた設定をアドオン側で読み取る事ができます。もしDuplicate Tabs CloserがManaged Storageに対応していれば、アドオンのソースコードを毎回編集する必要はありません。

以上の事を踏まえて、Managed Storageから設定値を読み込んで反映するという動作を追加する変更を提案した物が、上記のプルリクエストです。こちらは作者の方のご理解を得る事ができ、無事にマージして頂けました。

法人向けサポートからアップストリームへフィードバックする事の意義

アップストリームへのフィードバックは開発の現場に直接関わる事に等しいため、的確なフィードバックには様々な知識が必要になってきます。FirefoxやThunderbirdのように「ソフトウェア開発者でないエンドユーザーも使う物」ではこの点がネックとなりがちなためか、ユーザー自身によってなされた報告にはどうしても、不正確な報告や発生条件の絞り込みが不足した報告が散見されます。また、多くの日本人にとっては英語自体がハードルとなって余計にフィードバックしにくいという問題もあります。当社が法人サポートの中で行っているフィードバックは、これらの点を補ってフリーソフトウェアを推進していく物と言えます。

ビジネス的観点においても、アップストリームへのフィードバックには意義があります。問題点をそのプロダクトの開発元にフィードバックしないで手元での回避だけを行っていると、プロダクトの更新や変更でその回避策が取れなくなる恐れがあり、将来的なリスク要因になります。早め早めにフィードバックしてプロダクト本体側で問題を解決する事によって、その種のリスクが低減され、より安定して運用できるようになる事が期待できます*2。安物買いの銭失いにならなければ、それはサービスの付加価値と言えます。

そうしてプロダクト自体の品質が高まって企業で採用しやすくなると、法人向けサポートの需要の拡大にも期待できます。また、当社内でフィードバック対象のプロダクトへの理解が深まり知見が増えれば、それだけサービスの品質が向上します。

開発プロジェクトにとっても、ユーザーとなるお客さまにとっても、サポートを提供する当社にとっても利益になるという事で、これは「自由と稼ぐの両立」という理念の1つの実践例となっているわけです。

まとめ

以上、当社が法人向けサポートサービスを通じて行ったフィードバックの例を挙げて、会社の理念をどのように実践しているかをご紹介しました。

当社の法人向けサポート事業はいわゆるSI業界の周辺にあり、直接の取引先やその先のお客さまには「お堅い」業界の会社さまも多くあります。そのような業界はオープンさ・自由さと縁遠いという印象を持つ方が多いかもしれませんが、実際にはその内側ではフリーソフトウェアが使われていることは珍しくありません。やり方次第ではその領域のビジネスと自由なソフトウェアの推進を同時に行えるという事例の1つとして、参考にして頂ければ幸いです。

また、業務上でのフリーソフトウェアの使用においてトラブルに遭遇されていて、フリーソフトウェア製品やプロジェクト側の知識の不足でお困りの企業の担当者さまがいらっしゃいましたら、お問い合わせフォームよりお問い合わせ下さい。

*1 Windowsのレジストリの情報を参照する方法が一般的です。

*2 理論上、そうして問題が解消されていった先には全く仕事が無くなるという未来がいずれ訪れる事になりますが、実際にはプロダクトもユーザー側の要件も変化し続けるため、そのような未来がすぐに訪れるという事は、幸か不幸かまだ無さそうです。

2019-09-09

OSSへフィードバックしてみたいけど、英語でどう書けばいいのか分からない

結城です。

ここまで、OSSへのフィードバックをやってみようとした時に躓きがちなポイントについて、フィードバックするトピックの見つけ方報告に盛り込むとよい内容その情報の送り届け先の選び方の知見をそれぞれ述べてきました。

ところで、それらの技術的な内容以前のハードルとして、言語の壁という物もあります。実際にOSS Gateワークショップでも、フィードバック内容をまとめた後、英語でそれを書き直すという段階で手こずっておられる方がかなり多い印象があります。

ITエンジニア向けに「こういう英語表現を覚えよう」という情報を紹介する記事は時々見かけます。ですが、ワークショップでビギナー参加者の方が英語を書くのに苦労している様子を実際に見ている印象では、必要なのはそういった記事で紹介される「実際の現場でよく使われる単語や熟語の情報」ではなく、「実際の現場で英文を書く時に行われる考え方の解説」の方であるように筆者には感じられました。

そこで今度は、OSSへのフィードバックを英語でやる時の考え方に焦点を当てて解説してみます。対象読者は、英語に苦手意識のある方、中学高校の英語の授業で英作文はした事があるけれども自分でゼロから何かを説明する英文を書いた事はない方、といったレベル感を想定しています。

「日本語の文章を英語に変換する」のではなく、「同じ事を説明する英語の文章を作る」という考え方をしよう

英作文に不慣れな人は、まず「日本語の文章」を考えて、それを「対応する内容の英語の文章」に変換するものだ、と考えがちなのではないでしょうか。

いきなり言ってしまうのですが、それは無理なので、潔く諦めてしまいましょう

例えば有名な話ですが、外出先から帰宅した人が在宅していた人に言う「ただいま」や、それに応えての「おかえりなさい」は、英語に翻訳しようがないと言われています。そもそも欧米には「帰宅した時に挨拶を交わす」文化が無いため、そういう言葉どころか概念が無いのだそうです。この例を一つとってみても、「日本語で書いた文章をきっちり英語に変換しきる」という事は根本的には不可能だという事が分かるのではないでしょうか。

ではどうするのかといえば、「伝えたい内容、対象」そのものの方に立ち返って、それを表現する英語の文章を考える、という事になります。

例えば、ここに1本のペンがあるとします。その状態を英語で表すには、どんな文章が考えられるでしょうか?

  • This is a pen.(これは一体何であるか?という点に着目)
  • There is a pen.(どこにあるか?という点に着目)
  • I have a pen. / This is my pen.(誰の物か?という点に着目)

「これはペンです」という日本語の文章を先に与えられていれば、「This is~」という文しか思いつかないかもしれません。しかし事実の方に焦点を当てると、色々な角度から表現できるという事が分かるのではないでしょうか。

「すでに書いてしまった日本語の文章」に囚われないで、「目の前にある物」や「目の前で起こっている事実」の方を意識するという事を、常に心がけるようにしてみて下さい。まずはそれが最初のステップです。

平易に言い換えてみよう

この事を考える上で参考になる物として、「やさしい日本語」(※リンク先はPDF)があります。

やさしい日本語とは、

  • 海外からの旅行者がよく訪れる観光施設や、移民者が手続きに来る市区町村の役所などにおいて、日本語が不得手な人が読む事を想定して
  • 複雑な表現や難しい単語をなるべく使わずに、平易な単語や単純な文の書き方を心がけて

書かれた日本語の文章の事を言います。日本語ネイティブスピーカーではない人だけでなく、子供や高齢者、障害者など幅広い層にとってもメリットがあるという事で、近年注目されてきているそうです。

「やさしい日本語」を書くためには、同じ物事をより平易な表現で言い換える必要があります。例えば、

訪日観光客で洛中・洛外は連日溢れかえっており、史跡や寺社仏閣が観光資源として有効に機能していて、誇らしい。

という文章は、

  • 多少ニュアンスは変わったとしても各単語を平易な表現に言い換える。
    • 訪日観光客→他の国から来た人達
    • 洛中・洛外→京都の町
    • 連日溢れかえり→毎日たくさんいる
    • 史跡や寺社仏閣→古い建物、お寺、神社など
    • 観光資源として機能→見どころになる
  • 接続された文を短い文に切り分ける。
  • リズム感や格好良さを優先していたトリッキーな語順を整理する。
  • 省略された主語(行為の主体)を補う。

といった加工をする事で、

京都の町には毎日、たくさんの人達が他の国から来る。
古い建物、お寺、神社などが見どころになっている。
私はとてもうれしい。

のように言い換えられるでしょう。

英語が不得意な人は、英語の文法にも単語力にも自信が無い事が多いと思います。しかしその一方で、日本語はそれなりに使えるという自信が(無意識にでも)あるはずです。そのため、自分の持てる限りの日本語力を駆使して考えた日本語の文章には、難しい単語や言い回しが無意識のうちに入ってしまいがちです。日本語力と英語力の間にギャップがあるために、日本語の方で選んだ単語や表現に対応する英語の言い方が分からなくて、手が止まってしまう……これこそが「英語になると途端になにも書けなくなる」事の正体です。

なので、日本語の文章を英語に訳せる自信が無い場合、その前段階のワンクッションとして、まずは自分が書いた文章を「やさしい日本語」に書き直すようにしてみて下さい。そうすればきっと、次のステップにもスムーズに進めるようになるでしょう。

「SVOの平叙文」で考えよう

英語の話に戻ります。

中学高校までの英語の授業では様々な文法や単語を教わりますが、こと「OSSへのフィードバック」という場面で言うと、実際に使う知識はその中のごく一部です。特に文章の形は、基本的にS(subject:主語)-V(verb:動詞)-O(object:目的語)の形式で書くと考えていいです。これは「私は○○をした」「あなたは××である」のような単純な文章、いわゆる平叙文という形式です。

日本人が英文を書くと受動態(受け身の形、主語が何々されるという文)が多くなりがちだ、とよく言われます。それは、こなれた日本語の表現では主語が省略されがちで、主語が省略された文章をそのまま直訳すると、必然として受動態にせざるを得ないからです。

ですが、「誰が」何をしたのか、「誰が」どうなったのか、という事を省略しないようにすれば、文章は自然とSVOの文型になります

「誰が」を書いたら主語が「I(私は)」「You(あなたは)」ばかりになってしまう? それは、「主語は人間だ」という無意識の思い込みがあるからです。英語では、物でも現象でも概念でも普通に主語になります。主語をどう書くかに迷ったら、一旦あらゆる物事を擬人化してみて下さい

例えば、

Firefoxの法人向けポリシー設定で、検索候補の表示を無効化するSearchSuggestEnabledfalseに設定しても、単独のWeb検索バーでは検索候補が表示され続けてしまう。

という不具合を報告したいとします*1。これはどう言い換えられるでしょうか。

日本語での省略された主語は「私が(~を設定した)」ですね。しかし、ここには「Firefox」や「SearchSuggestEnabledという設定項目」、「その設定の値がfalseである状態」、「単独のWeb検索バーというUI部品」「検索候補」といった登場人物達がいます。ということでこれらを主語にしてみると、例えば以下のような表現ができます。

  • 「設定の値がfalseである状態」を擬人化して主語にする:

    SearchSuggestEnabled=false does not hide search suggestions on the search bar.
    SearchSuggestEnabled=falseという状態が、検索バーの検索候補を隠してくれない)

  • 「検索候補」を擬人化して主語にする:

    Search suggestions on the search bar are visible with SearchSuggestEnabled=false.
    SearchSuggestEnabled=falseという設定の時に、検索バーで検索候補が見える)

どうでしょう。このくらいの英文なら、自分にも書けるような気がしてきませんか? これに

  • There is an enterprise policy SearchSuggestEnabled.
    (~という法人向けポリシー設定があります。)

  • The location bar shows search suggestions. The web search bar also.
    (ロケーションバーは検索候補を表示します。Web検索バーも同様。)

などの文を添えれば、上述の例で報告したかった内容は言い表せたと言っていいでしょう。

いかがでしょうか。長く複雑な構造の文章で表されていた内容であっても、一文一文を細かくぶつ切りにして、それぞれに主語を与えてSVOの形で書き直してみれば、こんな要領の平易な英語の文章で表現できるのです。

ソフトウェア開発の場面であれば、「モジュール名」「メソッド名」「変数名」「ソースコード中の行番号で示した行」などなど、他にも色々な物が「登場人物」になり得ます。皆さんも、名前が付いている物は片っ端から主語にしてみて下さい。

ありふれた動詞や形容詞を使おう

文章をぶつ切りにしてSVOの文型に揃える事には、同時に、使う動詞や形容詞を辞書から見つけやすくなるという効果もあります。というのも、和英辞書で動詞や形容詞を調べると例文は大抵「誰々が何々をする」のようなSVOの文の形になっているからです。辞書で見つけた例文をそのまま使える、これは大きな利点です。

また、先の例で使った動詞が

  • show(表示する)
  • hide(隠す)
  • are visible(~が見える状態である)

という、教科書に出てきたりプログラムやCSSの中で見かけたりするような、特に難しくない・ありふれた単語だったという点に気が付いたでしょうか? 「やさしい日本語」において、難しい単語を使わず平易な単語を使うように言い換える、多少の細かいニュアンスをそぎ落としてでも端的に意味が通じる言葉を選ぶという事を説明しましたが、英語でも同じ事が言えます。言いたい事に厳密に当てはまる難しい英単語は、おおむね意味合いが通じる平易な単語で代用できるのです。平易な動詞や形容詞でも、主語を変えると意外と広い意味で使える、というのもSVOの文型を使う事の利点です。

筆者はこういう場面で使われやすい英単語をもう少し知っているので、実際にはこのような場面では状況に応じて

  • appear(出現する、表れる)
  • append / add(追加する)
  • disappear(消える)
  • deactivate / disable(無効化する)

のような単語を使うと思います。ですが、こういった単語を知らなくても、先の平易な単語でも言いたいことは通じます。より厳密な単語を覚えていくのは追々で問題ありません。他の人が書いている文章で「この単語、なんていう意味だろう?」と思ったらその場で調べて、自分が似たような場面に遭遇したら次から使ってみる、そんな感じで少しずつ語彙は増えていくので安心して下さい。

とはいえ、ソフトウェア開発の場面で特有の頻出単語という物は確かにあります。例えば以下のようなまとめの記事もありますので、暗記が得意なら、頻出単語をあらかじめ頭に入れておいてもいいでしょう。

困った時は箇条書きにしよう、文章以外の表現手段も使おう

報告に盛り込むべき内容の話において、不具合や要望を伝える時は

  • steps to reproduce(再現手順)
  • expected result(期待される結果)
  • actual result(実際の結果)

を軸にすると上手く言いたい事を整理できる、という事を述べました。実際に報告を書く段階においては、このそれぞれを前述のようなぶつ切りのSVOの文型で書いていけばよいという事になります。

しかし、そうすると今度は文同士の繋がりをどうするかで悩む事になりがちです。確かに、ぶつ切りの文章は少々不格好なのは否めません。せめて順接*2か逆接*3かくらいは示しておきたい……いやそれだけじゃなくて因果関係も示したい……という風に考え始めて、またしても「日本語での表現の仕方は分かるが、英語での表現の仕方が分からないので、手が止まる」というドツボにはまり込んでしまうのは、よくある事です。

筆者は、そこで悩むくらいなら箇条書きで書いてしまう事をお勧めします。例えば対等な物を並べるなら、この項の冒頭のように序列無しの箇条書きにすればいいですし、順番や優先順位に意味がある場面では、項目の頭に数字を付けた序列付きの箇条書きにすればいいです。例えば「再現手順」は以下の要領で書けるでしょう。

  1. ◯◯をする。
  2. ××をする。
  3. △△が◇◇になる。

これなら、受け取り手が順番を読み違える事は無いですし、「Repeat steps from 1 to 4.(1から4を繰り返す)」のように手順そのものを指し示す抽象的な書き方も容易にできます。また、箇条書きを階層化すると、

  1. ◯◯をする。
    • ◎◎が出る。
  2. ××をする。
    • ◎◎が消える。
  3. △△が◇◇になるかどうか。
    • ◇◇になっていたら、4に進む。
    • ◇◇になっていなかったら、1に戻る。

というように、各項目に付随する補助的な情報だったり、条件ごとに変わる操作だったりと、各項目の因果関係や依存関係を含んだ複雑な情報もスッキリ表せます。

あるいは、ここまでやるならむしろフローチャートの図で情報を示してもいいかもしれません。そう、説明する方法は「英語の文章」でなくてもいいのです。スクリーンショットで示した方が早ければスクリーンショットに丸や矢印を描き込んだ物を添付してもいいですし、静止画では説明が難しければスクリーンキャストをYouTubeあたりにアップロードして伝えてもいいです。あるいは、プログラムやデータを実際に作れるのであれば、それを使って実行すれば必ず現象が再現するというテストケースを添付するのが一番いいです。

思い出してみて下さい。皆さんがしたい事は、「英語の文章を書く事」ではなかったはずです。本当にしたい事は「不具合を伝える事」や「要望を伝える事」の方ですよね。その手段として日本語が通じないから、別の手段として英語で説明しようとしてるだけです。「英語という手段で説明する事」自体に囚われてしまうと、本来の目的を見失ってしまいがちです。そんな時は画面から目を離して、落ち着いて深呼吸しましょう。そうすると、別のやり方を思いつけるかもしれません。とにかく自分に今使えるあらゆる手段を柔軟に使い分けて、どんな形ででも伝えたい事を伝える事が一番大切です。

もっと言うと、「OSSプロジェクトのイシュートラッカー」は美しい英文を披露する発表の場ではありません。OSSを公開して実際に障害報告を受ける側になっている筆者の実感としては、ぶっちゃけ、流暢な英語で芸術的に見事な文章をたらたらと書かれても、英語が不得手な筆者はかえって読むのに苦労するばかりで、全然嬉しくないです。それだったら箇条書きで書いてくれた方が、ずっと読みやすく・分かりやすくて助かります。

そう、これも忘れてしまいがちなのですが、OSSプロジェクトで既に英語で文章を書いている人だって必ずしも英語が得意とは限らないのです。OSSの開発者には中国人もいれば韓国人もいるし、ロシア人やインド人もいます。皆さんもどうかその事を心に留め置いて、「英語」にばかり固執しないようにして貰えると筆者は嬉しいです。

自信が無ければ英和翻訳にかけてみよう

話題がまた少し逸れました、英語の話に戻りましょう。

前述したような考え方に則って英語の文章を書いてみたけれど、ちゃんと書けているのか、本当に通じるのか、自信が無い……そんな時は、書いてみた英文を「英語→日本語」の機械翻訳にかけてみる事をお勧めします。

機械翻訳は、「日本語に熟達した人が書いたこなれた日本語の文章を、そのニュアンスを捉えた上で、こなれた英文に翻訳する」というような高度な翻訳は(まだ)こなせませんが、「平易な英文を、平易な日本語の文章に翻訳する」のには充分すぎるくらいに使えます。英和翻訳した結果の日本語の文章を読んでみて、言いたかった事がちゃんと言い表されていると思える結果になっているなら、その英文は人前に出して全然大丈夫です。

筆者も実際に、書いてみた英文の正しさに自信が無い時はGoogle翻訳で英和翻訳してみています。その結果を見ながら、単語の意味や前置詞の使い方を間違えている事に気が付いて手直しするという事も多々あります。

平易な文章の英和翻訳が実用になるなら、平易な文章の和英翻訳の結果も使えるのでは? と思うかもしれませんが、筆者はそれは以下の3つの理由からお勧めしません。

  • 前述しましたが、日本語に熟達した人が日本語で文章を書くと、どんなに気をつけていても、無意識のうちに主語や目的語の省略などを行ってしまいがちです。機械翻訳ではそのような「文章の中には含まれていない外部の情報」を推測する事が難しいために、誤訳が発生しやすいです。
  • 和英翻訳された後の英文が自分自身の英語力を大きく超える物だった場合、その意味を自分で読み取る事すらできず、翻訳結果が正しいかどうかを自分では判断できないという事が起こります。
  • サービス・ツールによっては、機械翻訳の結果の文章の使い道を利用規約で制限している場合があります。例えば、OSSは商用利用を禁止しませんが、翻訳結果の商用利用を禁止しているサービスで翻訳したテキストをOSSに含めてしまうと、利用規約に違反してしまいます。実際に、Web翻訳の結果をOSSに不用意に入れた人がいたために、その人が過去関わった全ての翻訳済みリソースについて権利面での妥当性を再確認せざるを得なくなったという事例があります。

近年、機械学習の発展のお陰か、みらい翻訳のように「こなれた日本語を入力しても、それなりにこなれた英語の文章に翻訳できる」*4翻訳サービスが登場してきています。しかし、どれだけ翻訳精度が向上しても上に挙げた2つ目や3つ目の懸念点は解消されません。機械翻訳の結果はあくまで、最終的にOSSに含めるテキストや投稿する文章そのものではなく、それらを考えるための判断材料として使うに留める事を、筆者は強くお勧めします。

まとめ

以上、OSSへのフィードバックで避けて通れない「英語で文章を書く」という事について、テクニックよりは原理原則に焦点を当てて知見を紹介してみました。

実は、ここに書いたようなことは海外旅行などでも使える一般的な考え方です。実際に、悪魔英語 喋れる人だけが知っている禁断の法則という漫画にもおおむね似たような事が書かれていました。「学校である程度の英語教育を受けたにも関わらず、自分からは英語を話せない・書けない」という人にとっての処方箋は、だいたい似たような感じの所に落ち着くのかもしれません。

ここで述べた事を実践して書かれる英語は、はっきり言えば「たどたどしくてつたなくて不格好な、ヘタクソな英語」です。でも、それでいいんです。ネイティブスピーカーとリアルタイムで舌戦を繰り広げるとか、格式高い場でスピーチをするとか、そういう場面でもない限りは 「だいたい通じる下手な英語」で大抵は間に合います

確かに、海外に移住して「生活」するとか、「相手に隙があれば目ざとくそれを突いて、とにかく自分の利益を最大化しよう、という考え方をしている相手」との交渉といった場面になってくると、英語が下手だと相手からナメられたり足下を見られたりといった不利益を被る事もあるようです。ですがOSS開発の場では、筆者が知る限りは別にそんな事はありません。むしろ、インド人や中国人など英語ネイティブでない人が、めちゃくちゃな英語で普通にコミュニケーションしているという場面の方をよく目にします。世界中で見れば「英語のネイティブスピーカー」より「外国語として英語を使う非ネイティブスピーカー」の方が多いわけで、そういう意味では我々の方が多数派なのです。特別に自分が劣っているというわけではないので、安心して下さい*5

このあたりの事について、英語話者の視点から書かれた「酷い英語をもっとお願いします」という記事(※リンク先はその日本語訳)があります。英語話者の人にとっても、英語が壁となって有用な意見やフィードバックを得られない事は損失なので、英語の上手い下手なんて気にしないでガンガン伝えて欲しい、という感覚があるのです*6。ですから皆さん、どうか物怖じしないで英語での報告に挑戦してみて下さい。

*1 これは前の記事でも紹介した、実際に報告を行った事例です。実際の報告内容も併せて参照してみて下さい。

*2 後に続く文章が前の文章にそのまま続く事を示す接続。日本語なら「そして」「さらに」など、英語なら「and」「then」などがあたります。

*3 後に続く文章が前の文章を否定する接続。日本語なら「しかし」「なのに」、英語なら「but」「however」などがあたります。

*4 英語が得意で留学経験もあるような人が見ても、「なるほど確かにこう書けるな」と思えるレベルの翻訳結果が得られているようです。

*5 かといって、上達しようという意識を全く持たなくてもよいという訳でもありません。自分の言いたい事を適切に表現できる英語力があった方が、説明や説得をよりスムーズに進められる事は間違いありませんから。

*6 この対極の話として、努力で英語を身に付けられた人が「みんな英語にすればいいじゃん」とローカル言語への翻訳を否定する発言をした事例がありましたが、その事例ではプロジェクト運営者サイドからも改めて、英語だけが全てではないという事が述べられていました

2019-07-12

OSSへのフィードバックをしてみたいけど、何をフィードバックしたらいいのか分からない

結城です。

これまで、「OSSを使用していてトラブルに遭遇しているか、改善の提案があり、その情報を開発元のイシュートラッカーに伝えようとしている」という人向けの情報として、「どういう場所に報告するのが適切か」「どんな内容を書くのが適切か」という話をしてきました。

しかしOSS Gateワークショップにビギナーとして参加された方の中には、そういった「どうやるか」の前の段階の話として、「フィードバックしたい情報」をまだ持っていないという人もいます。というか、「別に今の段階で何か具体的にOSSの使用上で困っているわけではないが、とにかくOSS開発に関わりたい、OSS開発に関われる人になりたい」という人の方がまだ数としては多く、OSS Gateワークショップはまさにそのような人を想定した内容になっています。

なので、OSS Gateワークショップでは「フィードバックするネタ*1の見つけ方」からやるようにしています。この記事ではワークショップで実際に伝えているやり方も含めて、「フィードバックするネタ」の見つけ方を紹介します。

どのOSSにフィードバックするか

ネタを探すには、当然ですが、ネタを探す先のOSSが必要です。

ワークショップに来られた方に「このOSSにフィードバックしたいなあ、というプロジェクトは何かありますか?」と聞くと、「Linuxカーネル」や「Docker」といった有名なプロジェクトの名前を挙げられる事が結構あります。

ワークショップでは「なるべく普段から使っているOSSを題材にしましょう」と案内しているので、その意味ではこれらの名前を挙げても間違いではないのですが、単に「OSS」と言われてそれらの有名なプロジェクトくらいしか思い浮かばなかったと見受けられるケースでは、筆者はなるべく、もっと身近な・小規模のプロジェクトをお薦めするようにしています。というのも、有名だったり大規模だったり歴史が長かったりするOSSは、「別に今の段階で何か具体的に使用上で困っているわけではない」という人がフィードバック先にするには色々とハードルが高い事が多いから*2です。

「普段使っているOSS」を意識してみよう

「身近なOSS」なんて思いつかない、という人でも、普段の仕事で使うアプリケーションやコマンドを作業の順番に従って思い浮かべてみると恐らくOSSがいくつも見つかるはずです。例えば以下の要領です。

  • インターネット上の情報を調べるためにChromeを起動した。
    • ChromeのベースとなっているChromiumはOSSです。
    • Chrome/Chromiumが依存しているcrc32cdom-distiller-jsenum34などの多数のライブラリ( chrome://credits/ で確認できます)もOSSです。
    • Chrome/Chromiumで使える拡張機能にもOSSが多くあります。
  • 作業を始めるために、ターミナルを起動して、Bashの上でgit cloneを実行した。
    • GitはOSSです。
    • BashもOSSです。
  • Ruby on Railsで構築されたサービスのユニットテストを走らせるために、bundle instalLしてrake testした。
    • Ruby on RailsはOSSです。
    • RubyもOSSです。
    • RailsやRailsアプリケーションのGemfileに従ってbundle installでインストールされた多くのGemもOSSです。
    • bundleコマンドを提供するBundlerもOSSです。
    • rakeコマンドを提供するRakeもOSSです。
  • ソースコードを編集するためにVisual Studio Codeを起動した。
    • VSCodeはOSSです。
    • VSCodeのベースになっているElectronもOSSです。
    • ElectronのベースになっているChromiumもOSSです。
    • VSCodeで使えるプラグインにもOSSが多くあります。
  • Vue.jsで構築されたフロントエンドにファイルを追加し、ESLintでエラーがない事を確かめて、Babelでトランスパイルした。
    • Vue.jsはOSSです。
    • ESLintもOSSです。
    • BabelもOSSです。
    • ESLintやBabelのpackage.jsonに従ってインストールされた多くのNPMモジュールもOSSです。
  • 編集したソースコードをコミットする時、コミットメッセージの編集にVimを使った。
    • VimもOSSです。
    • VimのプラグインにもOSSが多くあります。

こうして見てみると、開発に使用しているフレームワーク自体ツールがOSSであったり、また、有名なOSSも名前を聞いた事のないような無数のOSSを利用して作られていたり、という状況である事が分かります。現代のソフトウェア開発の現場では、OSSを全く使わずにいるということはあまりないでしょう。

「OSS」の見つけ方

ところで、いきなりたくさんOSSの例を挙げましたが、そもそも「どうだったらOSSなのか?」を説明していませんでした。

OSS(Open Source Software)とは、OSIがOSSライセンスと認めているライセンス一覧に載っているいずれかのライセンスの元で頒布されているソフトウェアのことです*3。例えばVSCodeでは、「Help」→「View License」で開かれるページの冒頭に「Source Code for Visual Studio Code is available at https://github.com/Microsoft/vscode under the MIT license agreement at https://github.com/Microsoft/vscode/blob/master/LICENSE.txt.(VSCodeのソースコードはMITライセンスの元で入手可能です)」と書かれています。MITライセンスは上記の一覧にあるOSSライセンスなので、VSCodeはOSSであると言える訳です。

他にも、ソフトウェアのライセンスを調べる方法はいくつかあります*4

  • GUIアプリであれば「(ソフトウェア名)について」「ライセンス」のようなメニューからこのような情報を確認できる事があります。
  • 配布ページやプロジェクトのトップページにライセンスが書かれている事もあります。
  • 公開されているソースコードのリポジトリ上にCOPYING(著作権情報)やLICENSE(ライセンス情報)といった名前のファイルがあり、そこに詳細が書かれている場合もあります。
  • READMEに端的に「このソフトウェアは何々ライセンスです」と書かれている事もあります。

「OSSに何かフィードバックしてみたいが、具体的にはビッグネーム以外すぐに思いつけない」という場合には、まずはこのようにして、自分が直接的・間接的に使用しているOSSを掘り下げてみて下さい。LinuxカーネルやDockerよりは、ずっと身近な所にOSSを見つけられるはずです。

些細な「つまずき」に敏感になろう

題材にするOSSを決めたら、次はそのOSSを普通に使います。そうして 普通に使っている途中で遭遇した些細な「つまずき」 が、フィードバックする内容となります。

しかし、そう言われても、何が「つまずき」なのかピンと来ないという人も多いでしょう。「普段から自分もOSSを使っていた事は分かったけど、べつにつまずいた事なんて無いんだけどな……」そう思うなら、それはあなたの運がよかったか、あなたより前につまずいた他の人が道を整備してくれていたか、あるいは、あなたが「自分がつまずいたという事」自体に気付いていないかのいずれかです。

最後の「気付いていない」というのは実はよくあります。ITに詳しくない人から「エラーって言われるんだけど!」と泣き付かれて、「何かした?」と聞いても「何もしてないって!」と本人は言うばかり、というのはよく聞かれる話ですが、それと同じ事を自分でも知らず知らずのうちにしている可能性があるのです。

「そんなバカな!」と思いますか? では、以下の事に身に覚えはないでしょうか?

  • 公式に情報が無いので、「(機能の名前) 使い方」でWeb検索してQiitaやStack Overflowや個人のブログの解説を探した。
  • 動かしてみたらいきなりエラーメッセージが出た。
  • よく分からないけどWebを検索したら対処法が出てきたので、書いてある通りにやったらエラーが出なくなった。

どうでしょう。「いつもの事だから、特別つまずいたとは思っていなかった」「そんな事でいちいち立ち止まってたら仕事にならないし」と思う人もいるのではないでしょうか。ですが、これらはすべて立派な「つまずき」なのです。

つまずくのは自分のレベルが低いからではなく、説明が足りないから
  • インストール説明の手順の中に、必要な前提条件が書かれていなかったので、インストールに失敗した。ググったら、◯◯というパッケージを先に入れておかないといけないと書いてあるブログが見つかった。
  • コマンドを実行しようとしたら、初期設定をして下さいというメッセージが出てエラーになった。
  • 機能を実行しても、ちゃんと動いたかどうかよく分からなかった。

実は、これらは実際にOSS Gateワークショップでもよく登場するつまずきです。こういうつまずきに遭遇した時、「つまずいたのは自分の勉強が足りなかったからだ」「こんな所でつまずくのは初心者だけに違いない、こんな所でつまずいたと人に知られるのは恥ずかしい」と思う人は少なくないでしょう。

しかし、謙虚に学ぶ姿勢は確かに大事なのですが、ここは一旦謙虚さを忘れて、自分の気持ちに素直になってみませんか?

  • インストール説明の手順の中に前提条件が書かれていなかったのでつまずいた。→必要なパッケージがあるなら、最初からそれも手順に書いておいたり、自動的にインストールするようになってくれていたりすればいいのに……
  • コマンドを実行しようとしたら、初期設定をして下さいと言われてエラーになった。→初期設定が必須なら、インストール手順の中に書いてあればいいのに……
  • 機能を実行しても、ちゃんと動いたかどうかよく分からなかった。→確認方法も書いてあればいいのに……

ほら、改善できそうな点が見えてきましたね。

多くの小規模なOSSプロジェクトでは、インストール手順や使い方の説明などのドキュメントの整備には手が回らなくなりがちです。この手の情報は、1回使い方を習得した後は2回3回と同じ人が見返す訳ではないため、今現在関わっている人にとっては「かかる手間のわりには自分自身が恩恵を受けられない」という性質があるからです。

しかしそのような状況が長く続きすぎると、「既にわかっている人」以外にとってそのOSSはどんどん使いにくく・関わりにくくなってしまいます。そうすると、新規のユーザーや開発に関わる人が増えない一方で、関わっていた人達はライフステージの変化などにより離れていき、最終的には新機能の追加も無ければ脆弱性の修正も無い、誰にもメンテナンスされない見捨てられたプロジェクトになってしまいます。そうなって一番困るのは、一般のユーザーです。「不親切な案内を丁寧な内容に直す・補完する」のは、プロジェクト自体の長期的な継続のために重要なフィードバックと言えます。

ですので、「つまずいたのは自分の勉強不足のせい」「こんな所でつまずいたと人に知られたら恥ずかしい」という考え方は一旦封印しましょう。むしろ、 「つまずいた自分じゃなく、こんな最初の方につまずくような部分が残ってる事の方が悪い」 くらいに考えておいてちょうどいいくらいです。

「注意しなくても使える」のが一番いい

また、「使う上で注意が必要」という箇所も、フィードバックのきっかけにしやすい部分です。

例えば、筆者はFirefoxの法人利用においてポリシー設定でのカスタマイズをよく行っていますが、この機能自体は導入されてまだ歴史が浅いために、色々とこなれていない部分があります。つい最近も、「検索候補を表示するかどうか」を制御するポリシー設定がロケーションバーに対してしか作用せず、単独のWeb検索バーでは設定に関わらず常に検索候補が表示される、という制限事項に遭遇しました。

そういう制限事項を見つけた時に、「この機能にはこういう制限事項があるので、使う時は注意が必要だ」というノウハウを社内で共有したりブログやQiitaに書いたりするのは、もちろん有用な事です。ただ、そういった情報ばかりが増えていくと、新しく使い始めようとした人には「なんだか、使うのに色々勉強が必要なんだな……」「気軽には使えないんだな……」という印象を持たれ、いずれは「敷居が高そうだから、触るのやめておいた方がいいかな……」と思われるまでにもなってしまいかねません。

OSSに関わりたいと考えている人は、ぜひそこから一歩先に考えを進めてみて下さい。「使う時に特別な注意が必要ないのが一番いい」、これはどんな場合にも言える事です。 その視点で考えれば、「検索候補を無効化する設定はロケーションバーだけでなく当然Web検索バーにも反映されるべきで、そうなっていないのは不具合だ」という見方ができるようになり、「注意点」に見えていた物は「フィードバックした方がよい点」として浮かび上がってきます

実際に、先のポリシー設定の制限事項は筆者が不具合として報告した結果、改善されるべき点と認識してもらう事ができました。そして、提出したパッチが取り込まれた結果、「ポリシー設定で検索候補を表示しないように設定したら、ロケーションバーでも単独のWeb検索バーでも検索候補が表示されなくなる」という、使う上で特別な注意が必要ない状態になりました。

ドキュメントの例にしても機能の例にしても、つまずきに対して「無いならしょうがない」「駄目ならしょうがない」「変えられないんだからしょうがない」と考えていると、どうにかして自分のフィールドの中だけで物事を解決しようという、いわゆる「運用で回避」の方向に行ってしまいがちです。しかし考え方を少し変えれば、そういった「面倒事」は「解決した方がよい問題」と言い換えられます。「OSSにフィードバックするって、何か特別な事だ」「そんな事ができるのは別の世界の人だ」と思っていた人でも、そう考えてみれば、OSSへのフィードバックとは決して特別な事ではなく、日常の延長線上にある物だ、という事を実感できるのではないでしょうか。

まとめ

現代は「問題は自分で解決できる」という実感を得にくい時代かも知れません。ITの分野だけ見ても、今はスマートフォンのように分解修理が不可能な機械は多いですし、Webサービスにしても、クラウドで提供されている物に何かトラブルがあってもユーザーはただベンダーによる復旧を待つ事しかできません。「駄目なら、しょうがない」と早々に諦めなくてはならない場面は非常に多いです。

また、「未解決の問題の解決に取り組む」という事は、まさしく挑戦です。成功が確約された挑戦というものは無く、挑戦するからには失敗のリスクも伴います。「これ以上の失敗はしたくない、失敗は許されない」という空気が強いと、そういう点でも挑戦するのは憚られるものでしょう。前述したFirefoxのポリシー設定の改善提案も、Firefox開発チームの理解や協力を得ることに失敗していたら、制限がそのまま残り続けることになっていたかもしれません。

しかし、「できることは何も無い」「絶対に解決できると分かっている問題以外、取り組みたくない」と最初から諦めていると、それ以上知識も深まらなければ経験も増えません。問題に遭遇した時、「もしかしたら何か解決策があるかもしれない」「解決できるかどうか分からないけど、もし解決できるなら解決したい」と考えて試しに掘り進めてみる、それこそが多くのOSS開発者が日常的にやっている事です。「OSS開発者という人になりたい」という思いは一旦脇に置いて、まずは「OSS開発者がしているような考え方をして、OSS開発者がしているような行動を取る」という事を実践してみて下さい。そうして行動している人、それが「OSS開発者」の正体です。

「OSS開発者」は、プロのスポーツ選手や士業のように試験に合格すればなれる物でも、名誉として手に入れる肩書きでもありません。行動しているという状態こそが「OSS開発者」なのだと言えます。行動すればすぐになれる「OSS開発者」に、皆さんも是非なってみて下さい。

そのような行動をいきなり一人で始められる自信が無いという方は、OSS Gateワークショップがいいきっかけになるかもしれません。東京以外の地域でもワークショップが開催されていますので、一度足を運んでみてはいかがでしょうか。

*1 OSS Gateワークショップでは「フィードバックポイント」と呼んでいます。

*2 例えば、Issue Trackerではなくメーリングリストが主な情報のやり取りの場だったり、独自の高機能なイシュートラッカーを使用していたり、プルリクエスト形式ではなく伝統的なパッチの形で修正を送る必要があったりという事があります。

*3 他の意味・定義でOSSという言葉を使う人もいますが、OSSという言葉を生み出し広めたOSIはそのように定義しています。

*4 多くのOSSライセンスは「何らかの形でライセンス名を明記する事」を利用条件に含めているため、根気よく調べると何かしらの情報を見つけられるようになっているはずです。OSSではない物をOSSと誤認して二次利用すると、もしかしたら訴訟を起こされたり賠償を求められたりするかもしれませんので、ライセンスに関する情報を見つけられない場合は安全側に倒して、OSSではない物と判断する事をお勧めします。

2019-07-08

MariaDBへ不具合をフィードバックするには

はじめに

MySQL/MariaDB用のストレージエンジンのひとつにMroongaがあり、MySQL/MariaDBユーザーに高速な日本語全文検索機能を提供しています。
先日リリースされたMroonga 9.04では、MySQL 8.0向けのパッケージの提供(CentOS 6とCentOS 7)もはじまりました。

クリアコードでは、Mroonga関連プロダクトのサポートサービスを提供しています。
今回は、サポートサービスをきっかけにMariaDBへ不具合を報告し、修正されたケースがあったのでその事例を紹介します。

不具合の発覚のきっかけ

Mroongaでは、ユーザーの利便性のためにMariaDBにMroongaをバンドルしたWindows版のパッケージを配布しています。
不具合発覚のきっかけは、Windowsでこのパッケージを利用中のお客様からの「レプリケーション中にスレーブがとまる」という報告でした。

実際にお客様から提供された問題発生時のログを確認すると、次のようなメッセージが記録されていました。

[ERROR] Semisync slave socket fd is 580. select() cannot handle if the socket fd is greater than 64 (FD_SETSIZE).
[Note] Stopping ack receiver thread

このエラーメッセージに対応するのはMariaDBの以下の実装箇所でした。

該当するソースコードを抜粋すると次のようになっていました。

#ifndef WINDOWS
      if (socket_id > FD_SETSIZE)
      {
        sql_print_error("Semisync slave socket fd is %u. "
                        "select() cannot handle if the socket fd is "
                        "greater than %u (FD_SETSIZE).", socket_id, FD_SETSIZE);
        return 0;
      }
#endif //WINDOWS

ソケットのファイルディスクリプターの値が大きいせいで、スレーブからのackを受け取るスレッドが止まっていました。
「Windows以外で」上記のチェックが有効なはずなのに、実際には「Windowsで」エラーが発生していました。
これは、#ifndef WINDOWSWINDOWS が定義されていないことによるものでした。
通常こういう場合は #ifndef _WIN32 が使われるので、正しくありません。

実際、他の実装としてMySQLを確認したところ、Bug#23581389 として一年半以上前にまったく同じ問題の修正がなされていました。

不具合を報告するには

MariaDBの場合、MariaDB bugs databaseに不具合を報告します。

遭遇した問題は既知の問題として報告されていなかったので、新規課題の「作成」をクリックして問題を報告しました。
(課題の報告にはあらかじめアカウントを作成してログインが必要です。)

対象コンポーネントは「Replication」を選択し、環境には「Windows」であることを明記しました。
問題の説明では、エラー発生時のログや、実装のどこが問題であるかに言及しました。

実際に報告した内容は次のとおりです。

MDEV-19643の不具合報告

この問題については、報告から1ヶ月しないうちに修正されました。
修正版はMariaDB 10.3.17や10.4.7としてリリースされる見込みです。

まとめ

今回、サポートサービスをきっかけにMariaDBへ不具合を報告し、問題が修正されたケースがあったのでその事例を紹介しました。
Mroongaに限らず関連プロダクトのサポートが必要な方は問い合わせフォームからご相談ください。

2019-07-01

OSSへのフィードバックには何をどう書けばいいのか、どのプロジェクトに報告すればいいのか

※注:この記事の対象読者は、「OSSを使用していてトラブルに遭遇しているか、改善の提案があり、その情報を開発元のイシュートラッカーに伝えようとしているが、どんな内容を書けばよいかわからない」という人です。そもそも「どこに報告すればよいかわからない」「イシュートラッカーに書き込んでいいかどうか不安がある」という方は、1つ前の記事をご参照下さい。

結城です。

1つ前の記事では、フォーラムとイシュートラッカーのどちらに書き込めばよいかわからない場合の考え方について詳しく語りました。その際、両者の目的の違いを以下のようにまとめました。

  • フォーラム:個人の問題の解決を図る場所。
  • イシュートラッカー:ソフトウェアの問題の解決、品質の向上を図る場所。

この記事ではその続編として、イシュートラッカーの目的から導かれる「適切な、書くべき報告の内容」を語ってみます。

書くべき内容、避けるべき内容

フォーラムとイシュートラッカーの目的の違いは、「投稿する内容をどうまとめればよいのか」という事を考える時の指針にもなります。「ソフトウェアの問題の解決」を目的とするイシュートラッカーに報告する内容は、その目的に沿って、「ソフトウェアの開発に関わる人達が、このソフトウェアの問題を解決するには、どのような情報が必要か?」という視点で整理する事になります。

例えば、以下の3項目は典型的な「書くべき内容」です。

  • Steps to reproduce:現象の発生条件、再現に必要な手順
    • 他の人は、どうすればその現象が起こるかを知らないし、あなたが普段どのようにその機能を使っているのかも知らないので、あなたが詳しく説明するしかありません。
    • メニューの選び方、機能の呼び出し順序など、あなたが無意識でしている操作の仕方がその現象を引き起こす決定的なきっかけとなっているかもしれません。
  • Actual result:実際に起こる結果
    • 他の人は、自分の環境で得られた結果が、あなたの見ている物と同じかどうか分からないので、あなたが詳しく説明するしかありません。
  • Expected result:本来期待される結果
    • 他の人は、あなたほどにはその機能や使い方の事を詳しく知らないかもしれないので、あなたが詳しく説明した方が効率がよいです。

その問題のせいで自分がいかに困っているか? という訴えは報告のメインの内容とはなり得ません。プロジェクト全体の中でその問題の重要度の高さを認識してもらうための材料として、問題の影響度・深刻さの説明としてそれらの訴えを盛り込む場合はありますが、そのような訴え自体が問題の原因究明や修正箇所の特定に役立つという事は、基本的には無いからです。

重複する報告も、場違いな報告も、無駄じゃないのでしていい

しかし、いざ報告しようとしても、

  • 既に他の人が報告しているにもかかわらず、自分の探し方が下手なだけで、既存の報告を見つけられていないだけかもしれない。
  • この問題をこのプロジェクトに報告するのが適切かどうか分からない。もっと適したプロジェクトがあるかもしれない。場違いな報告をしたら迷惑かもしれない。

という心配から報告をためらう、という事例はワークショップの中でもよくあります。筆者はそのような場合には、「不安を感じるくらいなら、自分が思う内容・表現のままでそのプロジェクトに報告してしまってよい」とアドバイスする事が多いです。

既知の報告がないか全く調べていない、というのはさすがにまずいですが、自分の思いつく表現で検索して報告が見つからないなら、その表現で報告して大抵は問題ありません。なぜなら、仮にその後既知の他の報告と重複していると分かって、そちらに誘導された上でクローズされたとしても、その報告内容自体が、同じ探し方をする人のための今後の誘導になるからです。

適切な報告先プロジェクトかどうか分からない、という場合も同様です。その問題が根本的には別のソフトウェアの問題だったと後から分かっても、同様の現象に遭遇した人のための誘導になるので、報告は無駄にはなりません。例えば1つ前の記事で紹介した事例では、今後同様の現象に見舞われて検索して辿り着いた人は、今後はMicrosoft IMEの動向に注意するとよい、という情報を得られる事になります。

また、根本的な原因については既に報告があったとしても、その報告で触れられている以外の現象の情報があると、それは問題の影響度合いを示す情報になります。当初の報告では些細な問題だから後回しにしようと判断されていた物が、実はユーザーレベルで大きな影響を受ける物だったと分かって、対応の重要度・緊急度が引き上げられるという事は度々あります。

却下された報告や提案にも価値がある

明らかな不具合というよりは「こうなっているとより良い」という提案の性質が強い報告の場合に多いのですが、「それは対応しない」「やらない」と判断され却下される場合もあります。そのような場合も、「無駄だった」「失敗した」という考え方をする必要はありません。というのも、「ある提案が却下された」という事そのものにも意義があるからです。

OSSのプロジェクトのスコープ*1は、必ずしもすべて明確化されているとは限りません。ある人にとっては「当然あるべき」と思える機能が無かったり、逆に、「無駄にしか思えない」という機能があったり、特に、それについて何の説明も無いという事はざらにあります。大規模なプロジェクトでは、すでに参加している人の間ですら解釈が別れていたりもします。

しかし、例えスコープが明文化されていなくても、一貫性を持った数々の判断の結果、その輪郭は段々と浮かび上がってきます。新たな判断の事例が1つ増えるという事は、不明瞭だった基準がより明確化されたという事になり、後から来た人にとって有用な情報となります。また、判断と共にその根拠が示されていれば、後になって状況が変わり根拠が薄れた時などに、それを理由として再考を求める事もできます。

そのような意義ある判断を引き出すためにも、議論は「自分の意見を押し通すため」ではなく「プロジェクトの目的をより良く達成するため」という点を念頭に置いて、「相手(敵) v.s. 自分」ではなく「問題 v.s. プロジェクト」という方向で行いましょう。反論も、主張の材料を補ったり、説明しきれていなかった理由を追加で説明したりと、意味のある反論をする事が肝要です。感情的に同じフレーズを繰り返すだけや、明確な根拠無しに強弁するだけのような不誠実な態度、他の参加者の共感を得られないゴリ押しは、百害あって一利無しです。

ただし、実際に手を動かす人が足りないから労力的に対応できないという理由で却下される場合は、より深く関わるチャンスです。自分で実装してパッチ(プルリクエスト)を提出し、その後もメンテナンスを引き受け続ければ、あなたも立派に開発者の仲間入りという訳です。

「対案」無しでも報告・提案していい

  • 報告だけでは無責任なのではないか? 対案まで考えてから出さないと駄目なんじゃないか?
  • パッチ(プルリクエスト)まで書いて初めてOSS開発への参加と言えるんじゃないか?

といった点で悩んで報告をためらう、というケースもあります。筆者はこのような場合、「対案は無くてもいいから、まず報告してみよう」とアドバイスします。

もちろん、コードのレベルで修正の仕方まではっきり分かっているのなら、パッチやプルリクエストの形でフィードバックできるに越した事はないです。しかし、そうできないならフィードバックしない方がよい、という事はありません。

むしろ、自分の考えつく解決策が最良であるとは必ずしも限りません。詳しい事情が分かっていない段階で自分の思い込みだけで独りよがりに実装を作り込んでしまうと、その労力がまるっきり無駄になるという事もあり得ます。例えば、ある機能の不具合を見つけて、その問題を解消するための複雑な変更を持ち込んだが、単にその機能全体を既存の別ライブラリで置き換えれば済む事だった、というような事はよくあります。筆者も、過去に強引な実装のパッチを代理で投稿してもらった事がありましたが、実装の筋が悪すぎたためか、それ以上話が進む事はありませんでした。

イシュートラッカーは「問題の解決」に取り組むための場であって、「特定の解決策の実現」に取り組む場ではありません。解決策は関わる人みんなで考えてよく、一人で抱え込む必要はないのです。よく「文句を言うなら対案を示すべき」という言い方がされますが、だからといって「対案が無いなら文句を言ってはならない」は真ではありません。問題解決に取り組む1つのチームの一員として、対案を考える事も含めて皆で取り組む、という姿勢・考え方をするのがポイントです。

同じ問題に取り組む仲間と思って接しよう

真摯に議論しようという話とも共通しますが、「問題を解決する開発者達 v.s. 外部から善意で協力しているただのユーザーの自分」という壁を自ら作る事は避けた方がよいです。対等な関係ではないという事を過剰に意識してしまうと、実際以上に大きな上下関係を自分の中に作り上げてしまい、コミュニケーションの妨げになります。

1つ前の記事ではOSSに新たに関わる人を新入社員に例えましたが、そう考えれば、なぜ自分とプロジェクトの間に過度に壁を作るべきでないのかが分かるのではないでしょうか。新メンバーがお客様気分で上げ膳据え膳に期待してふんぞり返るのはおかしいですし、かといってへりくだりすぎるのもおかしいですよね。

無論、雇用されフルタイムで開発に従事している人と、外部から余力の範囲で関わっている人の間では、立場も権限も異なります。しかし、そのプロジェクトの目的、あるいは報告した1つの問題の解決に向けて一緒に取り組む仲間という点では差は無いはずですから、お互いに敬意を持って接する事が大事です。

特に、「自分は問題で損をした被害者なんだ。その被害者自身がわざわざ協力してやってるんだ。だからお膳立てはあいつらの方で整えてくれて然るべき」というような捉え方は、問題の解決を遠のけさせこそすれ、近づけさせは決してしません。このような意識を強く持ってしまうと、相手の無理解を責めたり相手をやり込めたりする事にばかり意識が向いてしまい、ひいては特定個人に対する罵倒や中傷にまでエスカレートし得ます*2

まとめ

以上、OSSにフィードバックする事に不慣れな場合に悩みがち・判断に迷いがちと思われる点について、イシュートラッカーの目的から導かれる「適切な考え方」を、筆者の感覚から語ってみました。

表現を変えながら繰り返し何度か述べていますが、OSS開発に関わる際は基本的に「問題 v.s. それに立ち向かうプロジェクト参加者達(そして、その一員としての新人の自分)」という構図で考え取り組む事が大事だというのが、筆者の考えです。かつては思ったように成果を出せなかった自分も、そのように考えられるようになって以降は、より有用な報告ができ、提出したパッチやプルリクエストを取り込んでもらえる機会も増えたように思います。まだOSSの開発への参加に不慣れで、報告がうまくいかなくて悩んでいるという方は、この記事で述べた点を意識してみてはいかがでしょうか?

また、この記事で述べた事は実際にはOSSに特有の話ではなく、一般的に企業内でバグ票をやり取りするような場合にも共通して言える事です。1つ前の記事ではOSSのイシュートラッカーを出入り自由の社屋や工場に例えましたが、OSS開発の場で広く共有されているノウハウの中には、普通に仕事の上で有用な物も多く含まれています。特に、OSS開発では「多くの人を雇って人海戦術で乗り切る」というような力業での解決が難しいため、いかに少ないコストで最大の効用を得るかという点で工夫がなされている事も多いです。当社のOSS開発支援サービスでは、OSS開発に参加していきたいという企業さまのお手伝いだけでなく、OSSコミュニティでの開発ノウハウを企業内での開発に活かすお手伝いも承っています。そういった事に関心をお持ちの担当者さまがいらっしゃいましたら、ぜひお問い合わせフォームよりご連絡下さい。

*1 プロジェクトとして取り組む事の範囲。

*2 お恥ずかしい話ですが、筆者も何度かこれで失敗しています。

2019-06-19

OSSへのフィードバックはユーザーフォーラムとイシュートラッカーのどちらに書くべきか?

※注:この記事の対象読者は、「OSSを使用していてトラブルに遭遇しているか、改善の提案があり、その情報を開発元に伝えたいが、どこで伝えればよいかわからない」という人です。「どういう体裁で報告すればよいか分からない」「何を報告すればよいか分からない」という人向けの話はまた日を改めて書くつもりです。

結城です。

OSS Gateワークショップで、初めてフィードバックをしようとしているビギナー参加者のサポートをしていると、ビギナー参加者から以下のような質問を受ける事があります。

  • こんな簡単な・くだらないレベルの事を報告してもいいんでしょうか?
  • ユーザーフォーラムとイシュートラッカー(バグトラッキングシステム)*1のどちらに報告すればいいんでしょうか?
  • どのプロジェクト(開発元)に報告すればいいんでしょうか?

これらの点に対する筆者の回答は、端的には以下のようになります。

  • 簡単でも些細でも何でも、あらゆる未解決の問題は報告していい。
  • これは自分個人の問題ではなく、そのソフトウェアの全ユーザー向けに解決されるべき問題だ、と思えるならイシュートラッカーに報告していい。
  • 自分で確かめられる範囲ではこのプロダクトの上でしか問題に遭遇しない、他のプロダクトでは問題が再現しない、と言えるならとりあえずその開発元に報告していい。

奇しくも最近、これらの事を考える上で非常に興味深い事例に出会いました。以下、それを題材として上記の点をどのように判断しているのかを語ってみます。

報告者からフォーラムへ、フォーラムからイシュートラッカーへ、MozillaからMicrosoftへ

当社の法人向けのThunderbirdサポートにおいて、数ヶ月前から複数のお客さまより「メール作成時に日本語入力で漢字への変換ができなくなる」という現象のお問い合わせを頂くという事がありました。

お問い合わせの中にはMozillaZineフォーラムに同様の事例の報告がある旨をお書き添え頂いた物もあり、参照してみると、スレッドの大まかな流れは以下のようになっていました。

  1. このような現象が起こっている、既知の解決策はどれも有効でなかった、という情報が投稿される。(2018年12月末)
  2. 現象の再現条件の特定が複数人によって進められる。(2018年12月末~1月頭)
  3. Thunderbirdに対して行われた変更のうち、どの変更以降からこの現象が起こるようになったのかが特定される。(2019年1月)
  4. 暫定的な回避作として、このアドオンをインストールすると現象が起こらなくなる、という情報が寄せられる。(2019年1月末~2月)

当社の検証環境でも、条件が整うと確かに現象が再現するという事を確認できました。そこで、Mozillaが製品開発のために運用しているイシュートラッカーであるBugzillaに何か情報が無いかと検索してみたのですが、思いつく限りのキーワードで検索しても該当するBugを見つけられませんでした。そのため、ひとまず筆者自身で新しいBugとして報告を行いました。これが2019年5月の事です。

もし既に報告されている問題であれば、そちらへの参照情報が追加され、この新しいBugは「重複する報告」としてクローズされます。しかし実際にはこれが最初の報告だったようで、最終的にはFirefoxとThunderbirdの共通基盤であるGeckoエンジンの日本語入力部分の担当であるMozillaの中野さんにまで連絡が行くという事にまでなりました*2

ただ、話はそれで終わりませんでした。仮にこれがGeckoの明白なミスに起因する現象だったのであれば、Gecko側で修正されてBugはクローズされます。しかし実際には、Mozillaの中野さんによる詳細な調査ではGecko側に問題は見当たらなかったため、Microsoft IME(MS-IME)側の不具合である可能性があるとして、中野さんからMicrosoftのフィードバック窓口へ2019年5月末に報告されたそうです。

以上が、この件の2019年5月末時点の状況です。

ずっと以前から現象が確認されていても全く進展が無かったのが、イシュートラッカーに報告された途端に事態が動き出す、という事は度々あります。上記の経緯のうち、フォーラムからBugzillaへの報告までの流れはまさにそのような推移に見えます。

しかし最終的な結果を見ると、Thunderbirdの問題でなくMicrosoft IMEの問題である可能性があるなら、MozillaのイシュートラッカーであるBugzillaではなくMicrosoftに報告するのが正しかったのではないか? という見方もできます。もし仮に本当にMicrosoft IMEの問題だったのであれば、現象の確認から適切な場へ情報が伝わるまでに半年もの時間がかかってしまったという事になります。

さて、この問題は一体どのように取り扱われるべきだったのでしょうか?

誰の問題か、何をしたいかという観点で持ち込み先は変わる

ここまでの経緯には多数の論点が入り交じっていますが、目的・動機という点では大まかに以下の2つにまとめられます。

  • 報告者にとっての「どうすれば自分が今困っているこの状況を脱せるのか?」という視点=個人の問題を解決したいという視点
  • 開発者にとっての「そのソフトウェアにどんな問題があるのか、解決されるべき問題はどんな内容なのか」という視点=ソフトウェアの問題を解決したいという視点

ソフトウェアの問題が解決された結果として個人の問題が解決されるという事も、個人の問題を解決するためにした事が結果的にソフトウェアの改善にもなるという事もあります。しかし、それらはあくまで偶然の結果そうなっただけで、最初から偶然に期待するというのは効率が悪くお薦めできません。

OSSでは、というよりもこれは人間社会で一般的に言える事ですが、報告・投稿を持ち込む先は目的によって決めるものです。問題がいつまでも解決されない場合、最大の原因は問題を持ち込んだ先が適切でなかったからだった、という事がよくあります。

フォーラムが解決できる問題

上記の「フォーラムに報告がなされた例」では、関わった人達の期待とその結果は以下のように言えます。

  • トラブルに遭遇した本人(フォーラムに最初に投稿した人)にとって:
    • 期待:
      • 自分が遭遇した問題の解決。
    • 結果:
      • フォーラムに集う人達に調べて貰えて、発生条件を絞り込めた。
      • その人固有の状況で有効な暫定的回避策が判明し、当座をしのげるようになった。
  • フォーラムに集う人達にとって:
    • 期待:
      • 相談者によって持ち込まれた問題の解決の手助け。
    • 結果:
      • 助けを求めてやってきた人の問題の解決に力を貸せた。
  • その後同様の問題に躓いた人達にとって:
    • 期待:
      • 問題にそもそも遭遇しない事
    • 結果:
      • ソフトウェアの問題は未解決のままだったので、同じ現象に後から遭遇してしまった
      • フォーラムの投稿者と状況が一致していれば、暫定的解決策で現象を回避できる。
      • フォーラムの投稿者と状況が一致しないと、暫定的解決策では現象を回避できない
  • ソフトウェアの開発・品質向上に関わる人達にとって:
    • 期待:
      • ソフトウェアの開発が進む事、品質が向上する事。
    • 結果:
      • 普段見ていない場所で話が進行していたので、問題の存在自体を把握できなかった
      • 問題が把握されなかったため、対応もなされなかった

このように、個々のトラブルの解決や、同様のトラブルに遭遇した人の解決のための情報源にはなるものの、同様のトラブルが繰り返される事そのものの解決にまではならないのが、ユーザーフォーラムの性質と言えます。

また、ユーザーフォーラムは「ユーザー同士の相互扶助の場所」であるという点にも注意が必要です。基本的にはボランティア運営なので、分かる人がいなければ反応を得られないという事もあります。そこに集まる人は全く無関係の人よりは詳しい人である可能性が高いですが、開発者ほどには詳しくない場合も多いです*3

イシュートラッカーが解決できる問題

では仮に、最初から問題をイシュートラッカーに投稿しようとしていたら何が起こったでしょうか。筆者の見立てでは、最悪の場合の結果は以下のようになったのではないかと考えています。

  • トラブルに遭遇した本人や、同様の問題に躓いた人達にとって:
    • 問題の原因調査の方法に詳しくないと、有用な情報を提供できない。
    • そもそも技術的な知識の有無以前に、英語が不得意だと、自分の置かれた状況(問題の発生条件)を上手く説明できない。
    • 原因が特定され、次のバージョンで修正されても、今使っているバージョンでの問題は解決しない
    • 調査の過程で暫定的な解決策がたまたま見つからなければ、今この瞬間のトラブルは解決しない
  • ソフトウェアの開発・品質向上に関わる人達にとって:
    • 調査方法を知らない・開発者レベルの知識が無い人に調査の仕方を説明するために、時間と手間を使う必要がある。
    • 有効な情報を得られないと、スッキリしないものを抱えたままになる。

一方、もし理想的に推移したとすると、以下のようになるでしょう。

  • トラブルに遭遇した本人や、同様の問題に躓いた人達にとって:
    • 原因が特定され、次のバージョンで修正される事になれば、そのバージョンのリリースを待てば問題は解決する。
    • しかし、今使っているバージョンでは問題は解決しない
    • 調査の過程で暫定的な解決策がたまたま見つからなければ、今この瞬間のトラブルは解決しない
  • ソフトウェアの開発・品質向上に関わる人達にとって:
    • 問題の原因を特定できれば、根本的解決や回避策を検討できる。
    • その成果を次のバージョンに盛り込めれば、同様のトラブルの発生を防げる。
    • 根本的な原因がそのソフトウェアに無いとしても、より適切な場所へ、開発者レベルでの詳細な調査結果を報告できる。

いずれの場合も、今この瞬間に起こっている問題の解決には必ずしもつながらないし、また、その優先順位も低い、という事には注意が必要です。

イシュートラッカーに関わる人には、ある程度の知識が要求されます。報告者に全く知識が無いという場合、問題自体の深刻度がよほど高いと判断されれば、開発者もなんとか原因究明に必要な情報を報告者から引き出そうとして手厚く手助けする事もあります。しかし、それだけの手間をかけるのは割に合わないとなれば、残念ながらそれ以上の手助けはなされません。

このような事になるのは、OSSプロジェクトのイシュートラッカーは「個人の問題を解決する場所」ではなく、あくまで「ソフトウェアの開発を進め品質を向上する事を目的とする場所」だからです。

ソフトウェアの改善・ソフトウェアの問題の解決に関わりたいならイシュートラッカーへ

例えて言えば、これは企業の社屋や工場に誰でも出入り自由になっている状況のようなものです。社会科見学では見学しかできないのに対し、見学ルートを外れて「中の人達」同士の議論に混ざって発言できる、参加の機会が開かれているのがOSSです。そう考えれば、そこで行われている議論や開発に誰でも気軽についていける訳ではなく、機会があるかどうかとその中で一人前の参加者として振る舞えるかどうかは別の話だ、という事も納得しやすいでしょう。

しかし、これは言い換えれば、開発に関わりたい・品質向上に寄与したいという積極的な意志を持つ人にとっての門戸が開かれているという事です。開発の拠点が海外でも、普段の仕事がソフトウェア開発と無関係でも*4、あるいはライバル製品の開発元に所属していてすらも*5、やる気次第で自分から参入できます。

  • 「ここの所に穴が開いてるみたいなんだけど、大丈夫かなあ……塞がれないのかなあ……? 気になる……」「この機能のこの部分、これだと不十分に見えるんだけどなあ……ソワソワする……」という風に、ソフトウェアのちょっとした不具合が気になって仕方ない人。
  • 「ああもう! この人達(開発者)はまるで分かってない! 俺の方がずっとスマートにこの問題を解決できるのに!」「ああもう! なんでここの所はこんな風にできてるんだ? 使いにくい! 俺ならこんな作り方にしないのに!」という風に、問題が放置されている事に我慢がならない人*6

こういったモヤモヤを感じた時に、「でも、手を出せないんだからしょうがない」と諦めないで根本的解決できる可能性が開かれているのがOSSなのです。

ですから、「OSS開発に関わってみたい、ソフトウェアの改善に関わりたい、その最初の一歩を踏み出してみたい」と思ってOSS Gateワークショップに参加を考えた人であれば、もうその時点で「イシュートラッカーに報告する」という選択をして全く問題ない訳です。

また、その際には最初から理想的に振る舞える必要はありません。OSSプロジェクトに初めて関わる人というのは会社の新入社員と同じですから、当然要領は良くありませんし、最初は簡単な仕事からしかこなせませんし、あるいはミスもします。分からない事があれば、会社の先輩に質問するのと同じように、先に参加していた人を捕まえて質問するだけの事です。

「フォーラムに書くべきか、イシュートラッカーに書くべきか」「こんな些細な問題を報告してもいいのか」という問いへの筆者の答えが「個人の問題でないならイシュートラッカーに書いてよい」「些細でも報告してよい」となるのは、以上のような理由からです。

まとめ

以上、OSS Gateワークショップの中で度々聞かれる「フォーラムとイシュートラッカーのどちらに書くべきか判断できない」という悩みについて、フォーラムとイシュートラッカーの目的を把握した上で、「ソフトウェアの問題を解決する場所」というイシュートラッカーの目的に沿った報告をすればよい、という事を語ってみました。

イシュートラッカーへの報告は、問題が解決されればより多くの人に恩恵がもたらされます*7。OSSのイシュートラッカーはソフトウェア開発の現場ですので、ソフトウェア開発の知識がある人はよりよい報告ができる可能性がありますし、パッチやプルリクエストを通じて直接的に問題解決を図る事もできます。一歩進んだ関わり方として、皆さんもイシュートラッカーへの報告にぜひ挑戦してみて下さい。

ところで、先の事例にはもう1つ気になる点が残っています。それは、Microsoft IMEの問題だった可能性があるのにThunderbirdの問題として報告してしまってよかったのか、自分では適切な判断ができない時は一体どこに報告すればよいのか、という点です。次の記事では、それも含めて「どんな報告の仕方をすればいいか、報告の内容はどう書けばよいか」について語ってみます。

なお、当社ではOSS開発を推進したい企業さまのお手伝いをするOSS開発支援サービスを行っています(直近のアカツキさまでの事例の紹介)。社内のITエンジニアのOSS開発者としての活躍を促進したいとお考えの担当者さまがいらっしゃいましたら、ぜひお問い合わせフォームよりご連絡下さい。

*1 「ソフトウェアの不具合」を「バグ」と呼ぶことから、バグの修正状況の追跡をする物として「バグトラッキングシステム(Bug Tracking System、BTS)」という呼び方が以前はよく使われていました。しかし、この呼び方だと「不具合ではないただの要望はバグとして取り扱うべきなのかどうなのか?」という点で混乱が生じる場合があります。Mozillaが運用するバグトラッキングシステム「Bugzilla」では、不具合報告だけでなく要望もすべてひっくるめて「バグ」と呼ぶという運用ルールにする事でこの混乱を回避していますが、運用ルールを知らない人にとっては違和感が残ります。そのため現在は一般的には、バグや要望などを「解決されるべき課題=issue」と捉えて「イシュートラッキングシステム(Issue Tracking System、ITS)」や「イシュートラッカー」と呼んだり、あるいは、どんな課題であってもシステム上で管理される時はひとつの「チケット」になるからという事で「チケットトラッキングシステム」と呼んだりします。本稿では、ひとまず「イシュートラッカー」で統一しています。

*2 またその過程で、`intl.tsf.enable`という設定の値を`false`に変更すると現象が解消されるという、別の暫定的解決策も紹介されていました。これはWindows XP以降で標準となっている文字入力の仕組みを使うかどうかを制御する設定で、`false`に設定すると従来の文字入力の仕組みを使うようになります。音声からの文字入力などTSFで初めて可能になった事はできなくなりますが、その状態でも、キーボードからの一般的な文字入力操作に関しては特に支障なく行えます。

*3 これに対し、当社の法人向け有償サポートサービスは、ソースコードレベルの調査を実施したり、一定の期限内で回答したり、という事をお約束する事をもって対価を頂いているという事になります。

*4 ソフトウェア開発ではなく運用に従事しているという人や、まだ就職していなくて学生で開発に参加する人もいます。

*5 実際に、Mozillaに雇用されている人達もChromiumにバグ報告をしています。

*6 「プログラマーの三大美徳」の1つの「傲慢」とは、このような事(自分にはこの問題を解決できるはず、という考え方をすること)を指します。

*7 当社の法人向けサポートサービスは、お客様の問題(個別の問題)について原因や回避方法を調査してその解決を図ると同時に、その問題がソフトウェアの問題であった場合はソフトウェアの品質向上のためアップストリームへのフィードバックも行う事を心がけています。

2019-06-18

Thunderbirdアドオン「CardBook(連絡先)」でローカルに保存されるデータの暗号化に対応しました

CardBookと企業利用

皆さんはCardDAVという仕様をご存じでしょうか? CardDAVはWebDAVのプロトコルを使ってLDIF形式のアドレス帳をやり取りするという物で、これを用いると「読み書き両方を行えて、内容が複数PC間で同期される」という種類のリモートアドレス帳を汎用の物として実現することができます。CardDAVサーバーとして振る舞える製品にはownCloudやDAViCalなどがあり、読み取り専用に設定したリモートアドレス帳を複数人で共有するという事もできますので、企業利用では重宝する場面がありそうです。

このように便利なCardDAVですが、残念ながらThunderbirdは本体の機能としては対応していません。CardDAVベースのリモートアドレス帳を使うにはアドオンをインストールする必要があります。CardBookは、そのようなCardDAV対応のためのアドオンの一例です。

ところで、企業によっては個人情報の取り扱い方について、「顧客や取引先の個人情報をローカルに保存する際は必ず暗号化する」といったプライバシーポリシーを定めている場合があります。前出のCardBookはリモートアドレス帳のデータをIndexedDBを使用してローカルにキャッシュする設計で、この時のデータは暗号化されないため、そのままでは前述のポリシーに抵触するので採用できないという事になります。

そのような背景から、「CardBookでローカルに保存されるデータを暗号化したい」というご相談を頂き、成果を開発元に還元する前提で先行して作業を進めていたのですが、残念ながら受注には至らず、手元には実現可能性の調査のために行った試験的な実装が残るという結果になりました。しかしせっかく実装した物をそのまま放置しておくのも勿体なかったので、CardBookプロジェクトに還元したところ、標準機能の1つとして取り込まれるに至りました。現在リリース済みのCardBook 33.9以降のバージョンでは、設定画面でチェックボックスをONにすればローカルデータの暗号化が有効になるようになっています。

以下、CardBookのローカルデータベースの暗号化を実現するにあたって行った具体的な内容をご紹介します。

IndexedDBに格納するデータの暗号化と復号

幸い、Thunderbirdの基盤であるGeckoには、暗号化のための汎用APIであるWeb Crypto APIが実装されています。あるのなら使わない理由はありませんので、CardBookでもデータの暗号化はWeb Crypto APIによる共通鍵暗号で行う事にしました。何故公開鍵暗号ではなく共通鍵暗号なのかについては別項で詳しく述べていますので、そちらも併せてご覧下さい。

実装は、まず暗号化・復号を行う専用のモジュールを追加した上で、IndexedDBの読み書きを行うモジュールの書き込み用のデータを用意する箇所に暗号化処理を読み込んだデータを検証する箇所に復号処理を仕掛けることで、他のモジュールに影響を与えず透過的に動作するような組み込み方としました。

この時気をつけなくてはならないポイントとして、暗号化をどのタイミングで行うかという点が挙げられます。以下、暗号化を行っている実際の箇所を抜粋しながら説明します。

元々の設計では、IndexedDBへのデータ書き込みは以下の要領で行われていました。

addCard: unction (aDirPrefName, aCard, aMode) {
  var db = cardbookRepository.cardbookDatabase.db;
  // トランザクション開始
  var transaction = db.transaction(["cards"], "readwrite");
  var store = transaction.objectStore("cards");
  var storedCard = aCard;
  // データの書き込み
  var cursorRequest = store.put(storedCard);

  // 以下、成功時・エラー時の処理
}

ここに暗号化処理を組み込むのですが、Web Crypto APIは暗号化したデータがPromiseで返されるため、値を使うには.then()のコールバック関数で受け取るか、awaitで値の解決を待つ必要があります。コールバック関数を使うスタイルで実装するにはこのメソッドの書き方を大幅に変えなくてはなりませんが、asyncキーワードとawaitを使うと、この同期処理の関数を容易に非同期処理に対応させることができます。

addCard: async function (aDirPrefName, aCard, aMode) { // asyncキーワードを追加
  var db = cardbookRepository.cardbookDatabase.db;
  // トランザクション開始
  var transaction = db.transaction(["cards"], "readwrite");
  var store = transaction.objectStore("cards");
  // 暗号化処理を追加
  var storedCard = cardbookIndexedDB.encryptionEnabled ? (await cardbookEncryptor.encryptCard(aCard)) : aCard;
  // データの書き込み
  var cursorRequest = store.put(storedCard);

  // 以下、成功時・エラー時の処理
}

当初はこの例のように、書き込みを行う直前で暗号化を行うようにしていました。しかし実際に動作させてみると、これではIndexedDBでのデータ書き込みに失敗するという結果になりました。何故でしょうか?

実は、IndexedDBでのデータ書き込みはトランザクション開始から書き込みまでを同期的に(同じイベントループ内で)行う必要があります。この例ではトランザクション開始後にawaitを使ってしまっているせいで、store.put(storedCard)が次のイベントループでの実行となってしまい、そのせいで書き込みに失敗してしまうという訳です。

そのため最終的な実装では、以下の例のようにトランザクション開始前に暗号化を終えておくようにしました。

addCard: async function (aDirPrefName, aCard, aMode) {
  // 暗号化
  var storedCard = cardbookIndexedDB.encryptionEnabled ? (await cardbookEncryptor.encryptCard(aCard)) : aCard;
  var db = cardbookRepository.cardbookDatabase.db;
  // トランザクション開始
  var transaction = db.transaction(["cards"], "readwrite");
  var store = transaction.objectStore("cards");
  // データの書き込み
  var cursorRequest = store.put(storedCard);

  // 以下、成功時・エラー時の処理
}

これなら、トランザクション開始から書き込みまでが同期的に行われるため問題ありません。

復号時には、特にこのような注意は必要ありません。また、元々IndexedDBからのデータ読み取りは結果が非同期で返されるので、CardBookのデータ読み込み処理もその前提で設計されていました。そのため、IndexedDBから返ってきたデータを非同期で復号した上で返却するという処理を挟み込んでも、CardBookのデータ読み込み処理全体としてはインターフェースを変えずに済んだのでした。

パスワード入力を求める方式にしなかった理由

CardBookのローカルデータ暗号化では、暗号化・復号に使う共通鍵は、バックグラウンドで自動生成した物を暗黙的に使い、鍵自体をユーザープロファイル内に保存する形としました。

「暗号化されたデータと鍵を同じ場所に置いておくのでは、暗号化の意味が無いじゃないか」と思うでしょうか? 実際、変更をフィードバックした際にもCardBookプロジェクトの開発者の方からも「パスワード入力を求める方式にした方がいいのではないか?」という質問がありました。Web Crypto APIの機能を使うとユーザーが入力したパスワードから秘密鍵を作る事もできる(Web Crypto APIの解説記事の「パスワードを鍵に変換する」の項をご参照下さい)のに、そうしなかったのは何故でしょうか。

ここで一旦、パスワードの安全な運用という事を考えてみましょう。パスワードを自分で記憶しておきその都度入力するという方式は、一見すると安全なように思えます。しかしながら、運用の仕方によっては却って危険になる場合があります。

  • パスワード入力には、肩越しに入力の様子を覗き見るショルダーハックや、キーの入力を監視するキーロガーなどによってパスワードを盗み取られるリスクがあります。パスワード入力の頻度が多ければ多いほど、このリスクは高まります。
  • 人間の記憶容量には限りがあるため、あまり複雑なパスワードを複数覚えるという事はできません。そのために「同じパスワードを何度も使い回す」「推測が容易なパスワードにする」といった事が行われてしまい、そうなると却って危険な状態となります。
  • 定期的なパスワード変更にも、同様の問題があります

これらの理由から、パスワードの入力は「複雑で憶えにくいパスワードを1つだけ覚える」「それをマスターパスワードとして使い、それ以外はパスワードマネージャに憶えさせる」という運用にするのが比較的安全だというのが現在の定説となっています*1

「企業でThunderbirdを使う」というシチュエーションでは、「PCのログオン」「受信メールサーバーの認証」「送信メールサーバーの認証」などでそれぞれパスワードの入力が発生する可能性があります。という事は、ここにさらに「ローカルデータの復号」のためのパスワードが加わるというのは、さすがに実運用を妨げるレベルの煩わしさでしょう。かといって、他の部分ではパスワードを使用していないのにここでだけパスワードの入力を求める、というアンバランスな運用も考えにくいです。そういった事を考慮した結果として、CardBookのローカルデータ暗号化は現在比較的安全とされている運用を想定し、

  • 秘密鍵は、自動生成した物をJWK形式でエクスポートし、「長いパスワード文字列」の一種としてThunderbirdのパスワードマネージャに記憶させる。
  • 秘密鍵の保護が必要な場合は、使用者が任意でThunderbird本体のマスターパスワード機能を有効化する。

というポリシーを採用する事にしたのでした。

まとめ

Thunderbird用アドオンのCardBookに対して行った、ローカルデータの暗号化対応の概要をご紹介しました。

当社では、一般に公開されているFirefox用アドオン・Thunderbird用アドオンをはじめとした様々なフリーソフトウェア・OSSについて機能追加・改造のご依頼を承っております。また、成果をアップストリームに還元しても差し支えがないケースでは、積極的に還元を行うようにしています。自社でフリーソフトウェア・OSSを採用したいが少しだけ要件に合わない、という事でお悩みの場合には、メールフォームからお問い合わせ下さい。

*1 ただし、これはあくまで現時点での話です。技術の進歩や、この分野での研究が進む事などによって、「最もマシ」なやり方は変わっていく可能性があります。

2019-05-28

回帰テストの対象は適切に設定しよう

先日、Bug 1541748 - New tab and restored tab notified via tabs.onCreated can have invalid (too large) index という報告をFirefoxのバグトラッキングシステムに行った結果、提出したパッチがFirefox 68に反映される事になりました。

とはいえ、実はこのパッチで追加されるコードは皆さんのお手元にインストールされるFirefoxには反映されません。何故なら、これは自動テストを追加するだけのパッチだからです。

パッチが投入されるまでの経緯

この報告は、「Firefoxのアドオン開発に使用するWebExtensionsのいくつかのAPIにおいて、タブの位置を示すindexというプロパティが不正な値になる場合がある」という物でした。ただ、これは実はWebExtensions APIの実装の問題ではなく、Firefoxのタブが内部的に持っている対応する情報自体が壊れていたというのが真相でした。

(図:壊れた内部状態が露出した事が原因で、APIの情報が壊れている様子)

「根本的な原因」の方は Bug 1504775 - Index is wrong for restored tabs という別のBugで取り扱われていました。今回の報告と前後してそちらのBugの作業が進行していたため、そちらの方で修正が行われた事の影響として、報告からあまり間を置かずにWebExtensions APIの不具合も自然解消したという状況になっていました。

一般的に、このような「ある報告の原因が別の報告の修正で解消された」というようなケースでは、その旨を記して修正済み扱いとしたり、もしくは重複する報告であるとして処理する事が多いです。

(図:2つのBugに因果関係がある時、両方が閉じられる様子)

このような場合、そちら側に提出されたパッチはBugのクローズに伴って破棄されがちです。しかし今回はそうではなく、Bugはクローズされずに留め置かれ、新たに自動テストを加えるだけのパッチを追加投入する事が承認されました。

これは、根本原因の方のBugがFirefox内部の問題のみを取り扱う物であったのに対し、こちらで報告したBugはアドオンという外部アプリケーションに対する互換性の問題を取り扱っていたからです。

テストは目的ごとに必要

根本原因が修正されたパッチでは、Firefox内部のAPIに対するテストケースは追加されていますが、WebExtensionsのテストケースには特に変更は加えられていませんでした。

今回はFirefoxがたまたま「タブの内部的なindexがそのままWebExtensions APIを通じてアドオンに露出する」という実装を取っていたために、WebExtensions API側の問題も偶然解消されました。しかし、それはあくまで現時点の実装がそうであったからというだけで、将来に渡ってそうであるという事の保証はどこにもありません

(図:内部向けの保証はあってもアドオン相手の保証は無いという様子)

よって、今後Firefox内部の実装やWebExtensions APIの実装が変更された場合に、人知れず再び同じ問題が起こるようになるという可能性は依然としてあります。

そのため、今回のパッチでWebExtensions APIのレイヤにおいて後退バグ*1が発生していないかを検出するための回帰テスト*2を追加することで、WebExtensions APIに意図せず影響を与えてしまう(そのような変更が気付かれずに投入されてしまう)という事故の発生を未然に防ぎ、APIの安定性を保証しやすくしたという事になります。

(図:保証する相手ごとにテストがある様子)

気をつけて欲しいのは、これは「単体テストと結合テストではレイヤが違うので、単体テストで検証済みの事もすべて結合テストで再検証しよう」という話ではない、という事です。同じ事を保証するテストがそれぞれの実装レイヤに含まれるという事はあり得ますが、それはあくまで結果的にそうなっているだけに過ぎません。テストは誰に対して何を保証するか、という観点で整備される事が重要です。

今回は「Firefox内部向け」と「アドオンという外部アプリケーション向け」のそれぞれで修正・互換性を保証する必要があったためにこうなったわけです。

まとめ

Firefoxに投入された自動テストのみのパッチの背景説明を通じて、テストの目的とテストを追加するかどうかの判断基準の一例をご紹介しました。

自動テストの追加は、ドキュメントの修正に次いで「外部のコントリビューターとして開発に参加する最初のステップ」として取り組みやすい作業です。自動テストが全く存在しないプロダクトや、自動テストがあっても特定の機能についてテストが不足しているというケースに遭遇したら、皆さんもぜひテストの追加に取り組んでみて下さい。独りでやりきるには不安があるという方は、スケジュールが合うOSS Gateのワークショップに参加してサポーターの人に相談してみても良いかもしれません。

*1 今まで期待通りに動いていた物が、別の変更の影響で期待通りに動かなくなってしまう事。

*2 ある問題を修正したときに、問題が発生しなくなっている事を検証するテストのこと。

2019-04-23

FirefoxやChromiumのアドオン(拡張機能)で効率よくタブを並べ替えるには

この記事はQiitaとのクロスポストです。

Firefox 57以降のバージョンや、Google Chromeをはじめ各種のChromiumベースのブラウザでは、アドオン開発に共通のAPIセットを使用します。タブバー上のタブを任意の位置に移動するAPIとしてはtabs.move()という機能があり、これを使うと、タブを何らかのルールに基づいて並べ替えるという事もできます。実際に、以下のような実装例もあります。

このようなタブの並べ替え機能を実現する時に気をつけたいポイントとして、 いかにAPIの呼び出し回数を減らし、効率よく、且つ副作用が少なくなるようにするか? という点があります。

ソートアルゴリズムの効率とは別の話

「並べ替え」「効率」というキーワードでソートアルゴリズムの事を思い浮かべる人もいるのではないでしょうか。確かにそれと似ているのですが、実際にはこの話のポイントはそこにはありません。

確かに、ソート自体は前述の2つのアドオンでも行っており、

  • バラバラの順番のタブをコンテナごとに整理する
  • URLなどの条件でタブの順番を整理する

という処理はまさにソートそのものです。しかし、現代のJavaScript実行エンジンは充分に高速なため、余程まずいアルゴリズムでもない限りJavaScriptで実装されたソートの速度自体は実用上の問題となりにくいです。

それよりも、その後に行う 「実際のタブを移動して、算出された通りの順番に並べ替える」 という処理の方が影響度としては深刻です。「タブ」というUI要素を画面上で移動するのは、純粋なデータのソートに比べると遙かにコストが大きいため、数百・数千個といったオーダーのタブをすべて並べ替えるとなると相応の時間を要します。また、タブが移動されたという事はイベントとして他のアドオンにも通知され、その情報に基づいて様々な処理が行われ得ます*1ので、そのコストも馬鹿になりません*2

タブの並べ替えには手間がかかる

実際の並べ替えの様子を図で見てみましょう。[i, h, c, d, e, f, g, b, a, j] という順番のタブを、[a, b, c, d, e, f, g, h, i, j] に並べ替えるとします。

(図:並べ替え前後)

ぱっと見で気付くと思いますが、これは a, b, h, i の4つを左右で入れ換えただけの物です。しかし、人間にとっては一目瞭然の事でも、プログラムにとっては自明な事ではありません(それを検出するのがまさにこの記事の本題なのですが、詳しくは後述します)。

最もナイーブな*3実装としては、元の並び順からタブを1つづつ取り出していくやり方が考えられます。その場合、行われる操作は以下のようになります。

(図:ナイーブな並べ替えの流れ。計8回の移動が生じている様子。)

というわけで、この場合だと10個のタブの並べ替えに8回の移動が必要となりました。元のタブの並び順次第では、移動は最大で9回必要になります。

ちなみに、tabs.move()は第一引数にタブのidの配列を受け取ることができ、その場合、第一引数での並び順の通りにタブを並べ替えた上で指定の位置に移動するという挙動になります。そのため、

browser.tabs.move([a, b, c, d, e, f, g, h, i, j], { index: 0 });

とすれば、APIの呼び出し回数としては確かに1回だけで済ませる事もできます。ただ、これは内部的には

browser.tabs.move(a, { index: 0 });
browser.tabs.move(b, { index: 0 + 1 });
browser.tabs.move(c, { index: 0 + 2 });
browser.tabs.move(d, { index: 0 + 3 });
browser.tabs.move(e, { index: 0 + 4 });
browser.tabs.move(f, { index: 0 + 5 });
browser.tabs.move(g, { index: 0 + 6 });
browser.tabs.move(h, { index: 0 + 7 });
browser.tabs.move(i, { index: 0 + 8 });
browser.tabs.move(j, { index: 0 + 9 });

といった移動操作と同等に扱われるため、タブが実際に移動されるコストは変わらず、タブが移動された事を通知するイベントもその都度通知されます。

ソートと同時にタブを移動するのではどうか?

ここまではArray.prototype.sort()でソートした結果に従って、後からまとめてタブを並べ替えるという前提で説明していました。では、Array.prototype.sort()を使わずに独自にソートを実装し、その並べ替えの過程で同時にタブも移動する、という形にするのはどうでしょうか?

例えばこちらのクイックソートの実装の過程にtabs.move()を呼ぶ処理を追加すると、以下のようになります。

// 再帰によるクイックソートの実装
function quickSort(tabIds, startIndex, endIndex) {
  const pivot = tabIds[Math.floor((startIndex + endIndex) / 2)];
  let left = startIndex;
  let right = endIndex;
  while (true) {
    while (tabIds[left] < pivot) {
      left++;
    }
    while (pivot < tabIds[right]) {
      right--;
    }
    if (right <= left)
      break;
    // 要素の移動時に、同じ移動操作をタブに対しても行う
    browser.tabs.move(tabIds[left], { index: right });
    browser.tabs.move(tabIds[right], { index: left });
    const tmp = tabIds[left];
    tabIds[left] = tabIds[right];
    tabIds[right] = tmp;
    left++;
    right--;
  }
  if (startIndex < left - 1) {
    quickSort(tabIds, startIndex, left - 1);
  }
  if (right + 1 < endIndex ){
    quickSort(tabIds, right + 1, endIndex);
  }
}
const tabs = await browser.tabs.query({windowId: 1 });
quickSort(tabs.map(tab => tab.id), 0, tabIds.length - 1);

先の [i, h, c, d, e, f, g, b, a, j] を [a, b, c, d, e, f, g, h, i, j] に並べ替えるという例で見てみると、

(図:クイックソートでの並べ替えの流れ。計4回の移動が生じている様子。)

このように、今度は4回の移動だけで並べ替えが完了しました。先の場合に比べると2倍の高速化なので、これは有望そうに見えます。

……が、そう考えるのは早計です。この例で並べ替えが4回で終わったのは、部分配列の左右端から要素を見ていくというアルゴリズムが、左右の端に近い要素が入れ替わる形になっていた [i, h, c, d, e, f, g, b, a, j] という元の並び順に対して非常にうまく作用したからに過ぎません。例えば入れ替わった要素の位置が左右の端から遠い位置にある [c, d, i, h, e, b, a, f, g, j] のようなケースでは、

(図:クイックソートでの並べ替えの流れ。計12回の移動が生じている様子。)

と、今度は12回も移動が発生してしまいます。クイックソートは場合によっては同じ要素を何度も移動する事になるため、タブの移動のコストが大きい事が問題となっている今回のような場面では、デメリットの方が大きくなり得るのです。

また、クイックソートには「ソート結果が安定でない」という性質もあります。各要素のソートに使う値がすべて異なっていれば並べ替え結果は安定しますが、Firefox Multi-Account Containersでの「タブのコンテナごとに整理する」というような場面では、同じグループの要素の順番がソートの度に入れ替わってしまうという事になります。このような場合は要素間の比較の仕方を工夫するか、別のソートアルゴリズムを使う必要があります。

どうやら、このアプローチでは一般的に効率よくタブを並べ替えるという事は難しいようです。一体どうすれば「人が目視でするように、可能な限り少ない移動回数で、タブを目的の順番に並べ替える」という事ができるのでしょうか。

diffのアルゴリズムを応用する

このような問題を解決するには、diff編集距離といった考え方が鍵となります。

Unix系の環境に古くからあるdiffというコマンドを使うと、2つの入力の差を表示する事ができます。Gitなどのバージョン管理システムにおいてもgit diffのような形で間接的に機能を利用できるようになっているため、各コミットで行った変更点を見るために使った事がある人は多いでしょう。例えば、A = [a, b, c, d] と B = [a, c, d, b] という2つの配列があったとして、AとBの差分はdiffでは以下のように表されます。

  a
- b
  c
  d
+ b

行頭に-が付いている行は削除(AにはあってBには無い)、+が付いている行は追加(Aには無くてBにはある)を意味していて、これを見れば、「Aのbを2番目から4番目に移動する」というたった1回の操作だけでAをBにできるという事が見て取れます。このような「最小の変更手順」を求めるのが、いわゆる「diffのアルゴリズム」です。

つまり、これを「タブの並べ替え」に当てはめて、 「現時点でのタブの並び順」と「期待される並び順」とを比較し、算出された差分の情報に則ってタブを実際に移動するようにすれば、最小の手間でタブを並べ替えられる というわけです。

JavaScriptでのdiffの実装

diffの実装というとdiffコマンドが真っ先に思い浮かぶ所ですが、diffをライブラリとして実装した物は各言語に存在しており、もちろんJavaScriptの実装もあります。以下はその一例です。

diffのアルゴリズムをタブの並べ替えに使うには、こういったライブラリをプロダクトに組み込むのがよいでしょう。ただ、その際には以下の点に気をつける必要があります。

  • ライブラリのライセンスが他の部分のライセンスと競合しない事。
  • ライブラリの内部表現を機能として利用できる事。
  • 入力として任意の要素の配列を受け受ける事。

ライセンスの事は当然として、内部表現が利用できるとはどういう事でしょうか。

前述の例の「行頭に-+が付く」という表現は、git diffなどで目にする事の多い「Unified Diff」形式に見られる表記ですが、これはあくまで最終出力です。ライブラリ内部では、相当する情報を構造化された形式で持っている事が多いです。npmのdiffであるjsdiffの場合は以下の要領です。

[
  { count: 1, value: [a] },                // 共通の箇所
  { count: 1, value: [b], removed: true }, // 削除された箇所
  { count: 2, value: [c, d] },             // 共通の箇所
  { count: 1, value: [b], added: true }    // 追加された箇所
]

先のテキストでの表現をパースしてこのような表現を組み立てる事もできますが、ライブラリの内部表現→出力結果のテキスト表現→内部表現風の表現 という無駄な変換をするよりは、素直に内部表現をそのまま使えた方が話が早いです。

また、ライブラリの入力形式には任意の配列を受け付ける物である方が望ましいです。文字列のみを入力として受け付けるライブラリでも使えなくはないのですが、内部ではどの実装も基本的に配列を使っています。こちらも、比較したい配列→ライブラリに与えるための文字列→内部表現の配列 という無駄な変換が必要の無い物を使いたい所です。

npmのdiffパッケージであるjsdiffは、これらの点で使い勝手が良いのでおすすめの実装と言えます。

diffのアルゴリズムを使ったタブの並べ替え

diffを使ってタブを並べ替えるには、「ソート前の配列」「ソート後の配列」に「作業中の状態を保持する配列」を加えた3つの配列を使います。

const tabs = await borwser.tabs.query({ currentWindow: true });

const beforeIds = tabs.map(tab => tab.id); // ソート前のタブのidの配列
const afterIds  = tabs.sort(comparator)
                      .map(tab => tab.id); // ソート後のタブのidの配列
let   currentIds = beforeIds.slice(0);     // 現在の状態を保持する配列(ソート前の状態で初期化)

次に、ソート前後のタブのidの配列同士の差分を取り、その結果に従ってタブを移動していきます。

const differences = diff.diffArrays(beforeIds, afterIds);
for (const difference of differences) {
  if (!difference.added) // 削除または共通の部分は無視する
    continue;
  // ここにタブの移動処理を書いていく
}

タブの移動による並べ替えという文脈では、「削除」の差分で操作されるタブは対応する「追加」の操作によっても操作されます。また、「なくなるタブ(=処理中に閉じなければいけないタブ)」は原則として存在しません。よって、差分情報のうち「共通」と「削除」は無視して、「追加」にあたる物のみを使用します。

「追加」の差分情報を検出したら、まずタブの移動先位置を計算します。tabs.move()はタブの位置をindexで指定する必要がありますが、この時のindexは以下の要領で求められます。

  // 以下、forループ内の処理

  // 移動するタブ(1個以上)
  const movingIds = difference.value;
  // 移動するタブ群の中の右端のタブを得る
  const lastMovingId = movingIds[movingIds.length - 1];
  // ソート後の配列の中で、移動するタブ群の右に来るタブの位置を得る
  const nearestFollowingIndex = afterIds.indexOf(lastMovingId) + 1;
  // 移動するタブ群がソート後の配列の最後に来るのでないなら、
  // 移動するタブ群の右に来るタブのindexを、タブ群の最初のタブの移動先位置とする
  let newIndex = nearestFollowingIndex < afterIds.length ? currentIds.indexOf(afterIds[nearestFollowingIndex]) : -1;
  if (newIndex < 0)
    newIndex = beforeIds.length;

  // 移動するタブ群の左端のタブの現在のindexを調べる
  const oldIndex = currentIds.indexOf(movingIds[0]);
  // 現在の位置よりも右に移動する場合、移動先の位置を補正する
  if (oldIndex < newIndex)
    newIndex--;

タブ群の移動先の位置を求めたら、いよいよtabs.move()でタブを移動します。このAPIは第1引数にタブのidの配列を渡すと複数のタブを一度に指定位置に移動できるので、APIの呼び出し回数を減らすためにもそのようにします。

  // 実際にタブを移動する
  browser.tabs.move(movingIds, {
    index: newIndex
  });
  // 現在の状態を保持する配列を、タブの移動後として期待される状態に合わせる
  currentIds = currentIds.filter(id => !movingIds.includes(id));
  currentIds.splice(newIndex, 0, ...movingIds);

タブの移動はAPIを介して非同期に行われますが、タブの移動指示そのものはまとめて一度にやってしまって構いません。むしろ、途中でtabs.move()の完了をawaitで待ったりすると、タブの並べ替えの最中に外部要因でタブが移動されてしまうリスクが増えるので、ここはバッチ的に同期処理で一気にやってしまうのが得策です。

ただしその際は、2回目以降のtabs.move()に指定するタブの移動後の位置として、前のtabs.move()の呼び出しで並べ替えが完了した後の位置を指定しなくてはならないという点に注意が必要です。そのため、ソート前後の配列とは別に「並べ替えが実施された後の状態」を保持する配列を別途用意しておき、それだけを更新しています。前述のコード中で「タブの現在の位置」を求める際に、tabs.Tabindexを参照せずにこの配列内での要素の位置を使用していたのは、これが理由なのでした。

実際の並べ替えの様子を、最初と同じ例を使って見てみましょう。[i, h, c, d, e, f, g, b, a, j] を [a, b, c, d, e, f, g, h, i, j] に並べ替えるとします。初期状態は以下のようになります。

  • beforeIds: [i, h, c, d, e, f, g, b, a, j]
  • afterIds: [a, b, c, d, e, f, g, h, i, j]
  • currentIds: [i, h, c, d, e, f, g, b, a, j]
  • 実際のタブ: [i, h, c, d, e, f, g, b, a, j]

この時、beforeIdsafterIdsを比較して得られる差分情報は以下の通りです。

// 実際にはvalueはタブのidの配列だが、分かりやすさのためにここではアルファベットで示す
[
  { count: 2, value: [i, h], removed: true },
  { count: 2, value: [a, b], added: true },
  { count: 5, value: [c, d, e, f, g] },
  { count: 2, value: [b, a], removed: true },
  { count: 2, value: [h, i], added: true },
  { count: 1, value: [j] }
]

前述した通り、ここで実際に使われる差分情報は「追加」にあたる以下の2つだけです。

  1. { count: 2, value: [a, b], added: true }
  2. { count: 2, value: [h, i], added: true }

まず1つ目の差分情報に基づき、aとbを移動します。移動先の位置は、bの右隣に来る事になるタブ(=c)のcurrentIds内での位置ということで、2になります。

(図:diffを使った並べ替えの流れ。まず2つのタブを移動する。)

この時、b, aだった並び順も併せてa, bに並べ替えています。タブの移動完了後は、それに合わせる形でcurrentIdsも [i, h, a, b, c, d, e, f, g, j] へと更新します。

次は2つ目の差分情報に基づき、hとiを移動します。移動先の位置は、iの右隣に来る事になるタブ(=j)のcurrentIds内での位置から求めます。jの位置は9ですが、これはhの現在位置より右なので、そこから1減らした8が最終的な移動先位置になります。

(図:diffを使った並べ替えの流れ。次も2つのタブを移動する。)

この時も、i, hだった並び順をh, iに並べ替えています。タブの移動完了後は、それに合わせる形でcurrentIdsも [a, b, c, d, e, f, g, h, i, j] へと更新します。

ということで、APIの呼び出しとしては2回、タブの移動回数としては4回で並べ替えが完了しました。冒頭の例の8回に比べるとタブの移動回数は半分で済んでいます。

クイックソートでは却って移動回数が増えてしまった、[c, d, i, h, e, b, a, f, g, j] からの並べ替えではどうでしょうか。この場合、比較結果は以下のようになります。

[
  { count: 2, value: [a, b], added: true },
  { count: 2, value: [c, d] },
  { count: 2, value: [i, h], removed: true },
  { count: 2, value: [e] },
  { count: 2, value: [b, a], removed: true },
  { count: 2, value: [f, g] },
  { count: 2, value: [h, i], added: true },
  { count: 1, value: [j] }
]

前述の通り、このうち実際に使われる差分情報は以下の2つだけです。

  1. { count: 2, value: [a, b], added: true }
  2. { count: 2, value: [h, i], added: true }

これは先の例と全く同じなので、APIの呼び出し回数は2回、実際のタブの移動は4回で済みます。

(図:diffを使った並べ替えの流れ。タブを4回移動している様子。)

これらの例から、diffのアルゴリズムを用いると安定して少ない移動回数でタブを並べ替えられるという事が分かります。diffのアルゴリズムを用いた差分の計算自体はタブの移動に比べれば一瞬で済みますので、タブの移動回数を少なく抑えられれば充分に元が取れると言えるでしょう。

まとめ

以上、diffのアルゴリズムを用いてブラウザのタブを効率よく並べる方法について解説しました。

冒頭で例として挙げたタブの並べ替え機能を持つアドオンに対しては、以上の処理を実際に組み込むプルリクエストを既に作成済みです。

これらの変更(およびこの記事)は、「ツリー型タブ」での内部的なタブの並び順に合わせてFirefoxのタブを並べ替える処理が元になっています。ツリー型タブではnpmのdiffとは別のdiff実装を使用していますが、diffの結果として得られる内部表現の形式はよく似ており、タブの並べ替え処理そのものは同じロジックである事が見て取れるはずです。

このように「最小の変更内容を計算して適用する」という考え方は、Reactで広まった仮想DOMという仕組みのベースとなっています。コンピューターが充分に高性能となり、富豪的プログラミングが一般化した現代においては、多少の事は力業で解決してしまって問題無い事が多いです。しかし、ネットワーク越しの処理や、携帯端末などデスクトップPCよりも性能が制限されがちな環境などで、何千・何万回といった単位で実行されるような処理にボトルネックがあると致命的な性能劣化に繋がりかねず、そのような場合には「コストの高い処理の実行頻度を減らしてトータルの性能を向上する」という正攻法での性能改善が依然として有効です。

なお、2者間で「変更箇所を検出する」というのは「共通部分を検出する」という事と表裏一体ですが、そのような計算は最長共通部分列問題と呼ばれ、diffのアルゴリズムの基礎となっています。どのような理屈で共通箇所を検出しているのか知りたい人は解説や論文を参照してみると良いでしょう。筆者は理解を諦めましたが……

*1 例えば、タブをツリー状の外観で管理できるようにするアドオンであるツリー型タブでは、親のタブが移動された場合は子孫のタブもそれに追従して移動されますし、既存のツリーの中にタブが出現した場合や既存のツリーの一部だけが別の場所に移動された場合などにも、ツリー構造に矛盾が生じなくなるようにタブの親子関係を自動的に調整するようになっています。

*2 ブラウザ自体のタブの並べ替えにかかる時間の事よりも、むしろ、こちらの副作用の方が問題だと言っていいかもしれません。

*3 愚直な。

2019-04-19

Firefox 67 以降でPolicy Engineによるポリシー設定でPOSTメソッドの検索エンジンを追加できるようになります

Firefoxのポリシー設定では、以下の要領でロケーションバーやWeb検索バー用の検索エンジンを登録することができます。

{
  "policies": {
    "SearchEngines": {
      "Add": [
        {
          "Name":               "検索エンジンの表示名",
          "IconURL":            "https://example.com/favicon.ico",
          "Alias":              "ex",
          "Description":        "検索エンジンの説明文",
          "Method":             "GET",
          "URLTemplate":        "https://example.com/search?q={searchTerms}",
          "SuggestURLTemplate": "https://example.com/suggest?q={searchTerms}"
        }
      ]
    }
  }
}

Firefox 66以前のバージョンではこの時、HTTPのメソッドとしてGETのみ使用可能で、POSTメソッドを要求する検索エンジンは登録できないという制限事項がありました。OpenSearchの検索プラグインではPOSTの検索エンジンも登録できたので、これはPolicy Engineだけの制限ということになります。

RESTの原則からいうと「何回検索しても同じ結果が返る事が期待される検索結果一覧ページを表示するためのリクエストはGETが当然」という事になるのですが、実際にはFirefoxのロケーションバーやWeb検索バーは「検索」に限らず、入力された語句を含む任意のHTTPリクエストを送信する汎用のフォームとして使えます。よって、適切な内容の「検索エンジン」を登録しておきさえすれば、FirefoxのUI上から直接「Issueの登録」や「チャットへの発言」を行うといった応用すらも可能となります。しかし、そういった「新規投稿」にあたるリクエストはPOSTメソッドを要求する場合が多いため、それらは残念ながらPolicy Engine経由では登録できませんでした。

この件について 1463680 - Policy: SearchEngines.Add cannot add effective search engine with POST method として報告していたのですが、最近になって「バックエンドとなる検索エンジン管理の仕組みの改良によって、仕組み的にはPOSTメソッドも受け取るようになったので、パッチを書いてみては?」と促されました。そこでさっそく実装してみた所、変更は無事マージされ、Firefox 67以降では以下の書式でPOSTメソッドとそのパラメータを指定できるようになりました。

{
  "policies": {
    "SearchEngines": {
      "Add": [
        {
          "Name":        "検索エンジンの表示名",
          "IconURL":     "https://example.com/favicon.ico",
          "Alias":       "ex",
          "Description": "検索エンジンの説明文",
          "Method":      "POST",
          "URLTemplate": "https://example.com/search",
          "PostData":    "q={searchTerms}"
        }
      ]
    }
  }
}

"Method"の指定を"POST"にする事と、"URLTemplate"ではなく"PostData"の方に{searchTerms}というパラメータを指定する事がポイントです。

今回の変更自体は別のBugでの改良に依存しているため、ESR60に今回の変更がupliftされるためには、依存する変更も併せてupliftされる必要があります。ESR版自体のメジャーアップデートも視野に入りつつあるこの時期ですので、そこまでの手間をかけてこの変更がESR60に反映されるかどうかについては、現状では何とも言えません。とはいえ、次のESRのメジャーアップデート以降で使用可能になる事は確実です。もしこの機能をお使いになりたいというESR版ユーザーの方がいらっしゃいましたら、期待してお待ちいただければと思います。

2019-03-01

タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|