第6章 ファクタリング(要素分解)

私たちは実装フェイズについての研究を続けます。本章ではファクタリング(要素分解)に焦点を当てます。

分解とファクタリング(要素分解)は、同じブロックから切り出されたものです。 どちらも分割と整理が関係しています。 分解は予備設計中に発生します。 ファクタリング(要素分解)は詳細設計と実装の間に行われます。

コロンの定義はすべてファクタリング(要素分解)の決定を反映しているので、優れたファクタリング(要素分解)手法を理解することは、おそらくForthプログラマにとって最も重要なスキルです。

ファクタリング(要素分解)とは何でしょうか? ファクタリング(要素分解)は、コードを有用な断片に整理することを意味します。 断片を有用とするには、再利用可能な部分と再利用不可能な部分を区別しなければならないことがよくあります。 再利用可能な部分は新しい定義になります。 再利用できない部分は、定義に対する引数またはパラメータになります。

この分離を行うことは、通常「括り出し(factoring out)」と呼ばれます。この章の最初の部分では、さまざまな「括り出し」手法について説明します。

どの程度まで定義に入れるか、または定義から外れるかを決定することは、ファクタリング(要素分解)のもう1つの側面です。 第2節では、有用なファクタリング(要素分解)の基準について概説します。

ファクタリング(要素分解)技法

システム内で有用な箇所が2回以上現れる場合は、有用なサブ機能を識別して切り分けるようにしてください。モジュールの残りの部分は、元の呼び出し元に組み込まれる可能性があります( Structured Design [stevens74-6] )。

もちろん、「有用なサブ機能」が新しく定義された定義になります。「まったく有用ではない」という部分についてはどうでしょうか。 それはそれが何であるかによって異なります。

データの括り出し

Forthのデータスタックのおかげで、最も簡単に括り出せるのはデータです。 たとえば、1,000の3分の2を計算するには、次のように記述します。

1000 2 3 */

任意の数の3分の2を計算するワードを定義するには、定義から引数を取り除きます。

: TWO-THIRDS  ( n1 -- n2)  2 3 */ ;

データが有用なフレーズの中にくると、スタック操作を使用する必要があります。 たとえば、80桁の画面で長さ10文字のテキストを中央揃えにするには、次のように記述します。

80  10 -   2/ SPACES

しかし、テキストは必ずしも10文字の長さではありません。 フレーズをあらゆる文字列に役立つようにするには、次のように記述して長さを計算します。

: CENTER ( length -- ) 80  SWAP -  2/ SPACES ;

データスタックはアドレスを渡すためにも使用できます。 そのため、データ自体ではなく、データへのポインタが括り出される可能性があります。 データは数値でも文字列でもかまいませんが、それでもスタックを使用して括り出すことができます。

「数の差異」は機能の違いのように見える事もありますが、単純にスタック上の数値として括り出す事ができます。

Segment 1: WILLY WILLY   PUDDIN’ PIE AND
Segment 2: WILLY NILLY 8 * PUDDIN’ PIE AND

どうやって 8 * という操作を括り出せますか? 以下のように、ファクタリング(要素分解)に * を含め、それに1つまたは8つを渡すことによって可能です。

: NEW  ( n )  WILLY NILLY  *  PUDDIN' PIE AND ;
Segment 1:
1 NEW
Segment 2:
8 NEW

(もちろん、 WILLY NILLY がスタックを変更する場合は、適切なスタック操作演算子を追加する必要があります。)

操作に加算が含まれる場合は、ゼロを渡して無効にすることができます。

ヒント

簡単にするために、類似の断片の差異を、手続き上の違いとしてではなく、数値の違い(値またはアドレス)として表すよう試みて下さい。

機能の括り出し

一方、機能はしばしば差異があります。こんなのを目撃しました。

Segment 1:
BLETCH-A  BLETCH-B BLETCH-C
         BLETCH-D  BLETCH-E  BLETCH-F
Segment 2:
BLETCH-A  BLETCH-B  PERVERSITY
         BLETCH-D  BLETCH-E  BLETCH-F

悪いアプローチ

: BLETCHES  ( t=do-BLETCH-C | f=do-PERVERSITY -- )
   BLETCH-A  BLETCH-B  IF  BLETCH-C  ELSE  PERVERSITY
      THEN  BLETCH-D BLETCH-E BLETCH-F ;
Segment 1:
TRUE BLETCHES
Segment 2:
FALSE BLETCHES

より良いアプローチ

: BLETCH-AB   BLETCH-A BLETCH-B ;
: BLETCH-DEF   BLETCH-D BLETCH-E BLETCH-F ;
Segment 1:
BLETCH-AB BLETCH-C BLETCH-DEF
Segment 2:
BLETCH-AB PERVERSITY BLETCH-DEF

ヒント

制御フラグを下流へ渡さないで下さい。

何故でしょうか? まず、実行中のアプリケーションに、プログラム作成時に答えが判っている無意味な判定を下すように求めているため効率が低下します。 次に、用語は概念モデルと一致しません。FALSE BLETCHES とは対照的に、 TRUE BLETCHES とは何ですか?

制御構造内からコードを括り出す

IF ELSE THEN ステートメントの両側の繰り返しに注意してください。 たとえば以下のようなのです。

... ( c)  DUP  BL 127 WITHIN
       IF  EMIT  ELSE
       DROP  ASCII . EMIT   THEN ...

この断片は通常ASCII文字を生成しますが、その文字が制御コードの場合はドットを生成します。 いずれにせよ、 EMIT が実行されます。 次のようにして条件付き構造から EMIT を括り出します。

... ( c)  DUP  BL 127 WITHIN NOT
       IF  DROP  ASCII .  THEN  EMIT  ...

最も厄介な状況は、2つの定義の違いが制御構造内の機能である場合です。この場合断片の片割れだけを括り出すことは不可能です。この場合は、スタック引数、変数、更にはベクトル化を使って下さい。 ベクトル化をどのように使うのかは chapter7 を参照下さい。

以下は DO LOOP からコードを括り出すことについての注意です。

ヒント

DO LOOP の内容を新しい定義に分解する際には、 I (インデックス)が新しい定義内で参照されず、スタック引数として渡されるようにコードを作り直してください。

