読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

gforthでパイプ処理(と、Forthらしいコードの模索)

標準入力に何かしらフィルタをする方法を調べてみた。マニュアルを見ると、バッファを用意してバッファ 長さ stdin read-fileを繰り返してやればいいみたいだ。

read-filec-addr u1 wfileid -- u2 wiorというスタックエフェクトで、wiorが何の略かよくわからないけどおそらくthrow用のエラー番号、u2は読み込んだ長さみたいだ。

というわけでこういうコードでcat的な処理ができる。

create buff 10 allot

: cat  ( -- )
   begin
     buff dup 10 stdin read-file throw dup  ( -- buff u2 u2 ) 
   while  ( 読み込んだ文字数が0だったら終了 )
     type
   repeat drop ;

cat bye

cat.fsなどに保存して、cat cat.fs | gforth cat.fsで自身を出力することができる。

read-lineで1行ずつ処理する場合、長さ0の行もあるので、スタックエフェクトはc-addr u1 wfileid -- u2 flag wiorとflagが追加されている。それを使って処理するのでこうなる。

create buff 256 allot

: cat  ( -- )
   begin
     buff dup 256 stdin read-line throw   ( -- buff u2 flag ) 
   while
     type cr
   repeat drop ;

cat bye

行全体を読み込むのでバッファを多めに取ること、throwのあとflagを使ってそのままwhileで判定すること、改行コードは読み込まれないのでcrで改行を表示するところが違う。

Forthらしいコードを模索する

Forthらしいコード、というと語弊がありそうだけど、Forthだからこそできるような、読みやすくて改善しやすい書き方を考えてみる。

メインとして、ワード(のxt)を渡して関数渡し的に使う処理が書きたい。while ... repeatの間をそう書きなおせば、汎用的なフィルタ処理にできそうだ。

とりあえずcatをそう書きなおしてみる。ついでに少しfactoringしてみる。

create buff 256 allot

: read-line-stdin  ( buff max -- buff len )
  over swap  ( -- buff buff max )
  stdin read-line throw  ;

: pipe-filter  ( xt -- )
   \ xt ( addr len -- ) を使い、標準入力を処理していく
   begin
     dup buff 256 read-line-stdin ( -- xt xt buff len flag )
   while 
     rot execute  ( xt xt buff len -- xt )
   repeat ;

: fcat  ( addr len -- )  type cr ;

' fcat pipe-filter  bye

xtの実行に置き換えただけなので、特に難しいことは無い。

じゃあ次は行番号を表示してみよう。マニュアルによると<# #s #>というコードを使えば数字を文字列に変換できるみたいだ。

数値はDouble precisionという形式を使うので、ワードs>dで変換して使う。300 s>d <# #s #> typeというコードで300という文字列を表示できる。

で、ここからちょっと迷走が始まった。Thinking Forthを思い出しながら書いたんだけど、今までの経験から次のような考えで書いていた。

「paddingはいろんな詰め文字列に対応できるように、文字列を受け取ってそれを繰り返し表示するワードを作ってそれを使おう。右詰めもあるかもしれないから、ワード名はleft-paddingだ。条件を表すコードと早めのreturnをするコードも細かく分けよう。」

その結果スタックエフェクトが複雑になってローカル変数を使わざるを得なくなり、じゃあもうローカル変数も普通に使うか、とスタックエフェクトが複雑になることを許していって……次のような混乱したコードができた。

create buff 256 allot

: read-line-stdin  ( buff max -- buff len )
  over swap  ( -- buff buff max )
  stdin read-line throw  ;

: pipe-filter  { xt -- }
   \ xt ( addr len -- ) を使い、標準入力を処理していく
   begin
     buff 256 read-line-stdin ( -- buff len flag )
   while
     xt execute  ( buff len -- )
   repeat ;

: fcat  ( addr len -- )  type cr ;

: rep-type  ( addr u n -- )
   0 ?do 2dup type loop 2drop ;

: less?  ( a b -- a-b ? )  -  dup 0 <= ;
: less-guard  ( a-b ? -- ?a-b )  less?  if drop r> exit then ;
: left-padding  { len addr u n -- }
   n len less-guard  { rep }
   addr u rep rep-type ; 

: 0s  s" 0" ;
: sp  s"  " ;
: 0pad  { len n -- len }  len 0s n left-padding  len ; 
: spad  { len n -- len }  len sp n left-padding  len ;

0 variable linenum
: count-line ( -- n )  1 linenum +!  linenum @ ;
: num>str  ( n -- addr u )  s>d <# #s #> ;

: flinenum  ( addr len -- )
   count-line num>str 4 spad type space  fcat ;

' flinenum pipe-filter  bye

なんだろう、拡張性がありそうで無さそうなコードだ。今までなら「ちょっと変だけど拡張性のためだ」と小さい違和感で流してたんだけど、Thinking Forthの思想とコードを読んだあとだと違和感が無視できなくなった。

早すぎる最適化を避けるべきなのはおそらくどの言語を使っててもそうなんだろうけど、Forthだとそれに加えて「早すぎる柔軟化」も避けたほうがいい気がする。xtを渡すってアイディアもそうかもしれないんだけど、今回はxt渡しが目的なのでそのままにしておく。

それから、Forthのコードは、コードでは無く命令のまとまりとその辞書である、と意識した方が良さそうだ。ワードを名付けるとき、そのワードが何をするかも大事なんだけど、何を生み出すか、名詞を重視すべきかもしれない。

更に、人が読むための辞書なので、コードのレイアウトも気にする。Forthの場合は、インデントされた長めのワードより、1行の定義が整頓されて並んでた方が読む気になりそうだ。

以上から、拡張性を必要以上に考えず、ワード名も短い名詞を意識して、おまけにスタックエフェクトなどの縦の並びを整えてみた。

create buff 256 allot

: read-line-stdin  ( buff max -- buff u flag )
  \ flagは行を読み込めたかどうか(長さ0の行もある)
  over swap  ( -- buff buff max )
  stdin read-line throw  ;

: pipe-filter  ( xt -- )
   \ xt ( &s u -- ) を使い、標準入力を処理していく
   begin
     dup  buff 256 read-line-stdin  ( -- xt xt buff u flag )
   while 
     rot execute                    ( -- xt )
   repeat ;

: f-cat  ( &s u -- )  type cr ;

variable pad-width
: digit    ( n --       )  pad-width ! ;
: gap      ( u -- u gap )  pad-width @ over - ;
: padding  ( u -- u     )  gap  dup 0 <=  if drop exit then  spaces ; 

variable lines
: count-line  (   -- n    )  1 lines +!  lines @ ;
: num>str     ( n -- &s u )  s>d <# #s #> ;
: line-count  (   -- &s u )  count-line num>str ;
: f-linenum   ( &s u --   )  line-count padding type space  f-cat ;

4 digit ' f-linenum pipe-filter  bye

最後の行は「4桁の行番号をつけてフィルタする」みたいに読める、気がする。

なんとなく好感が持てるコードになったような、気がする。

先は長い。

広告を非表示にする