碧落にて気まぐれに一言

2011/05/08

行ごとの編集状態、その3

Filed under: 未分類 — タグ: — Suguru Yamamoto @ 17:05

Azukiの開発が始まって以来おそらく一番難しい、セーブ時点の状態を基準に行ごとに編集状態を管理・表示する機能の実装ですが、あまりにも難しいのであきらめることにしました。

とにもかくにも、テキスト全体が1つのギャップバッファになっているAzukiでは、行単位で何かをする機能は設計しにくい。本件も、おそらく各行を「UNDO/REDO機能を備えたバッファ」として作っておけばアッという間にできてしまうのではないかと想像しています。4/3の投稿ではDocument.Replaceメソッドの老朽化について触れましたが、それ以上に3/13の投稿で書いた基本設計の違いが大きいと今は考えています。頭で考えても紙に書いても思考が追いつかないのです…。

ちなみにVisual Studioのエディタコンポーネントで特定文字の位置を計算する場合、一次元座標(ファイル先頭から文字数で数えたインデックス番号)よりも二次元座標(行番号と列番号で数える)の方が効率が良いと記されています。以下に抜粋。

Although either coordinate system works in the text buffer, it is optimized to use two-dimensional coordinates. A one-dimensional coordinate system can create performance overhead. Therefore, use the two-dimensional coordinate system whenever possible.
MSDN – Text Buffer for the Visual Studio Editor

このことを考えると、おそらくVisual Studioの内部データ構造は行単位の構造になっているのかなと思います。なお私が知っている限り行の編集状態を「未編集・変更あり・保存後未編集」の3つで管理するアプリは知りません。

あきらめが肝心ということで、次のバージョンで昔の仕様に戻します…悔しいですが…。

2011/04/17

「一太郎2011 創」の初使用

Filed under: 未分類 — タグ: , , — Suguru Yamamoto @ 22:47

本日「.NETでドラッグ&ドロップ」という記事を公開しました。

この記事には名前の通り.NET上でドラッグ&ドロップを実装する場合の話を書いてあるのですが、合計4種類のGUIフレームワークを比較しつつドラッグ&ドロップAPIの考え方や使い方を記してあります。まあ、どちらかというと説明というより自分用のメモ書きを、他人でも読めるように整えたものですので不親切な部分も多々あるかもしれません。

私にとっての本記事、もう一つの価値は「一太郎2011 創」を初めて使って書いたことです。モリサワのフォントを使えるということで、一つぐらいは技術記事を新しく書いてPDF形式で公開したかったのです ;)

そんな不純(?)な動機もあって、今回はPDF版が用意されています。PDF版では、モリサワフォントの美しさもお楽しみください :)

(暇があったらヒラギノフォント版も作ってみたいなぁ…)

2011/04/03

行ごとの編集状態、その2

Filed under: 未分類 — タグ: — Suguru Yamamoto @ 23:49

本日Azukiの不具合修正版1.6.2をリリースしました。一応当日のブログなので一言触れておきます :)

ここのところ悩んでいるのは3/13にも本ブログで記した「行ごとの編集状態」です。もともと想定していた範囲を超える仕様であることは良いとしても、「ここまで複雑になるのか」と思うほど複雑です。紙に図をつらつらと書き記しながら設計し、仮実装して検討不足に気づき失敗に終わり。これを何度も繰り返している状態です。うむむ…手強い。

手強くしている原因の一つには、Azukiにおけるすべてのテキスト編集処理を実装しているDocument.Replaceメソッドのコードが老朽化してきていることが挙げられます。このメソッドはAzukiプロジェクトの中で、もっとも古いコードの一つです。2007年の12月に開始したプロジェクトですからもう3歳を超えていますね。ここは基礎の部分なので大きな変更を気軽には入れられず、慎重になった結果、過去にUndo/Redo機能を追加した際も「行ごとの編集状態」表示機能を追加した際も、アドホックな形で修正を加えました(ちなみに今回もアドホックな対応にしようと考えています…再設計は相当な高リスクと考えているので)。そして現在、大きなリファクタリングを伴わない修正が積み重なったためコードが複雑化し、同メソッドの処理内容を擬似コードでも簡単に記せないほど処理が入り組んでいます。擬似コードでサクッと書けるほどシンプルな作りであれば、おそらく何日も前に完成していると思うのですけれども…。