制御構造自体の括り出し

ここに IF THEN 構造の中が異なる2つの定義があります。

: ACTIVE    A B OR  C AND  IF  TUMBLE JUGGLE JUMP THEN ;
: LAZY      A B OR  C AND  IF   SIT  EAT  SLEEP   THEN ;

条件と制御構造は変わりません。イベントだけが違います。 IFTHEN を、それ以上分解することはできないので、最も簡単なのは条件を考慮することです。

: CONDITIONS? ( -- ?) A B OR C AND ;
: ACTIVE    CONDITIONS? IF TUMBLE JUGGLE JUMP THEN ;
: LAZY      CONDITIONS? IF    SIT  EAT  SLEEP THEN ;

同じ条件と制御構造の繰り返し数に応じて、その両方を括り出すこともできます。 以下を見てください。

: CONDITIONALLY   A B OR  C AND NOT IF  R> DROP   THEN ;
: ACTIVE   CONDITIONALLY   TUMBLE JUGGLE JUMP ;
: LAZY   CONDITIONALLY  SIT  EAT  SLEEP ;

条件に応じて、ワード CONDITIONALLY は、各定義の残りのワードをスキップする制御フローを変更することがあります。この方法にもいくつかの欠点があります。 この技法の賛否については 第8章 で議論します。

括り出す制御構造のより良い例にはcaseステートメントがあります。それはネストされた IF…ELSE…THEN や、複数の出口を持つループ(BEGIN…WHILE…WHILE…WHILE…REPEAT 構造)を取り除きます。これらの話題については 第8章 でも議論します。

名前の括り出し

名前がほとんど同じだが、まったく同じではないと思われる場合は、名前を括り出すこともできます。 以下のひどいコード例を見てください。これは、8つのチャネルそれぞれに関連付けられた3つの変数を初期化することを意味しています。

VARIABLE 0STS       VARIABLE 1STS       VARIABLE 2STS
VARIABLE 3STS       VARIABLE 4STS       VARIABLE 5STS
VARIABLE 6STS       VARIABLE 7STS       VARIABLE 0TNR
VARIABLE 1TNR       VARIABLE 2TNR       VARIABLE 3TNR
VARIABLE 4TNR       VARIABLE 5TNR       VARIABLE 6TNR
VARIABLE 7TNR       VARIABLE 0UPS       VARIABLE 1UPS
VARIABLE 2UPS       VARIABLE 3UPS       VARIABLE 4UPS
VARIABLE 5UPS       VARIABLE 6UPS       VARIABLE 7UPS
: INIT-CHO   0 0STS !  1000 0TNR !  -1 0UPS ! ;
: INIT-CH1   0 1STS !  1000 1TNR !  -1 1UPS ! ;
: INIT-CH2   0 2STS !  1000 2TNR !  -1 2UPS ! ;
: INIT-CH3   0 3STS !  1000 3TNR !  -1 3UPS ! ;
: INIT-CH4   0 4STS !  1000 4TNR !  -1 4UPS ! ;
: INIT-CH5   0 5STS !  1000 5TNR !  -1 5UPS ! ;
: INIT-CH6   0 6STS !  1000 6TNR !  -1 6UPS ! ;
: INIT-CH7   0 7STS !  1000 7TNR !  -1 7UPS ! ;
: INIT-ALL-CHS    INIT-CHO  INIT-CH1  INIT-CH2  INIT-CH3
   INIT-CH4  INIT-CH5  INIT-CH6  INIT-CH7 ;

まず、変数の名前には類似点があります。 それから、INIT-CH 系ワードで使われているコードには類似性があります。

これが改良された表現です。 同様の変数名は3つのデータ構造にまとめられ、長い INIT-CH 系ワードの演奏会は DO…LOOP にまとめられました。