1.6.3で「行ごとの編集状態」の問題は解決したいのですが、最悪の場合は見送りにせざるをえないかもしれません :(

(リファクタリングするのは2.0だろうなぁ…コードフォールディング機能を実装するには大規模な再設計が必要になると思うので、その際にやるしかないか。)

2011/03/13

正規表現の「^」記号とマッチング範囲

Filed under: 未分類 — タグ: , , — Suguru Yamamoto @ 20:06

はじめに

(2011/4/6追記。本件はMicrosoftから将来のリリースで修正するとの回答がありました)
(2011/8/27追記。最新版のVisual Studio 2010+最新の.NET Framework 4で試したところ、まだ修正されていないようです。)

本日、各言語での正規表現エンジンを使って「^」記号(文字列または行の先頭を示す、アンカーあるいはゼロ幅アサーション…と呼ぶらしい)に関する動作を調査しました。背景にあるのはAzukiが内蔵している正規表現検索で行頭マッチングが行われないというユーザ報告の不具合(に近いが対策できず黙認していた動作仕様)です。過去に検索機能を実装した際の調査結果では.NETの正規表現エンジンで「Multilineモード」を有効にしても各行の先頭でない位置でマッチする現象が起こり、使えないと判断しました。今回は、この問題を改めて少し掘り下げて調査した結果報告(?)となります。

調査結果のポイントです。

  1. Microsoftの.NETが提供する正規表現エンジンでは、マッチング範囲の終了位置を指定すると「^」が常に「マッチング範囲の開始点」にマッチしてしまう(終了位置を指定しなければ常にはマッチしない)
  2. MSDNの「^」記号の説明Regex.Matchメソッドの説明からは「^」が文字列の先頭でも行頭でもない箇所にマッチする動作は予想しにくい上に、Regex.Matchのオーバーロード間での動作仕様の統一性が失われている
  3. Java (1.5以降)の正規表現エンジンでは「Anchoring Bounds」という概念で「^」の扱いをカスタマイズできる(→ Matcherクラスのリファレンス)が、.NETでは同等の機構が無い
  4. ユーザ指定の正規表現を使うアプリケーションで、特定のマッチング範囲を絞り、「^」をマッチング範囲の開始点にマッチさせたくない場合、実現できないと思われる

ポイント1および2です。Regex.Match(string,int)のオーバーロードを使ってマッチング開始点だけを指定した場合には、行頭でも文字列先頭でもない位置がマッチング範囲の開始点であっても「^」記号は該当位置にマッチしません。しかしRegex.Match(string,int,int)のオーバーロードを使うと、マッチします。したがってオーバーロード引数を追加するだけで動作仕様が変化してしまうため、不自然な印象を受けます。次に例を記します。

string text = "abc";
new Regex( "^[a-z]" ).Match( text, 1 ); // どこにもマッチしない
new Regex( "^[a-z]" ).Match( text, 1, text.Length ); // 1文字目のbでマッチする

ポイント3です。Javaの正規表現エンジンでは「Anchoring Bounds」という概念があり、「^」記号の扱いをカスタマイズできます。Anchoring Boundsを使うよう設定すると、「^」記号および「$」記号のマッチング時にマッチング範囲の前後が考慮されない — つまり問答無用でマッチング範囲の始点に「^」記号がマッチするようになります。そしてAnchoring Boundsを使わないよう設定すると、マッチング範囲の開始点が文字列先頭あるいは行頭でない限り、「^」記号はマッチング範囲の開始点にマッチしません。このように「^」記号のマッチング動作を明示的に指定できる機構があれば、それを使うことで問題回避できますが、残念ながら.NETにはありません。

ポイント4です。どうやら.NETの正規表現エンジンではJavaでいう「Anchoring Bounds」の扱いがRegex.Matchのオーバーロードごとに異なっており、Regex.Match(string,int)はAnchoring Boundsを使わず、Regex.Match(string,int,int)はAnchoring Boundsを使う動作となっています。ここで、もしAnchoring Boundsを「常に使いたい」場合は外部でマッチング対象の文字列をSubstringすることで代替できます。しかしAnchoring Boundsを「常に使いたくない」場合、Regexクラスの外部でこれを実現する方法は無いように思われます。

この仕様はAPIからもドキュメントの記述内容からも想定できるものでなく、また統一が取れていないという点も考えると、意図的な設計結果とは思えません。本件は、改めてMicrosoft社に報告と確認をしておこうと考えています。

以下、各言語・環境での検証コードおよび検証結果を記します。なお言うべきことはすでに記したので、細かい説明はしません。興味のある方や再現してみたい気分になった方へ向けた情報です。

(続きを読む…)

行ごとの編集状態

Filed under: 未分類 — タグ: , , — Suguru Yamamoto @ 14:04

Azukiでは行ごとに3種類の編集状態を定義し、管理しています。具体的には未編集、編集済み、保存済み(一度以上編集されたが最後に保存された状態から変化していない)の3状態です。Azukiで3状態を扱うようにした背景は安直にも「Visual Studioに合わせただけ」なのですが、実際にやってみると思っていたより難しいことが分かりました。

この難しさの背景にはAzukiのデータ構造が単一のバッファ(gapped buffer)で構成されていることがあります。ドキュメント全体を単一のバッファで管理していることから、AzukiはUNDO/REDO情報をドキュメント全体に対する差分として記録しています。言い換えると「行単位の編集履歴」にはなっていません。もし編集履歴を行単位で記録していれば、ある行が「保存済み」状態であるかどうかは簡単に判定できます。しかし、ドキュメント単位での編集履歴で記録していると、正確に3状態を管理する方法が簡単には思いつきません。

元々は「保存済み」状態はUNDO/REDOによって復元されない仕様としていたので「未編集」と「編集済み」の2状態に関しては深く仕様検討したものの、「保存済み」状態についてまでは掘り下げて検討しませんでした。仕様範囲外だったので仕方ない部分もありますが、もう少し先を読んでおくべきだったかなと反省しています。「保存済み」状態までキッチリ検討できていないなら3状態とせず2状態とすることもできたはずなので。

2011/02/05

Azukiのステップ数を数えてみた

Filed under: 未分類 — タグ: , — Suguru Yamamoto @ 16:50

本日は駄投稿です :)

何となくAzukiのソースコードの「ステップ数」を計測してみようという気分になったので計測してみました。計測に使わせていただいたのはコロ助というフリーソフトです。また計測したソース一式は2011/02/05時点でsourceforge.jpのSubversionレポジトリにある最新のものです。

結果です。単体テストのコードを削除した上で全ソースファイルについて、コメント行や空行をのぞいた「有効行」を計測したところAzukiは20984行でした。ついでにAiB Toolsを計測してみたところ、こちらは24450行。4000行程度、AiB Toolsの方が多いようでした。

絶対値としては多いとも少ないとも驚かない数でしたが、Azukiという一つの単機能ライブラリがAiB Toolsという補助ライブラリを含むアプリケーション群の規模に追いついていることが、驚きというより感慨深かったです。ええと、以上です。話のオチはありません ;)

2010/12/12

Azuki 1.6、公開。

Filed under: 未分類 — タグ: — Suguru Yamamoto @ 00:38

本日、Azukiの最新版1.6.0を公開しました。1.5.0のリリースは2009/12/12だったので、なんとも奇遇なことに(ほぼ)ちょうど一年が経っていたことになります。

正直なところ、毎日肌で感じている「クラウド化」という情報技術の変化はOS上という層に位置する技術であるAzukiを開発する私に悪い刺激を与えています。Azukiで実現したいことはまだあるわけですが、モチベーションが開発開始当初より明らかに落ちていると感じています。