: ARRAY  ( #cells -- )  CREATE  2* ALLOT
   DOES> ( i -- 'cell)  SWAP  2* + ;
8 ARRAY STATUS  ( channel# -- adr)
8 ARRAY TENOR   (        "       )
8 ARRAY UPSHOT  (        "       )
: STABLE   8 0 DO  0 I STATUS !  1000 I TENOR !
   -1 I UPSHOT !  LOOP ;

私たちに必要なコードはこれっきりです。

最も他愛のない場合でさえ、小さなデータ構造は余分な名前を排除することができます。 慣例により、Forthはテキストを「文字数付き文字列」(すなわち、最初のバイトに文字数)で扱います。 「文字列のアドレス」を返す単語は、実際にはこの文字数格納先頭アドレスを返します。 この2要素のデータ構造を使用すると、「文字列」と「カウント」に別々の名前を使用する必要がなくなるだけでなく、文字列とカウントを1つの CMOVE でコピーできるため、文字列をメモリ内で移動しやすくなります。

あちこちで同じぎこちなさを見つけ始めたら、物事を組み合わせてぎこちなさを解消することができます。

定義ワード内の機能の括り出し

ヒント

定義のシリーズに同一の機能が含まれていて、データに違いがある場合は、定義ワードを使用してください。

このコードの構造を調べてください(ここでは、その目的は気にしないでください。後でもう一度同じ例を出します)。

: HUE  ( color -- color')
   'LIGHT? @  OR  0 'LIGHT? ! ;
: BLACK   0 HUE ;
: BLUE   1 HUE ;
: GREEN   2 HUE ;
: CYAN   3 HUE ;
: RED   4 HUE ;
: MAGENTA   5 HUE ;
: BROWN   6 HUE ;
: GRAY   7 HUE ;

上記の方法は技法的には正しいですが、定義ワードを使用する次の方法よりもメモリ効率が低くなります。

: HUE   ( color -- )  CREATE ,
   DOES>  ( -- color )  @ 'LIGHT? @  OR  0 'LIGHT? ! ;
 0 HUE BLACK         1 HUE BLUE          2 HUE GREEN
 3 HUE CYAN          4 HUE RED           5 HUE MAGENTA
 6 HUE BROWN         7 HUE GRAY

(定義ワードは Starting Forth, Chapter Eleven;邦訳 FORTH入門 第11章 に説明があります。)

コンパイルされた各コロン定義は定義を終了するために EXIT のアドレスを必要とするので、定義ワードを使用するとメモリを節約します(8ワードを定義する場合、定義ワードを使用すると16ビットのForthで14バイト節約できます)。また、コロン定義では、数値リテラルを参照するたびに LIT (または literal )をコンパイルする必要があります)、定義ごとにさらに2バイト(1と2が事前定義された定数の場合、これにはさらに10バイト、合計24バイトのコストがかかります)。

読みやすさの点では、定義ワードは、それが定義するすべての色が同じワードのグループに属することを直接的に明確にしています。

しかし、定義ワードの最大の強みは、一連の定義が同じコンパイル時の動作を共有するときに生じます。 この話題は、後節の「コンパイル時ファクタリング(要素分解)」の主題です。

ファクタリング(要素分解)基準

ファクタリング(要素分解)手法について理論武装したので、今度はForth定義をファクタリング(要素分解)するためのいくつかの基準について説明しましょう。 以下の内容が含まれます。

  1. 定義のサイズを制限する
  2. コードの繰り返しを制限する
  3. 命名可能性
  4. 情報隠蔽
  5. コマンドインターフェイスの単純化

ヒント

定義を短く保って下さい。

私たちは ムーア に、「Forthの定義はどれくらいの長さであるべきですか?」と尋ねました。ムーアは言います。

ワードは1行の長さであるべきです。それが目標です。

あなたが、自身で有用とする正しい(おそらくデバッグや探索で必然的にそれらが存在する理由がある)ワードをたくさん持っているなら、あなたは問題の本質を抽出し、それらのワードで表現していると言えます。

短い言葉はあなたに良い感じを与えます。

ムーアのアプリケーションの1つを非公式に調べたところ、彼の定義は、ワードと数字が平均7つ含まれていました。これらは非常に短い定義です(実際、彼のコードは1行定義と2行定義の割合が半々でした)。

心理テストでは、人間の心は意識的注意を一度に7つのこと、それか7±2つだけに集中させることができることが示されています。 [miller56] それでも、昼夜を問わず、心の膨大なリソースは潜在的に膨大な量のデータを保存し、つながりや関連付けをし、問題を解決しています。

潜在意識がアプリケーションの各部分を内側から知っていても、私たちの狭視野意識は一度にその7つの要素しか関連付けることができません。 それ以上だと、私たちの把握はゆらぎます。短い定義は私たちの精神的な能力と一致します。

多くのForthプログラマが過度に長い定義を書きたくなるのは、ヘッダが辞書内のスペースを取るという知識です。 ファクタリング(要素分解)が粗いほど、名前が少なくなり、無駄になるメモリが少なくなります。

より多くのメモリが使用されるのは事実ですが、テスト、デバッグ、コードとの対話など全てに役立つものが「無駄」であるとは言い難いです。アプリケーションが大きい場合は、各辞書ヘッダーの名前フィールドに格納される文字数のデフォルトを3文字で使用してみてください。 衝突を避けるために、特定の名前について長さ制限無しに切り替えます。

それでもアプリケーションが大きすぎる場合は、拡張メモリを搭載したマシンで複数の辞書を含むForthに切り替えるか、32ビットアドレッシングを搭載したマシンで32ビットForthに切り替えます。

関連する恐怖は、Forthの内部インタプリタのオーバーヘッドのために過剰分解がパフォーマンスを低下させることです。 繰り返しますが、ネスティングの各レベルにはいくらかのペナルティがあります。 しかし、通常は、適切な分解による追加のネスティングに対するペナルティは目立ちません。 あなたの納期がそれほど厳しくないなら、本当の解決策はアセンブラに翻訳することです。

ヒント

分解するポイントは、あなたがあなたのコードがわけわかめと感じるポイントです(複雑さが意識的な限界に近づいていると感じるポイントです)。

「オレはこれを征服してやる!」なんて態度で挑む必要はありません。Forthコードは不快で複雑に感じることはありません。ファクタリング(要素分解)して下さい。

ムーア は言います。

あなたがファクタリング(要素分解)する理由の一つは、バグを導入したかもしれないと感じた時です。二重にネストされた DO…LOOP を見たときはいつでも、それはデバッグが難しいので何かが間違っているというサインです。 ほとんどの場合、内側の DO…LOOP を取り出しワードを作ります。

テスト用のワードをまとめたら、元に戻す理由はありません。 あなたはそれがそもそも役に立つと思ったのです。 あなたが再びそれを必要としないという保証はありません。

以下は同じ原則の別の側面です。

ヒント

コメントが必要に見える箇所はファクタリング(要素分解)して下さい。

特に、スタックの内容を思い出す必要があると感じた場合は、「休憩をとる」のがよいでしょう。

以下のようなのがあるとします。

... BALANCE  DUP xxx xxx xxx xxx xxx xxx xxx xxx xxx
     xxx xxx xxx xxx xxx xxx   ( balance) SHOW  ...

残高を計算することで始まり、それを表示することで終わります。 それまでの間、数行のコードでは、独自の目的で残高を使用しています。 SHOW を実行するときに残高がまだスタック上にあることを確認するのは困難なので、プログラマはスタック状況を差し挟みました。

この解決策は一般的に悪いファクタリング(要素分解)の兆候です。 以下のように書くほうがよいです。

: REVISE  ( balance -- )  xxx xxx xxx xxx xxx xxx xxx
     xxx xxx xxx xxx xxx xxx xxx ;
... BALANCE  DUP REVISE  SHOW  ...

物語スタック状況は必要ありません。更に、 さらに、プログラマは現在、再利用可能でテスト可能な定義のサブセットを持っています。

ヒント

コードの繰り返しを制限してください。

ファクタリング(要素分解)の2番目の理由は、コードの断片の繰り返しを取り除くことです。それは定義のサイズを小さくすることよりもさらに重要です。

ムーア は言います。

ワードが単に何かの一部である場合、明瞭さやデバッグには役立ちますが、何度も使用されるワードほどではありません。いつでも、一度だけしか使われないワードはその価値を疑問視したくなります。

プログラムが大きくなりすぎると、私はファクタリング(要素分解)のための候補として、目に止まるフレーズを探して何度も右往左往します。コンピュータはこれを行うことができません。 変数が多すぎます。

自分の作品を見てみると、同じフレーズや短い文章が何度か重複しているのがよくわかります。 エディタを書く際に、以下のフレーズが数回繰り返されているのがわかりました。

FRAME  CURSOR @ +

それが何度か現れたので、私はそれを AT と呼ばれる新しいワードにまとめました。

次のように、コーディング方法が異なるが機能的に同等の断片を認識するのは、あなた次第です。

FRAME  CURSOR @ 1-  +

1- は、このフレーズを AT の定義とは異なるもののように見せていますが、実際には以下のように AT を使って書くことができます。

AT 1-

その一方で、

ヒント

重複するコードを括り出すときは、括り出されたコードが単一の目的を果たすことを確認してください。

役に立たないかもしれない重複を盲目的に掴まえないでください。 たとえば、1つのアプリケーションのいくつかの場所で以下のフレーズを使用しました。

BLK @ BLOCK  >IN @ +  C@

私はそれを新しいワードに変えて、それを LETTER と呼びました。なぜならそれはインタプリタが指し示す文字を返すからです。

後の改訂では、予想外に、以下のように書かなければなりませんでした。

BLK @ BLOCK  >IN @ +  C!

最後の C@ のためではないのであれば、既存の LETTER を使用することができました。 新しい区間でフレーズの大部分を複製するのではなく、私は LETTER をより細かい粒度にリファクタリングして C@ を取り出すことを選びました。 そのときの用法は LETTER C@LETTER C! のどちらかでした。 この変更により、リスト全体を検索して、 LETTER のすべてのインスタンスを LETTER C@ に変更する必要がありました。 しかし、私は最初に文字の住所の計算と住所で実行される操作とを区別して、それを行うべきでした。

以下のヒントは、私たちのコード繰り返し禁止令と同じ意味です。

ヒント

パターンの繰り返しを探して下さい。

プログラム内の既存のコードでワードのパターンをコピーするように参照している場合は、一般的なアイデアと特定のアプリケーションが混在している可能性があります(訳注:それは、詳細に分析しない限り、書いた人にしか分からない)。 あなたが自身がコピーしているパターンは、おそらくすべての同様の場合に使用できる独立した定義として括り出すことができます。

ヒント

自分が分解したものに名前を付けることができることを確認してください。

ムーア は言います。
ハイフンで繋がれた名前でなく単一の名前を割り当てる事ができない概念がある場合は、それは名前ではなく、整形式の概念ではありません。名前を割り当てる能力は分解の主要な部分です。あなたはその考えに必ずや自信を持つようになります。

この見方と、第1章 の中の構造化設計に基づくモジュールを分解する基準とを比較してください。 その方法によれば、モジュールは「機能的結合」を示すべきであり、それはその機能を単一の非複合的な「文」で記述することによって検証することができます。 Forthの「アトム」である「名前」は、さらに洗練された命令です。

ヒント

変化する可能性のある詳細を隠すために定義を分解して下さい。

特に予備設計に関しては、情報隠蔽の価値を前述しました。実装段階でもこの基準を覚えておくと便利です。

以下は、非常に短い定義で、情報を隠すこと以外はほとんど何もしていません。

: >BODY  ( acf -- apf )  2+ ;

この定義により、辞書定義の実際の構造に依存せずに、acf(コードフィールドのアドレス)をapf(パラメータフィールドのアドレス)に変換できます。 もし >BODY の代わりに 2+ を使ったら、ヘッダがボディから分離されているForthシステムに変換した時にポータビリティを失うでしょう(これは、キム・ハリスによって提案されたワード・セット1つであり、Forth-83標準の[実験的提案] [harris83] に含まれています)。

以下は、エディタを書くのに使われるかもしれない定義のグループです。

: FRAME  ( -- a)  SCR @ BLOCK ;
: CURSOR  ( -- a)  R# ;
: AT  ( -- a)  FRAME  CURSOR @ + ;

これら3つの定義は、テキストを動かすために必要なすべてのアドレス計算の基礎を形成することができます。 これら3つの定義を使用すると、編集アルゴリズムとForthブロックへの依存が完全に分離されます。

それの何がいいのでしょうか? 開発中に、ユーザがブロックを破壊するようなエラーを起こさないようにするための編集バッファを作成することにした場合は、単に次の2つのワードを再定義するだけで済みます。

CREATE FRAME  1024 ALLOT
VARIABLE CURSOR

あなたのコードの残りはそのままです。

ヒント

結果を表示する定義の機能を括り出して下さい。

これは本当に分解の問題です。

ここに一例があります。以下で定義される「人々から経路へ(people-to-paths)」と発音されるワードは、グループ内の所与の数の人の間にいくつのコミュニケーション経路があるかを計算します(これは、プログラマチームの管理者にとって知っておくとよいことです。チームが新しく追加されるたびにコミュニケーション経路の数が劇的に増加します)。

: PEOPLE>PATHS  ( #people -- #paths )  DUP 1-  *  2/ ;

この定義は計算のみを行います。 以下は、PEOPLE>PATHS を呼び出して計算を実行し、その結果を表示する「ユーザ定義」です。

: PEOPLE  ( #people)
    ." = "  PEOPLE>PATHS  .  ." PATHS " ;

以下のようになります( 2 PEOPLE [ Enter ] )

2 PEOPLE = 1 PATHS
3 PEOPLE = 3 PATHS
5 PEOPLE = 10 PATHS
10 PEOPLE = 45 PATHS

その計算を1回しか実行しないと思うのも、その表示を1回しかしない思うのも、あなたは間違っています。あなたは後で戻ってきて、計算部分を括り出す必要があります。 おそらく、情報を右寄せの列に表示する必要があるか、結果をデータベースに記録する必要があるでしょう。 しかし、あなたは常にそれを考慮に入れなければならないので、あなたはそれを最初からちゃんと記述するかもしれません。(あなたはそれで何回かは逃れる事ができるかもしれませんが、ファクタリング(要素分解)をやっておくのは悪いことではありません)。

ワード . (ドット)はその代表的な例です。 ドットは99%の割合で素晴らしいですが、時々やり過ぎです。 実際には以下のようになります(Forth-83の場合)。

: .   ( n )  DUP ABS 0 <# #S  ROT SIGN  #> TYPE SPACE ;

しかし、スタック上の数値をASCII文字列に変換し、後で入力するためにバッファに格納したいとします。ドットはそれを変換しますが、それもタイプします。または、トランプを 10C 形式(10のクラブの場合)で書式出力したいとします。 ドットを使用して10を表示することはできません。最後に空白が出力されるからです。

以下はいくつかのForthシステムに見られる、より良いファクタリング(要素分解)です。

: (.)  ( n -- a #)  DUP ABS 0  <# #S  ROT SIGN  #> ;
: .  ( n)  (.) TYPE SPACE ;

計算関数から出力関数を分解できない例が、 第4章 の私たち自身のローマ数字の例にありました。私たちの解決策では、ローマ数字をバッファに格納することも、フィールドの中央に配置することもできません(もっと良い方法は EMIT の代わりに HOLD を使うことでした)。

情報隠蔽もファクタリング(要素分解)しない理由になることがあります。例えば、もし以下のフレーズをファクタリング(要素分解)して、

SCR @ BLOCK

定義にします

: FRAME   SCR @ BLOCK ;

編集フレームの位置を変更する必要があるかもしれないという理由だけで、そうしていることを覚えておいてください。 FRAME の定義を変更する可能性があり、また、 SCR @ BLOCK が本当に欲しい場合があるので、フレーズのすべての出現箇所を新しい単語 FRAME で盲目的に置き換えないでください。

ヒント

場合によっては繰り返しコードの断片が変わる可能性があるが、そうでない場合は、変わる可能性のある実体だけを括り出します。 断片が複数の方法で変更される可能性がある場合は、それを複数の定義に分解します。

情報を隠すタイミングを知るには、直感と経験が必要です。 あなたのキャリアに多くの設計変更を加えたので、あなたは物事が将来最も変わる可能性があるだろう難しい方法を学ぶでしょう。

しかし、すべてを予測することはできません。 次節の「実装における反復アプローチ」にあるように、試行は無駄になるでしょう。

ヒント

コマンド数を減らしてコマンドインターフェイスを簡素化します。

それは逆説的に見えるかもしれませんが、良いファクタリング(要素分解)はしばしば少ない名前で済みます。 第5章 では、6つの単純な名前(LEFT, RIGHT, MOTOR, SOLENOID, ON, OFF)が、8つの悪い因果関係のあるハイフンで繋がれた名前と同じ仕事をすることができます。

別の例として、私は最近Forthを導入したある部門で2つの定義が循環しているのを見つけました。純粋に教育的観点から、どちらの語彙が CURRENT でどれが CONTEXT であるかをプログラマに思い出させるために、以下の説明をしました。

: .CONTEXT   CONTEXT @  8 -  NFA  ID.  ;
: .CURRENT   CURRENT @  8 -  NFA  ID.  ;

あなたが以下のようにタイプしたら、

.CONTEXT

システムは以下のように応答します

.CONTEXT FORTH

(少なくともそのシステムでは、それらは、ボキャブラリ定義のネームフィールドにバックアップされたのを表示しました。)

コードが明らかに繰り返されていることが、悪いファクタリング(要素分解)の兆候として私の目を惹きました。 繰り返された箇所を第三の定義にまとめることは可能だったでしょう。

: .VOCABULARY   ( pointer )  @  8 -  NFA  ID. ;

元の定義を次のように短くします。

: .CONTEXT   CONTEXT .VOCABULARY ;
: .CURRENT   CURRENT .VOCABULARY ;

しかし、このアプローチでは、2つの定義の唯一の違いは表示されるポインタです。 良いファクタリング(要素分解)の一部は、より多くの定義ではなく、より少ない定義を作成することであるため、定義を1つだけにして、それを引数として CONTEXT または CURRENT というワードを取ることは論理的に思えます。

良い命名の原則を適用して、私は以下の提案をしました。

: IS  ( adr)   @  8 -  NFA  ID. ;

これは以下の構文を許します( CONTEXT IS [Enter] )

CONTEXT IS ASSEMBLER ok

または ( CURRENT IS [Enter] )

CURRENT IS FORTH ok

最初の手がかりはコードの繰り返しでしたが、目的としてはコマンドインターフェイスを単純化しようとしました。

以下は別の例です。 IBM PCには、テキストのみを表示する4つのモードがあります。

40桁モノクロ

40桁カラー

80桁モノクロ

80桁カラー

MODE というワードは私が使っているForthシステムで利用できます。 MODE は0から3の間の引数を取り、それに応じてモードを変更します。 もちろん、0  MODE または 1 MODE というフレーズ、どちらのモードがどれであるかを覚えておくのに役に立ちません。

プレゼンテーションを行う際にはこれらのモードを切り替える必要があるので、変更を有効にするために便利なワードセットを用意する必要があります。 これらの語は、現在の桁数(40または80)を含む変数も設定する必要があります。

以下は、要件を満たす最も簡単な方法です。

: 40-B&W       40 #COLUMNS !  0 MODE ;
: 40-COLOR     40 #COLUMNS !  1 MODE ;
: 80-B&W       80 #COLUMNS !  2 MODE ;
: 80-COLOR     80 #COLUMNS !  3 MODE ;

繰り返しを排除するようにファクタリング(要素分解)することによって、私たちは以下のバージョンを思い付きます。

: COL-MODE!     ( #columns mode )  MODE  #COLUMNS ! ;
: 40-B&W       40 0 COL-MODE! ;
: 40-COLOR     40 1 COL-MODE! ;
: 80-B&W       80 2 COL-MODE! ;
: 80-COLOR     80 3 COL-MODE! ;

しかし、コマンドの数を減らそうと試みることによって、そしてまた、数字のプレフィックスとハイフンで繋がれた名前に対する命令をたどることによって、列の数をスタック引数として使うことができ、そしてモードを計算することができることを悟ります。

: B&W    ( #cols -- )  DUP #COLUMNS !  20 /  2-     MODE ;
: COLOR  ( #cols -- )  DUP #COLUMNS !  20 /  2-  1+ MODE ;

これは以下のような構文になります。

40 B&W
80 B&W
40 COLOR
80 COLOR

私たちはコマンド数を4つから2つに減らしました。

繰り返しになりますが、コードが重複しています。 このコードを括り出すと、次のようになります。

: COL-MODE!  ( #columns chroma?)
   SWAP DUP #COLUMNS !  20 / 2-  +  MODE ;
: B&W    ( #columns -- )  0 COL-MODE! ;
: COLOR  ( #columns -- )  1 COL-MODE! ;

これで、より優れた構文が達成され、同時にオブジェクトコードのサイズも大幅に削減されました。 この例のようにコマンドが2つしかない場合、メリットはわずかです。 しかし、より多くのコマンドを使用すると、その利点は幾何学的に向上します。

最後の例は、特定のシステムの色を表すためのワードのセットです。 BLUERED のような名前は数字よりも素敵です。 1つの解決策は、次のように定義することです。

 0 CONSTANT BLACK                 1 CONSTANT BLUE
 2 CONSTANT GREEN                 3 CONSTANT CYAN
 4 CONSTANT RED                   5 CONSTANT MAGENTA
 6 CONSTANT BROWN                 7 CONSTANT GRAY
 8 CONSTANT DARK-GRAY             9 CONSTANT LIGHT-BLUE
10 CONSTANT LIGHT-GREEN          11 CONSTANT LIGHT-CYAN
12 CONSTANT LIGHT-RED            13 CONSTANT LIGHT-MAGENTA
14 CONSTANT YELLOW               15 CONSTANT WHITE

これらの色は、 BACKGROUNDFOREGROUNDBORDER などのワードで使用できます。

WHITE BACKGROUND  RED FOREGROUND  BLUE BORDER

しかし、この解決策は16の名前を必要とし、それらの多くはハイフンを含んでいます。これを単純化する方法はあるでしょうか?

8から15の間の色は、すべて0から7の間の色の「より明るい」バージョンです(ハードウェアでは、これら2つの組の間の唯一の違いは「強度ビット」の設定です)。 「明るさ(lightness)」、私たちは以下の解決策を思いつきます。

VARIABLE 'LIGHT?  ( intensity bit?)
: HUE  ( color)  CREATE ,
   DOES>  ( -- color )  @  'LIGHT? @  OR  0 'LIGHT? ! ;
 0 HUE BLACK         1 HUE BLUE           2 HUE GREEN
 3 HUE CYAN          4 HUE RED            5 HUE MAGENTA
 6 HUE BROWN         7 HUE GRAY
: LIGHT   8 'LIGHT? ! ;

その構文は以下のようになります。

BLUE

これは、それ自身によってスタックに1を返します。

LIGHT BLUE

これは、9を返します。(形容詞 LIGHT は色相(hues)によって使用されるフラグをセットし、その後クリアされます)。

更に読みやすさが必要なら、私たちは以下のように定義したいです。

8 HUE DARK-GRAY
14 HUE YELLOW

繰り返しますが、このアプローチを通じて、より快適な構文とより短いオブジェクトコードを実現しました。

ヒント

ファクタリング(要素分解)の為のファクタリング(要素分解)をしないでください。決まり文句を使って下さい。

以下のフレーズ

OVER + SWAP

これは、特定のアプリケーションでよく見られます(アドレスとカウントを DO‥LOOP に適した終了アドレスと開始アドレスに変換します)。

他によく見られるフレーズが以下です。

1+ SWAP

(「最初の数値」と「最後の数値」を、DO が必要とする「最後の数値+1」と「最初の数値」の順序に並べ替えます。)

(最初のフレーズでは) これらのフレーズを RANGE のようなワードに変えるのは、ちょっと素敵に思えます。

ムーア は言います。
その OVER + SWAP というフレーズは、有用なワードであることの限界を超えたものです。 ただし、何かをワードとして定義すると、1回しか使用されないことがよくあります。 あなたがそのようなフレーズに名前を付けると、 RANGE が何をするのかを正確に知ることが困難になります。 あなたはあなたの心の中の操作を見ることができません。 OVER + SWAPRANGE よりも覚えやすいです。

私はこれらのフレーズを「決まり文句」と呼びます。それらは意味のある一つの機能としてまとめて扱います。 フレーズ内のどのようになっているか覚えておく必要はなく、それが為すことだけを覚えておけばいいのです。そして追加の名前を覚える必要はありません。

コンパイル時のファクタリング(要素分解)

前節では、冗長性を減らすためにコードとデータを整理する多くの手法を調べました。

私たちは、Forthにいくつかの汚い作業をさせることで、コンパイル中に制限付き冗長性を適用することもできます。

ヒント

最大限の保守性を維持するために、コンパイル時でも冗長性を制限してください。

我々のアプリケーションで、 リスト 9 のように9つの箱を描かなければならないとしましょう。

リスト 9 問題:9つの箱
********     ********     ********
********     ********     ********
********     ********     ********
********     ********     ********
********     ********     ********

********     ********     ********
********     ********     ********
********     ********     ********
********     ********     ********
********     ********     ********

********     ********     ********
********     ********     ********
********     ********     ********
********     ********     ********
********     ********     ********

私たちの設計では、各箱の寸法、箱間の感覚の寸法、最初の箱の一番左と一番上の座標などの値を表す定数が必要です。

私たちは自然と、以下のように定義します。

8 CONSTANT WIDE
5 CONSTANT HIGH
4 CONSTANT AVE
2 CONSTANT STREET

(ストリート(street)は東西で、アベニュー(avenue)は南北です。)

さて、左マージンを定義するために、それを暗算するかもしれません。これらすべての箱を80カラム幅の画面の中央に置きたいと思います。 何かを中央に配置するには、80からその幅を引いて2で割り、左マージンを決定します。 すべての箱の合計幅を計算するために、私たちは以下の加算をします。

8 + 4 + 8 + 4 + 8 = 32

(3箱の幅とその間の2つのアベニュー). (80-32) / 2 = 24

それで、私たちは大胆にも以下のように定義します。

24 CONSTANT LEFTMARGIN

TOPMARGIN にも同じアプローチを行います。

しかし、後で幅を変えたり、箱の間の間隔を変えたりするために、後でパターンを再設計する必要がある場合はどうなるでしょうか?左マージンを自分で再計算する必要があります。

Forth環境では、コンパイルしているときでもForthの全機能を使用できます。なぜForthに考えさせないのですか?

WIDE 3 *  AVE 2 *  +  80 SWAP -  2/ CONSTANT LEFTMARGIN
HIGH 3 *  STREET 2 * +  24 SWAP -  2/ CONSTANT TOPMARGIN

ヒント

定数の値が以前の定数の値に依存する場合は、Forthを使用して2番目の値を計算します。

これらの計算はアプリケーションの実行時には実行されないため、実行速度は影響を受けません。

以下は別の例です。 リスト 10 は形を描くワードのコードを示しています。ワード DRAWPOINTS と呼ばれる表にリストされたすべてのxy座標で * を表示します(注:ワード XY は、スタック上の(x y)座標にカーソルを移動します)。

POINTS リストの直後の行に注目してください。

HERE POINTS -  ( /table)  2/  CONSTANT #POINTS
リスト 10 コンパイル時の冗長性を制限するもう1つの例。
: P  ( x y -- )  C, C, ;
CREATE POINTS
   10 10 P     10 11 P     10 12 P     10 13 P     10 14 P
   11 10 P     12 10 P     13 10 P     14 10 P
   11 12 P     12 12 P     13 12 P     14 12 P
HERE POINTS -  ( /table)  2/  CONSTANT #POINTS
: @POINTS  ( i -- x y)  2* POINTS + DUP 1+ C@  SWAP C@ ;
: DRAW  #POINTS 0 DO  I @POINTS  XY  ASCII * EMIT  LOOP ;

The phrase HERE POINTS - computes the number of x–y coordinates in the table: this value becomes the constant #POINTS, used as the limit in DRAW ’s DO LOOP.

この構造により、アスタリスクの数を気にせずに表にアスタリスクを追加または削除できます。 Forthがこれを計算します。

定義ワードによるコンパイル時のファクタリング(要素分解)

同じ問題に対する一連のアプローチを調べてみましょう。関連アドレスのグループを定義してみます。以下は最初の試行です。

HEX 01A0 CONSTANT BASE.PORT.ADDRESS
BASE.PORT.ADDRESS CONSTANT SPEAKER
BASE.PORT.ADDRESS 2+ CONSTANT FLIPPER-A
BASE.PORT.ADDRESS 4 + CONSTANT FLIPPER-B
BASE.PORT.ADDRESS 6 + CONSTANT WIN-LIGHT
DECIMAL

アイデアは正しいですが、実装は醜いです。 ポートごとに変わる唯一の要素は、数値オフセットと定義されているポートの名前です。 他のすべてが繰り返されます。 この繰り返しは、定義ワードの使用を示唆しています。

より読みやすい次のアプローチでは、繰り返されるすべてのコードを定義ワードの「する(does)」部分にまとめます。

: PORT  ( offset -- )  CREATE ,
   \ does>  ( -- 'port) @ BASE.PORT.ADDRESS + ;
0 PORT SPEAKER
2 PORT FLIPPER-A
4 PORT FLIPPER-B
6 PORT WIN-LIGHT

このソリューションでは、これらの名前のいずれかを呼び出すたびに、「実行時に」オフセット計算を実行しています。 次のように、コンパイル時に計算を実行するほうが効率的です。

: PORT  ( offset -- )  BASE.PORT.ADDRESS + CONSTANT ;
   \ does>  ( -- 'port)
0 PORT SPEAKER
2 PORT FLIPPER-A
4 PORT FLIPPER-B
6 PORT WIN-LIGHT

ここで私たちは独自の「コンパイル時」の振る舞いを持つ、定義ワード PORT を作成しました。すなわち、オフセットを BASE.PORT.ADDRESS に追加して CONSTANT を定義します。

さらに一歩踏み込むこともあります。 すべてのポートアドレスが2バイト離れているとします。 この場合、これらのオフセットを指定しなければならない理由はありません。 以下の数値の並び

0 2 4 6

それ自体が冗長です。

次のバージョンでは、スタックの BASE.PORT.ADDRESS から始めます。 定義ワード PORT はこのアドレスを複製し、そこから定数を作り、それからスタック上にまだ残っているアドレスに2を加えます。それは次の PORT の呼び出しのためです。

: PORT   ( 'port -- 'next-port)  DUP CREATE ,  2+ ;
   \ does>  ( -- 'port)
BASE.PORT.ADDRESS
  PORT SPEAKER
  PORT FLIPPER-A
  PORT FLIPPER-B
  PORT WIN-LIGHT
DROP ( port.address)

最初のポートを定義する前にスタックの最初のポートアドレスを与えなければならないことに注意してください。そしてすべてのポートの定義を終えたら DROP を呼び出してまだスタックにあるポートアドレスを取り除きます。

最後に一言。ベースポートアドレスは変更される可能性が非常に高いため、1か所だけで定義する必要があります。 これは定数として定義する必要があるという意味ではありません。 ベースポートアドレスがこのポート名の用語集の外側で使用されない場合は、ここで数値を使用して参照してください。

HEX 01A0  ( base port adr)
  PORT SPEAKER
  PORT FLIPPER-A
  PORT FLIPPER-B
  PORT WIN-LIGHT
DROP

実装における反復アプローチ

この本の最初の部分で、反復アプローチについて説明しました。設計フェイズへの影響に特に注意を払いました。 今、私たちは実装について議論しているので、コーディングにおいてこのアプローチが実際どのように使用されるのかを見てみましょう。

ヒント

一度に問題の1つの側面のみに取り組むようにしてください。

与えられたxy座標で箱を描画または消去するためのワードをコーディングする仕事を委託されているとします(これは、「 コンパイル時のファクタリング(要素分解) 」という節で紹介したのと同じ問題です)。

私たちは、まずは箱を描く(draw)という問題に注意を集中します。箱を消すことは考えません。 私たちはこんなのを思い付くでしょう。

: LAYER   WIDE  0 DO  ASCII * EMIT  LOOP ;
: BOX   ( upper-left-x  upper-left-y -- )
   HIGH  0 DO  2DUP  I +  XY LAYER  LOOP  2DROP ;

これが正しく機能することを確認するためにこれをテストしたので、今度は同じコードを使って箱を消去(undraw)する問題に目を向けます。 解決策は簡単です。ASCII * のようにハードコーディングするのではなく、出力文字をアスタリスクから空白に変更したいのです。 これには、変数の追加と、変数の内容を設定するための読みやすいワードが必要です。 そして私たちは以下のようにします。

VARIABLE INK
: DRAW   ASCII *  INK ! ;
: UNDRAW   BL  INK ! ;
: LAYER   WIDTH  0 DO  INK @  EMIT  LOOP ;

BOX の定義はアプリケーションの残りと共に同じままです。

このアプローチは以下の文法を許します。

( x y ) DRAW BOX

または

( x y ) UNDRAW BOX

明示的な値から値を含む変数に切り替えることで、間接的なレベルを追加しました。 この場合、定義を大幅に長くすることなく、「逆方向」の間接参照を追加し、 LAYER の定義に新しいレベルの複雑さを追加しました。

一度に問題の1側面に集中することで、各側面をより効率的に解決できます。 それがまだ別の未試行のものによって隠されてなくて、テストされてないコードの側面でなければ、あなたの思考に誤りのある問題は簡単に判ります。

ヒント

一度に沢山変更しないで下さい。

新しい機能を追加したり何かを修正したりしながらアプリケーションを編集している間に、他のいくつかのものを同時に修正したくなることがよくあります。 私たちのアドバイスは「ダメ、絶対」です。

編集・コンパイルするたびに、できるだけ少ない変更を加えます。 次に進む前に、必ず各リビジョンの結果をテストしてください。 あなたは、とても短い間隔で何度も何度も編集・コンパイル・テストのサイクルが回せる事にびっくりすることでしょう。

一度に1つずつ変更を加えることで、機能が停止したときにその理由がわかります。

ヒント

ファクタリング(要素分解)の方法を先走って考えないで下さい。

一部の人々は、ほとんどのForthシステムが定義ワード ARRAY を含まないのはなぜだろうと疑問に思います。 この規則がその理由です。

ムーア は言います。

私はしばしば配列と呼ばれるもののクラスを持っています。 最も単純な配列は単にアドレスに添字を追加してアドレスを返すだけです。 あなたは以下のように宣言することで配列を定義できます。

CREATE X   100 ALLOT

それから、次のように言います。

X +

または、以下のように言うことも出来ます。

: X   X + ;

私にとって最もイライラする問題の1つは、それに特定のデータ構造の定義ワードを作成する価値があるかどうかを知ることです。 それを正当化するのに十分な実例がありますか?

複数の配列を使用するかどうかを事前に知ることはめったにありません。 だから私は ARRAY という言葉を定義しません。

2つの配列が必要だと気付いたら、問題は限界を迎えます。

3つ必要なら、それらがそれぞれ異なる問題である場合を除いて、問題の限界は明らかです。そして何がどのぐらい欲しいかは異なるでしょう。あなたはバイト配列、またはビット配列が欲しいのかもしれませし、境界チェックをしたいのかもしれませし、現在の長さを格納して最後に追加できるようにしたいのかもしれません。

私は改まって、「そのデータ構造をすでに利用可能なワードに合わせるために、バイト配列でよいものをセル配列にする必要がありますか?」と問います。

問題が複雑になればなるほど、普遍的に適用可能なデータ構造が見つかる可能性は低くなります。 本当に複雑なデータ構造が普遍的に利用されている例は非常に少ないです。 成功した複雑なデータ構造の一例はForth辞書です。 非常にしっかりした構造、素晴らしい汎用性、それはForthの至る所で使用されています。しかし、そういう例はまれです。

あなたが ARRAY というワードを定義することを選択した場合は、分解ステップは完了です。 後に戻って、すべてのワードから配列の概念を括り出します。そして、あなたは別のレベルの抽象化に進みます。

抽象化レベルの構築は動的プロセスであり、予測できるものではありません。

ヒント

今日は機能させ、明日は最適化してください。

このインタビューの日に、ムーアは市販のICを使ったボードレベルのForthコンピュータの設計作業を完了していました。 ボードを設計するためのツールキットの一部として、彼はボードのロジックをテストするためにForthでシミュレータを作成しました。ムーアは言います。

今朝、私はチップの説明とボード上のチップの配置を混ぜ合わせていることに気づきました。 現時点ではこれは全然便利ですが、同じチップを使用したい別のボードを考えると非常によろしくない。

私はここでの説明とそこでの使用法でそれを考慮に入れるべきでした。 それから私はチップ記述言語を作ったでしょう。 ええ、これをしていた時、私はそのレベルの最適化には興味がありませんでした。

そのときに思いついたとしても、「後にする」と言ってから、自分がしていたことを先に進めていたでしょう。 当時、最適化は私にとって最も重要なことではありませんでした。

もちろん、私は物事をよく考慮しようと試みています。しかし、何かをするのに良い方法が見つからないようであれば、「それをうまく機能させるようにしましょう」と言います。

私の動機は怠惰ではありません。私は現時点の予測に対して予想外のアクシデントが入るのを知っています。今これを最適化しようとするのはばかげています。 全体像が目の前に表れるまでは何が最適なのかは分かりません。

この節の観察は、情報隠蔽や変更される可能性のある要素の予測についてこれまでに述べたことと矛盾しないようにする必要があります。 優れたプログラマは、組み込みの変更可能性の費用と、後で必要に応じてものを変更する費用のバランスを取ろうとし続けます。

これらの決定には経験が必要です。 しかし以下の原則があります。

ヒント

複雑さを増すのではなく、情報を整理することで、変化する可能性があるものを予測します。 現在の反復を機能させるために必要な場合にのみ複雑さを追加してください。

要約

この章で、私たちはファクタリング(要素分解)のさまざまな手法と基準について議論しました。 また、反復アプローチが実装フェイズにどのように適用されるのかも調べました。

参考文献

[stevens74-6]W.P. Stevens, G.J. Myers,and L.L. Constantine, IBM Systems Journal , vol. 13, no. 2, 1974, Copyright 1974 byInternational Business Machines Corporation.
[miller56]G.A. Miller, "The Magical Number Seven, Plus orMinus Two: Some Limits on our Capacity for Processing Information," Psychol. Rev ., vol. 63, pp. 81-97, Mar. 1956.
[harris83]Kim R. Harris, "Definition Field AddressConversion Operators," Forth--83 Standard , Forth StandardsTeam.