ある技術をメンテし続けるということはその時点の技術を使い続けるということでもあります。それ自体は何も悪いことではありませんが、それによって新しいモノへ割ける時間が減ります。そして新しいモノへついて行くのが難しくなるのだと思います。もちろん、どの時点の技術についても、そこに留まり支え続ける人々がいるからこそ新しいモノをその上に作ることができます。一番上だったレイヤーが、さらに上の層ができあがることで二番目のレイヤーになる。かつての最新技術は「時代遅れになる」のではなく、インフラ技術の一つになる。そういう話だと思います。

つまるところ今技術者としての自分の中にある焦りは「最先端にいられなくなるかもしれない」であり、迷いは「Azukiを含むOS直上の技術を磨き続けるべきなのか」です。まあ、それでもAiB ToolsやAzukiの開発をすぐ止めるつもりはありません。基本がクソマジメな人間なので作り手の責任を放棄するのは意地が許さないので ;)

2010/07/31

Unicodeの結合文字を改行コードに結合する

Filed under: 未分類 — タグ: , , — Suguru Yamamoto @ 21:20

Azuki 1.6のアルファ版でUnicodeの結合文字をサポートしました。ここで起こった失敗談を一つ記録しておきます。

Unicodeの結合文字についてUnicodeの仕様書を参照すると、どんな文字に対しても結合できるものであり、Unicodeの仕様としてはいかなり文字の並びであっても不正とは扱わないと書かれています。しかし、Azukiでまさにこのような考えで実装を行ったところ、思わぬ例外ケースが出てきてしまいました。「改行文字に結合文字が結合してしまう」ケースです。

まず、Azukiはプロジェクト発足当時から改行コードをCR、LF、CR+LFの3種類しかないという前提を置いてあらゆる機能設計を行っていました。そして、結合文字サポートに当たって、結合文字の前にどんな文字があるかを気にする必要は無いと考えてしまいました。その結果、改行コードの直後に結合文字がきてしまった場合にもAzukiはこれらを一つの書記素クラスタ(grapheme cluster)として扱うこととなり、「新しい改行コード」を生み出してしまいました。大前提であった「改行コードは3種類」を、うっかり崩していたわけです。そして大前提を崩した結果、ある処理中に特定条件が整うと無限ループに陥るバグが発生しました。

落ち穂を拾います。結合文字の前にどんな文字がきても良いのはUnicodeの話であって、Azukiの話ではありませんでした。Azukiにとって改行コードは文字であるだけでなく、行の区切りとして使われる特別な記号であり、そこに大きな前提を置いていたことを意識する必要があったと反省しています。

どうも最近出くわすプログラム不具合の多くは前提の見落としが原因になっています。assertionを多用することで見落としを防止できる前提もありますが、今回のケースではプログラム的な対策も難しかったと思います。結局のところ作っている人間の思考レベルで作り込むバグが一番対処しにくい。仕様書に記しても対策になりません。設計時に慎重になれば見つけられる可能性は上がりますが、そのかわりに生産性が落ちます。日常業務が終わった後の短い時間を少しずつ使って作っているAzukiの現状では、慎重になりすぎると何も形にならなくなってしまいますしね。難しいものです。

参照:

2010/07/09

修正に強いコーディング

Filed under: 未分類 — タグ: , , — Suguru Yamamoto @ 11:55

Azuki プロジェクトで一つデグレードを起こしてしまいました。クリップボードにテキストでないデータが入っているときに貼り付け処理を実行すると「クリップボードを開いたまま」になってしまい、他のアプリケーションがクリップボードを使えなくなってしまう、というバグです。

原因は、クリップボード内のデータ種別がサポート対象外だった場合にクリップボードをクローズせずに終了していた、という単純なものです。単純化して処理を擬似コードで示すと次のようになります。

クリップボードを開く
if( 開けない ) {
    return
}

if( クリップボードにUnicodeテキストがあるか )
    Unicodeテキストをそのまま取得
else if( クリップボードにANSIテキストがあるか )
    ANSIテキストをUnicodeに変換して取得
...
else
    return // クリップボードを閉じていない...

選択範囲を取得したテキストデータで置換

クリップボードを閉じる

まったくダメダメなミスで弁解の余地もありません…。ただ、実は最初からこのような処理になっていたわけではありません。あるときの機能追加でクリップボード関連処理を拡張するまでは、次のような形になっていました。

if( クリップボードにUnicodeテキストがあるか )
    データ種別 = UNICODE
else if( クリップボードにANSIテキストがあるか )
    データ種別 = ANSI
else
    return

クリップボードを開く
if( 開けない ) {
    return
}

if( データ種別がANSIテキスト )
    データをANSIテキストとして取得し、Unicodeに変換
else
    データをUnicodeテキストとしてそのまま取得

選択範囲を取得したテキストデータで置換

クリップボードを閉じる

元々は大丈夫だった、つまり機能拡張時のデグレードでした。

さて。元々の実装はバグがありません。なぜならクリップボード中のデータ種別をクリップボードを開く前に判定していたため、クリップボードにサポートしない種類のデータ(画像など)しか無い場合にクリップボードを閉じる必要は無かったのです。しかしこの「閉じる必要が無い」ことは、「例外ケースを前もって切り捨ててから処理に入る」という前提条件があってこそ実現されていたと思います。元々の実装ではこの前提条件が分かりやすく表現されていませんでしたし、コメントもありませんでした。最初に実装してから1年以上経過した私は、すっかり忘れていたというわけです(2008年1月に元々の処理を実装、2009年7月に機能拡張=デグレード、2010年7月にユーザフィードバックで発見される)。

では前提条件をはっきり記すべきだったのか?というと、私は違うと思います。最善は「前提条件を意識しないで済むよう実装する」ことだった思います。簡単に言うと、try/finally構文がまったく効果ない場合でも、リソースの解放が関わる場面なので愚直にtry/finally構文を使っておけば良かったのです。元々の実装ではtry/finallyはまったく不要でしたが、それでも泥臭くエレガントでもない愚直なtry/finallyでクリップボードを閉じておくべきだった、というのが今回得た教訓です。

前提条件を意識しないで済むようなコーディングを心がける。自分の集中力が多少低下していても大丈夫という意味で安全性を高めること、実装時の文脈を持たない他人に引き継いでも間違いを起こしにくくなることから、業務ではかなり意識しています。今回デグレードが見つかった箇所は学生時代に作った部分で、今読み返してみると未熟未熟。失敗を通して、成長を振り返る機会に恵まれたなと前向きに考えることにします。

2010/06/24

禁則処理に関する資料メモ

Filed under: 未分類 — タグ: — Suguru Yamamoto @ 21:44

「禁則処理」と呼ばれるテキストのレイアウト方法について、面白い資料を見つけたので備忘録。

W3C i18n article

2007年に書かれたものなので結構昔の資料ですね。何が面白いと思ったかというと、単純に「禁則処理」について英語で書かれているということと(禁則処理のこの部分は英語ではどう表現できるのか?という参考材料)、禁則処理が日本語特有の概念ではないことが分かること、の2点です。もちろん、私個人にとってですが。

Azukiの禁則処理の設計をある程度進めた段階で、中国語や韓国語にも禁則処理のようなものがありそうだなぁと漠然と考えていました。もし日本語以外にも禁則処理に相当する概念がある場合は少し意識して設計しておきたかったので、少しだけ気がかりだったのです。が、こうした資料から確実に存在することが分かり、またMicrosoft WordやAdobe Illustrator等の資料を読む限り、日本語版で使われている技法だけでどうやら対応できそう、だと分かりました。なので、日本的な禁則処理の実装方法で完成させてしまおうと思います。

中国人や韓国人に聞かないと分からないかと思っていたのですが、いやはや普通に検索すれば出てくるとは。インターネットというモノは、改めて凄いと思います。

Older Posts »

Powered by WordPress