hkoba blog

プログラマーです。プログラミング言語ミーハーです。ツッコミ歓迎です。よろしくどうぞ(能代口調)

私信:つい同期と自分を比較してしまう、新人さんへ

先日お話したこと、まとめておきますね。 (以下は医学的な裏付けのある話ではなく、私の個人的な体験に基づくアドバイスです)

考えることも体力を使う、と認識する

まず『(全力で)思考する・考える』こと自体が『脳を疲れさせる』という点に気づくことが大事です。脳に疲れが溜まると、筋肉と同じで、平常時のようには動かなくなっていきます。 (ゲームの HP や MP みたいに考えても良いかもしれませんが、どちらかと言えば、 疲労ゲージ ・ダメージゲージが溜まっていく風に、私は感じます)

これはつまり、『同期と自分とを比較する』ことに脳を使うのも、 『プログラミングの演習問題を一心不乱にこなす』ことに脳を使うのも、 脳を一定量、疲れさせる行為であるという点は、同じだ、ということです。 限られた資源である思考体力を、どちらに割り当てるか、という問題と考えて良いでしょう。

もっと言うと、(私の個人的な経験ですが)、 同じことをグルグルと結論も無しに考え続けた時、脳は物凄く消耗する、 という問題も有ります。無理矢理でも思考に割り込んで、結論を出して思考ループを停止させること、 そして残った体力で現実を変える行動を始めること、その切り替えこそが大事です。

もちろん人間なので、自分より進みの速い同期とつい比較してしまうことは、 ゼロには出来ないかもしれません。でも、そればかり見ていては 肝心の演習に使う脳の体力まで消耗してしまい、本末転倒なのです。

ですから、 如何にして『思考を切り替える』か が、今のあなたの人生のテーマ、 課題、天の?試練なんだ、と考えては如何でしょうか?

思考を切り替える技を身につける

私のお勧めは、頭の中身をメモに書き出すこと、です。 頭の中だけで考えると歯止めがかからないので、代わりに、 実際に紙やコンピューター上に考えをメモや日記として書き出す習慣を 付けましょう、ということです。

書き出すことにはいくつかメリットが有ります。

  • 思考が生む疲れを知覚できるようになる(沢山書くと体が疲れるので、気づきやすくなる)
  • 書く速度に合わせて思考がスローダウンし、その分、脳の疲れが抑えられる (ループ時は消耗を抑えることが大事)

更に、後でメモを読み返すことで得られる効果もあります。

  • 繰り返しに気づける、飽きる
  • 自分の考えていること自体を客観視しやすくなる
    • 事実と、自分による評価とを、分ける・整理するきっかけになる

文章として書く以外に、マインドマップのように、図的に書き出す方法も、良いかもしれません。

書き出したら、それについて考えることは一旦やめる(よう努力する)のが大事です(多分)。また同じことを考えてしまうかもしれませんが、そこで自分を過度に責めず、はいはい、程々にね…くらいにして、思考の切り替えを少しずつ練習してみて下さい。

最後に

それでも挫けそうな時が有ったら…

私の心の支えを貼っておきますね。

snit で Tcl/Tk が少し楽に書けようになるのでご紹介

この記事はOSS紹介 Advent Calendar 2017 の 13日目の記事です。

qiita.com

概要

snit という OOP ライブラリを使えば Tcl/Tk のプログラミングも大分マシになる…そういう話を書きます。(今どき Web じゃないの? Electron じゃないの?というツッコミは無視します ;-) 古臭い Tcl/Tk 製 GUI wrapper を snit で書き直すとどうなるか、具体的なコードを挙げてご紹介します。(紹介記事なのに長すぎてすみません) 記事のコードは Tcl 8.5 以上を前提としますが、 snit 自体はそれ以前の Tcl/Tk でも利用可能です。ソースは gist にもあります。

なお、ここでは Tcl/Tk 自体の解説は省略しますが、日本語だと もっと Tcl/TkTcl/Tk入門はじめようWidget の節がとっつきやすくて良いでしょう。英語で良ければ Tcler's Wiki も貴重な情報源です。

目次

  1. はじめに - 今さら何に Tcl/Tk を使うのか
  2. snit とは
  3. 生の Tcl/Tk で GUI を作ると、何が辛いか
  4. snit で書くと、どう改善されるか
  5. 最後に

はじめに - 今さら何に Tcl/Tk を使うのか

社内の Unix サーバー上の CLI ツールに GUI を用意して、あまり Unix に慣れていない普通の社員でも簡単に・間違えずに呼び出せるようにしたい、という需要は、しばしば有るでしょう。また、Unix に慣れた人にとっても、作業の種類によっては、GUI が助けになるケースが有ります。

もし GUI の提供を受ける対象ユーザーが社外の顧客であったり、社員のみであっても人数が多い場合は、 Web ブラウザで操作出来るツールにするのがベストでしょう。ただし Web から使用できるようにする場合は認証の実装がそれなりに工数を食うのではないでしょうか。その上、その Web アプリが (基本的に) 全部同じユーザ権限で動作することになるので、 Unix 側でのユーザー管理を活かせない、という点も残念です。(suexec するならともかく)

それに対して GUI の提供相手が全て社内の人間で、その Unix サーバーに SSH なりでログインするアカウントを持っているケースであれば、別の選択肢も候補に入ってきます。SSH の ForwardX11 や RemoteDesktop です。

GUI を書く時に SSH ForwardX11 や RDP を使う最大のメリットは、認証とユーザ・権限管理のコードを自分で書く必要が無いことです。(それらは Unix へのログインで済んでいる) ですから、真に書きたい処理に集中することが出来ます。

そのようなユースケースで、特に GUI の見栄えを要求されないケースについては、一つの選択肢として Tcl/Tk は今でも検討に値するのではないでしょうか?

描画の応答性についても、LAN であれば X11 はまだまだ使える速度が出ます。特に Tcl/Tk の質素(!)な GUI であれば、十分な速度が出るでしょう。

.oO(…余談ですが、 VS Code とか Gnome のファイラーを ForwardX11 すると、LAN 環境でも異様に遅い気がします。何とかならんのでしょうか…特に Mac の XQuartz や Windows の MobaXterm からだと遅さが特に…)

snit とは

snit とは Tcl/Tk で委譲(delegation)に基づくオブジェクト指向プログラミングを行うためのライブラリです。Pure Tcl で書かれているため、Tcl/Tk が導入済みならすぐに使えます。古くから tcllib に同梱されているので、apt や yum/dnf でも簡単にインストール出来ます。snit を詳しく学びたい方はチュートリアル形式の公式 FAQ snitfaq を参照して下さい。

抄訳(抜粋)ですが、snitfaq の What is Snit を訳してみます。

Snit は抽象データ型と megawidgetを定義するための、pure Tcl で書かれたフレームワークです…Snit の一番の目的は、オブジェクトどうしのグルー(糊)になることです…Snit は理論的な純粋さやコードの短さをめざしたものではありません。強力なことを簡単かつ一貫性の有る形で書くことを、それについて深く悩むことなく出来るようにすること、そうして自分のアプリケーションを作ることに集中できるようにすることが目的です。… (※. widget を組み合わせて作った widget を指す tcltk 用語)

生の Tcl/Tk で GUI を作ると、何が辛いか

Tcl/Tk で GUI を組む時に悩むこと

  1. GUI のレイアウト・構造を改良しようとした時に、コード変更の影響範囲を局在化するのが面倒
  2. 名前空間をデータコンテナとして使う必要が有り、その規約を考えるのが面倒。

snit はこの2つ両方に役立ちますが、今回は前者に焦点を当てて解説します。

v1. 最初は widget path 直書きでも苦にならない

はじめに、 ls コマンドを実行するボタンと、その実行結果を表示するテキスト領域だけからなるスクリプトを書いてみます。

f:id:hkoba501:20171209202808p:plain

至って平凡な、雑な GUI wrapper です。

#!/usr/bin/wish

# ボタンを作る
pack [button .b1 -text "Push Me!" -command [list Run ls -C]]

# テキスト領域を作る
pack [text .console -height 8] -fill both -expand yes

# callback 手続きを実装する
proc Run args {
    .console insert end [exec {*}$args]\n
    .console see end
}

v2. widget path 直書きではレイアウト変更が辛くなる。どうするか。

さて、このツールのテキスト領域にスクロールバーを足したくなりました。ここでは BWidgetScrolledWindowを用います。

f:id:hkoba501:20171209202816p:plain

コードはどう変わるでしょうか? .console を呼び出していた個所を全て .sw.console に書き換える必要がありました。 (問題が目立つよう、 敢えて変数を使わずに 書きます)

#!/usr/bin/wish

# ボタンを作る
pack [button .b1 -text "Push Me!" -command [list Run ls -C]]

# テキスト領域を作る(スクロールバー付き)
package require BWidget

ScrolledWindow .sw
.sw setwidget [text .sw.console -height 8]
pack .sw -fill both -expand yes

# callback 手続きを実装する
proc Run args {
    .sw.console insert end [exec {*}$args]\n
    .sw.console see end
}

GUI ツールにとってレイアウト変更は茶飯事なので、レイアウト変更に伴うコードの変更をどのように管理するかは重要です。

(snit などの OOP ライブラリを使わない) 古典的な Tcl/Tk アプリでは、

  1. (callbackである) Run の引数仕様を変え、第一引数に widget path も渡すようにする
  2. global 変数を使う

の二通りの解決策があります。

引数仕様を変える方法

Run の引数にテキスト領域を渡すためには、widget の作成順序を変えて、ボタンを後から作る必要があります。その影響で packレイアウトのオプションも変更する必要も生じます。

(※本当は、この程度のケースでは作成と widget path の決定を分離して、作成順序自体は変えない、という方法も有ります。ですが、その方法が常に使えるとは限らないですし、読みにくくもなるので…)

#!/usr/bin/wish

# テキスト領域を作る(スクロールバー付き)
package require BWidget

ScrolledWindow .sw
set myConsole [text .sw.console -height 8] 
.sw setwidget $myConsole
pack .sw -fill both -expand yes\
    -side bottom

# ボタンを作る
pack [button .b1 -text "Push Me!" -command [list Run $myConsole ls -C]]\
    -side top

# callback 手続きを実装する
proc Run {myConsole args} {
    $myConsole insert end [exec {*}$args]\n
    $myConsole see end
}

このように、コードの変更量は決して少なくありません。しかも、Run が参照する widget が増える度に、引数仕様も変わることになります。

global 変数を使う方法

前節で見たように、引数として widget path を渡す方法はコードの変更量が多くなりがちなため、(部品として使うことを意図しない)tcltk アプリケーションでは、私の主観では global変数を用いて widget path の抽象化を行うものが主流のようです。

ここではテキスト領域を myConsole という名前の global 変数 に格納することにします。

#!/usr/bin/wish

# ボタンを作る
pack [button .b1 -text "Push Me!" -command [list Run ls -C]]

# テキスト領域を作る(スクロールバー付き)
package require BWidget

ScrolledWindow .sw
set myConsole [text .sw.console -height 8] 
.sw setwidget $myConsole
pack .sw -fill both -expand yes

# callback 手続きを実装する
proc Run args {
  global myConsole
  $myConsole insert end [exec {*}$args]\n
  $myConsole see end
}

最初のバージョンとの差は、比較的少なめと言えるでしょう。

ただし、 global 変数と聞いて身構える人も多いかもしれません。その予感はとても正しいものです。地獄へようこそ。

v3. 部品を増やす度に、各手続きにも global 宣言が増えていく

次はコマンドの成功・失敗をボタンの近くにラベルで表示するようにしてみましょう。(ボタンとラベルはツールバー風に横向きに並べたいので、フレームを作ってその中にボタンとラベルを置くことにします)

f:id:hkoba501:20171209202823p:plainf:id:hkoba501:20171209202830p:plain

このコードで注目するべき点は Run callback の実装の先頭に並ぶ global 宣言です。もし callback が Run2, Run3 ... と増えた場合、それぞれに同様の global 宣言を並べる必要が有ります。極端に言えば callback の個数 x 参照する widget の個数だけ、global 宣言を書く必要が有るのです。嫌ではないですか?私は嫌です。

#!/usr/bin/wish

# ツールバー用のフレームを作る
pack [set bf [frame .bf]] -fill x

# OK/NG を出すラベルを作る
pack [set myOkNg [label $bf.l1 -text "OK"]] -side left

# ボタンを作る
pack [button $bf.b1 -text "Push Me!" -command [list Run ls -C]]  -side left

# テキスト領域を作る
package require BWidget

ScrolledWindow .sw
set myConsole [text .sw.console -height 8] 
.sw setwidget $myConsole
pack .sw -fill both -expand yes -side top

# callback 手続きを実装する
proc Run args {
    # XXX: 各 callback に、参照する widget 全ての global 宣言が必要になる→増えると大変
    global myConsole
    global myOkNg

    set err [catch {exec {*}$args} result]

    $myConsole insert end $result\n
    $myConsole see end

    if {$err} {
        $myOkNg configure -text "NG" -background red
    } else {
        $myOkNg configure -text "OK" -background gray85
    }
}

global 頼みだと複数インスタンスを作れない

global を使う抽象化の、実用上の最大の問題がこれです。例えばこの GUIタブ で複数個、同時に使えるようにしたい!と思った時、全く打つ手がありません。

snit で書くと、どう改善されるか

では実際に、 v1〜v3 のコードをそれぞれ snit で書き直した上で、 v4 としてタブ化にも取り組んでみます。

v1. ボタンとテキスト領域のみ

ここでは snit の snit::widget コマンドを使って、 myapp という名前の自作 widget として GUI 自体を定義します。snit::widget は何個でもインスタンスを生成することが出来るので、タブ化したい!といった欲求にも自然に対応できます。snit::widget{...} の中には method 定義や variable 宣言、option 宣言などを書くことが出来ます。

global 宣言の代わりになるものは、 snit::widget 定義の中の variable 宣言です。 variable 宣言を行った変数は、全ての method の中で参照出来るようになります。

constructor はこの widget を実体化する時に一度だけ呼ばれる手続きです。手っ取り早くは GUI の構築コードをここに入れてしまえば良いでしょう。constructor の中で自分自身を参照するには $self を使います。ただし widget path だけは $win を使う必要が有ります。

callback の呼び出し方は Run ls -C の代わりにメソッドとして $self Run ls -C のようにします。その実装は snit::method で与えます。

#!/usr/bin/wish

# snit をロードする
package require snit

# 自作widget `myapp` を定義する
snit::widget myapp {

    # テキスト領域を保持するためのインスタンス変数を宣言する
    variable myConsole

    # widget の実体化の際に呼ばれる手続きを実装
    constructor args {
        # ボタンを作る
        pack [button $win.b1 -text "Push Me!" -command [list $self Run ls -C]]

        # テキスト領域を作る
        set myConsole [text $win.console -height 8]
        pack $myConsole -fill both -expand yes
    }

    # callback メソッドを実装する
    method Run args {
        $myConsole insert end [exec {*}$args]\n
        $myConsole see end
    }
}

# 自作 widget `myapp` を .win として実体化・レイアウト
pack [myapp .win] -fill both -expand yes

v2. スクロールバーを追加

生 Tcl/Tk の時と同様に BWidget の ScrolledWindow を使ってスクロールバーを加えてみます。ですが、既に snit::widget の基本の書き方に従って記述してあったので、変更は constructor 内のみで済みます。

    constructor args {
        # ボタンを作る
        pack [button $win.b1 -text "Push Me!" -command [list $self Run ls -C]]

        # テキスト領域を作る(スクロールバー付き)
        package require BWidget
        set sw [ScrolledWindow $win.sw]
        pack $sw -fill both -expand yes

        set myConsole [text $sw.console -height 8] 
        $sw setwidget $myConsole
    }

v3. OK/NG ラベルを追加

成功・失敗を表示するためのラベルをメンバー変数 myOkNg にすれば、先程の global 地獄は解消出来ます。

snit::widget myapp {

    # ここで宣言した変数は全メソッドでアクセス可能に
    variable myOkNg
    variable myConsole

    constructor args {
        # ツールバー用のフレームを作る
        pack [set bf [frame $win.bf]] -fill x

        # OK/NG を出すラベルを作る
        pack [set myOkNg [label $bf.l1 -text "OK"]] -side left

        # ボタンを作る
        pack [button $bf.b1 -text "Push Me!" -command [list $self Run ls -C]] -side left

        # テキスト領域を作る(スクロールバー付き)
        package require BWidget
        set sw [ScrolledWindow $win.sw]
        pack $sw -fill both -expand yes

        set myConsole [text $sw.console -height 8] 
        $sw setwidget $myConsole
    }

    # callback メソッドを実装する
    method Run args {
        set err [catch {exec {*}$args} result]

        $myConsole insert end $result\n
        $myConsole see end

        if {$err} {
            $myOkNg configure -text "NG" -background red
        } else {
            $myOkNg configure -text "OK" -background gray85
        }
    }
}

v4. タブ化

最後に、生の tcltk + global 路線ではたどり着けなかったタブ化です。既に myapp 自体は複数個実体化することが可能になっています。後は実行するコマンドをインスタンス毎にオプションで設定できるようにするだけです。

f:id:hkoba501:20171210135045p:plain

snit::widget にオプションを追加するには option 宣言を使います。

snit::widget myapp {

    # widget オプションの宣言。 options(-exec) でアクセス可能
    option -exec [list ls -C]
    ...

    constructor args {
        ...

        # 実体化時の引数列を options 配列へ代入
        $self configurelist $args
    }

    # callback メソッドを実装する
    method Run args {

        # 引数が渡されない時はオプション `-exec` の内容を用いる
        if {$args eq ""} {
            set args $options(-exec)
        }

    ...

myapp に -exec オプションを渡せるようになったので、タブ毎に異なるコマンドを実行させることが出来ます。

# タブの親
pack [ttk::notebook .win] -fill both -expand yes

.win add [myapp .win.n1 -exec "ls -C /"] -text "/ "
.win add [myapp .win.n2 -exec "ls -C /home"] -text "/home "
...

おまけとして、この例では、このファイル自体を直接コマンドとして起動することも、ライブラリとして別の Tcl/Tk スクリプトに読み込んで使うことも出来るようにしてあります。

if {![info level] && $::argv0 eq [info script]} {

    # このファイルを実行した時だけ、GUI を実体化
    ...

} else {
    # source した時は実行しない。
}

コード全体

#!/usr/bin/wish

package require snit
package require BWidget

snit::widget myapp {

    # widget オプションの宣言。 options(-exec) でアクセス可能
    option -exec [list ls -C]

    # ここで宣言した変数は全メソッドでアクセス可能に
    variable myOkNg
    variable myConsole

    constructor args {
        # ツールバー用のフレームを作る
        pack [set bf [frame $win.bf]] -fill x

        # OK/NG を出すラベルを作る
        pack [set myOkNg [label $bf.l1 -text "OK"]] -side left

        # ボタンを作る
        pack [button $bf.b1 -text "Push Me!" -command [list $self Run]] -side left

        # テキスト領域を作る(スクロールバー付き)
        set sw [ScrolledWindow $win.sw]
        pack $sw -fill both -expand yes

        set myConsole [text $sw.console -height 8] 
        $sw setwidget $myConsole

        # 実体化時の引数列を options 配列へ代入
        $self configurelist $args
    }

    # callback メソッドを実装する
    method Run args {

        # 引数が渡されない時はオプション `-exec` の内容を用いる
        if {$args eq ""} {
            set args $options(-exec)
        }

        set err [catch {exec {*}$args} result]

        $myConsole insert end $result\n
        $myConsole see end

        if {$err} {
            $myOkNg configure -text "NG" -background red
        } else {
            $myOkNg configure -text "OK" -background gray85
        }
    }
}

if {![info level] && $::argv0 eq [info script]} {

    # このファイルを実行した時だけ、GUI を実体化
    # 起動時の引数として渡されたディレクトリ毎に、タブを作る

    # global 変数を無駄に増やさないため, apply を使う
    apply {{{dir "/"} args} {
        
        # タブの親
        pack [ttk::notebook .win] -fill both -expand yes
        
        foreach dir [list $dir {*}$args] {

            # タブの中に `myapp` を実体化
            .win add [myapp .win.n[incr i] -exec "ls -C $dir"] -text "$dir "
            # XXX: タブの最後の文字が隠れるケースが有ったので、末尾にスペースを足した:-<
        }

    }} {*}$::argv

} else {
    # source した時は実行しない。
}

最後に

本当は html + css で Tcl/Tk を書く方法とか、 windows だと X11 を使う代わりに putty と sshcomm の組み合わせを使う方法の話とかも有るのですがそれは別の機会に、ということで…

こんな長い記事に最後までお付き合い頂き、ありがとうございました。

私信:CS にも過渡現象論みたいな位置づけの理論があると良いのかも?みたいな…

この記事は、この方のつぶやき

に対する、@pokarim さんの、

この辺りのツイートに、私がこう、ぽやん…

と感想をぶら下げて始まった対話の、お返事です。

ぽかりむさんへ

私の考えがどんなものか、なぜその考えに至ったか(特に Immutability についての考え方の由来)、それに基づいて行動した結果どんな経験を得られたか、少しお話させて下さい。(結論は無いです)

まず私の考えを改めて文章にしますと、

『電気工学では、定常状態の理論と、過渡現象の理論を別立てに分けている。この結果、それぞれ定常状態の理論は単純で初等数学でも理解しうるものになり、過渡現象の理論は複雑な現象を誤魔化さずに記述し切ることが可能になっている』

『同様の分け方が、プログラミングや計算機科学の世界にも、有って良いのではないか。何故なら、理論とはモデル化であり、人の理解を助けるためののものであるから。一つのモデルで最終状態と途中の変化を両方記述しようとして、かえってモデルが複雑化して理解しづらくなったら、本末転倒』

『特に、Immutable な世界と、State の Mutation を取り扱う世界に関して、理論を分けて持つことが、同様のメリットを生むのではないか』

です。

なぜこの考えに至ったか

源流らしきものは2つありました。

一つ目は25年ほど前、電気工学科の(落第寸前の)学生だった頃のこと。OPアンプの負帰還増幅回路の動作を習う時に、教授から示唆された仮説です。雑でおぼろげな記憶ですが、

『そもそも人間は時間変化する現象を(言葉だけで)イメージ化して理解することが苦手なのだ。負帰還増幅回路を理解しようとする時も、「入力が増えて、増幅された出力が出て、それが遅延されて入力に入って…」のように時間変化に沿って理解しようとすると、かえって難しくなるのだ。それよりも、「- 端子と + 端子の入力の値が最終的に一致するように動く」というゴール状態から理解する方が優しい』

といったものでした。(当時は、ほえ〜、そういうものか〜、たしかに逐次で追うのは大変そうだな〜、位の受け止め方でした) 最終状態の不変式で理解するのが楽だ、という考え方だったのかもしれません。

もう一つは院の輪講で、確か…タネンバウムの分散システム関連の書籍を学んだ際に触れた、Immutability の効能の話です。

(分散)並列計算をまともに(?)機能させるためには、レース状態の入り込む個所を減らす・無くすことが大事で、そのためには(並列システム間でやり取りされる)public/published な値を全て Immutable とするのが最も単純で効果的である…

みたいな議論だった気がします(用語や論旨は脳内で補っているので雑です。書籍にそう書いてあったかどうか自体、ちょっと怪しいです。輪講の中での議論で出た話題だったかもしれません)。値は Immutable にした上で、更新はディレクトリ(インデックス)上の参照の差し替えで表現する、分散ファイルシステムなどでオーソドックスなやり方の話です。(同時期にデータベースの授業でトランザクションの話も聞いていたので、その影響もありそうです)

どんな経験が得られたか

この 20年ほど、仕事で私がデータ保存を伴うプログラムを作る時は、原則として Immutable なレコードの追記型ストア+その上のインデックス、という構成を取ってきました。(と言っても、誇れるほどの仕事の幅はないのですが…) インデックスの貼り方に頭を使う必要はあるものの、大事な一次データが絶対に消えない(最悪人力でサルベージできるし、インデックスもログからリビルド出来る)安心感は、大きかったと感じています。ですから、(システムから観察できる published な) 値を Immutable にすることのメリットは大である、と考えます。

それに対して過渡的な現象、状態の変化の過程をどう記述するべきかですが、変化の記述にまで Immutability を追求すべきかどうかについては、正直、そんなに拘らなくても良いのでは?と感じております。(最初に挙げた、published な値を Immutable にすることの、実用的なメリットと比べて、です) マルチスレッドプログラムで言えば、スレッドのスタックに乗っているprivate で一時的な値と、そこから(共有メモリやチャネルを使って) 他のスレッドから見えるように公開した値では、扱いを変えてよいのでは、という考えです。

もちろん、プログラムのモジュール性を向上させるために一時的な値であっても破壊を避ける、読みやすさを助けるために変数の使い回しを避ける、という話はあるでしょう。しかし、いずれにせよ、マシンのレジスタは使いまわされているし、L1, L2.. キャッシュも使いまわしてこそ性能が出るものです。命令のスケジューリングと記憶域の使い回しを、人の書くプログラムから完全に捨象せんとする努力が、(尊いことでは有るけれども) メリットだけなのか (デメリットはないのか?) という、立ち止まって振り返ることも大事なのではないか、そのように考えています。

間接的なヒントとして…10年前の私は SQL を書くのが嫌いで、ORM を使うべきだと考えていました。その後、複雑なクエリを ORM に吐かせることに疲れ、かつ SQL を書く力も上がった今では、 ORM を使うより SQL を書くほうが手堅い、と感じるようになっています。

結論なしの尻切れトンボですが、あまりお待たせするのも申し訳ないので、このくらいで終わりにします。ぽかりむさんの考えとは違うところもありそうですが、何かのヒントにでもして頂ければ幸いです。ではでは〜♪

SSLのクライアント証明書の扱いについて誤解を正して貰えて有難かった話

SSLのクライアント証明書を使って接続相手の認証まで行いたいケースで、 クライアント証明書が self signed certificate だと危ないのでは?という疑問を呟いたら @satoh_fumiyasu さんに突っ込んでもらえて、私の誤解を正してもらえました。…誤解したままだったら…と考えると、肝が冷えます。さとうさん、有難うございました。

その後、実際に postgresql で試してみた所、CNだけ同じの成り済ましクライアント証明書で接続した時はこんなエラーが出る、と確認できました。

psql: SSLエラー: tlsv1 alert unknown ca

じこひはん

クライアント側も CA として扱う、という発想が出てこなかったのが、自分の頭の硬い所だったなぁと。お互いがお互いを Certificate Authority として信頼する…おお、peer to peer だ。どうも私の発想はサーバーに偏りすぎてたようです…

ssh 先で sudo 実行するコマンドに渡すワンライナーを zsh に quote させる

いつもの通り、ツッコミ歓迎です。

その quote, zsh に任せると幸せ…かも??

笹田さんのこのツイを見て、おっと zsh 宣伝チャンス、と。

zsh には変数展開時に結果を自動で quote してくれる (q), (qq), (q-) ... という機能があります。

この機能を使って、 ssh 先の sudo に渡すスクリプトの quote を試みます。

(ローカル側は zsh ですが、 リモート側は /bin/sh で OK なのが味噌です)

まず最初の echo の例

% cmds=(
  "echo a b"
  "echo c d"
)

% ssh -t localhost sh -x -c ${(qqj/&&/)cmds}
+ echo a b
a b
+ echo c d
c d
Connection to localhost closed.
%
  • ssh に渡す ${(qqj/&&/)cmds} は以下の2つの組み合わせ
    • (qq) による自動 quote
    • (j/&&/)join("&&")

ssh に渡される実際のコマンドラインを知りたい場合は以下のようにすると良いです。

% print -R ssh -t localhost sh -x -c "${(qqj/&&/)cmds}"
ssh -t localhost sh -x -c 'echo a b&&echo c d'
% print -l ssh -t localhost sh -x -c "${(qqj/&&/)cmds}"
ssh
-t
localhost
sh
-x
-c
'echo a b&&echo c d'
%
  • -R は escape の抑制
  • -l は引数を一行ずつ出力する

ssh 先の sudo に渡すワンライナーzsh に quote させる

次は以下の二行を一度の ssh に渡すよう、quote してみます。

sudo ruby -e 'puts "Hello uid=#{Process.uid}"'

sudo ruby -e 'puts "Again uid=#{Process.uid}"'

上記の2つのワンライナーを配列に格納するには、 各々を '...' で quote する必要が有るので、

cmds=(

  'sudo ruby -e '\''puts "Hello uid=#{Process.uid}"'\'''

  'sudo ruby -e '\''puts "Again uid=#{Process.uid}"'\'''

)

これを手で入力すると大変ですが、幸い zsh には現在の入力を丸々 一本の文字列へと quote するコマンド M-' quote-line が用意されているので、これを使いましょう。 範囲指定版の M-" quote-region もあります。

出来た配列 $cmds を使って ssh してみます。

% ssh -t localhost sh -x -c "${(qqj/&&/)cmds}"
+ sudo ruby -e 'puts "Hello uid=#{Process.uid}"'
[sudo] hkoba のパスワード:
Hello uid=0
+ sudo ruby -e 'puts "Again uid=#{Process.uid}"'
Again uid=0
Connection to localhost closed.

quote-line を zsh の関数として書いてみる

使い捨ての作業なら quote-line で一行ずつ quote しても良いのですが、 スクリプト化したい時には不便です。引数それぞれを quote して繋げた文字列を 返す関数を定義してみましょう。

% function zquote { print -R "${(@qq)argv}" }

% zquote sudo ruby -e 'puts "Hello uid=#{Process.uid}"'
'sudo' 'ruby' '-e' 'puts "Hello uid=#{Process.uid}"'
%

(q-) の方が、読みやすい出力になって良いですね。

% function zquote { print -R "${(@q-)argv}" }

% zquote sudo ruby -e 'puts "Hello uid=#{Process.uid}"'
sudo ruby -e 'puts "Hello uid=#{Process.uid}"'
%

これを使うと、先の例はこう書き直すことが出来ます。

cmds=(

 "$(zquote sudo ruby -e 'puts "Hello uid=#{Process.uid}"')"

 "$(zquote sudo ruby -e 'puts "Again uid=#{Process.uid}"')"

)

zquote の出力を一本の文字列にする必要が有るので、 "$(zquote ...)" のように呼び出し全体を ".." で囲っています。

…面倒臭くなってきたので、この辺で!あとは皆さん、ご自由に!

…なお、もっと複雑なケースの時は私は tcl + sshcomm を使います。 が、その話はまたいつか…

参考

"--name <SPC> value" style options considered harmful for end-user scripting.

追記20170914朝JST: 以下の議論では簡単のため --name value に話を絞り -o VALUE 形式への言及を省略したが、後者においても辞書が必要となる点は変わらない。自明とは思うが念の為… 20170914昼JST: タイトルtypo 修正 ><

ストーリー

  • 以前あなたはプログラム A を書いた。それは職場で同僚たちが日々使用している。今のバージョンにはオプションが 5 個ほどある。オプションは今後も増え続けるだろう。
  • 同僚たちも、あなた程ではないが、省力化のための簡単なスクリプト程度は好んで書く人たちである。
  • あなたの書いた A は、同僚の書いたスクリプト B(それは例えば日々の仕事を定期実行するためのバッチや、よく渡すオプションを予めまとめたラッパースクリプトである)から呼び出されるようになった。
    • 同僚のスクリプト B を起動する時に、そこからあなたの A へ、いつもと違うオプションを渡したくなる時がある。だから B は殆どのオプションをあなたの A に素通しで渡したい。
    • ただし、 B 独自のオプションも持ちたい。
  • 同僚たちは (B だけでなく) 元々 A のユーザーでもあるので、あなたの A のマニュアルを読んでいる。そこにオプションが --name value (name と value の間のスペースに注意。 --name=value ではない)の形式で書かれている場合、ユーザーのメンタルモデルにはオプションが --name value 形式で刻まれる。だから B も、その仕様に従うだろう。
    • 何より、職場の Wiki に日々書かれるコピペ用のコード例が --name value スタイルで書かれるだろう。

“–name value” オプションの切り出し処理は辞書を要求する

  • あなたの A が、オプションを --name value 形式で受け取る場合、 --name に続く次の引数が、オプションの value か、あるいは無関係な別の引数であるかは、 name 次第となる。つまり任意の引数列からオプション列を切り出すには、 オプションの名前を網羅した 辞書 を用意する必要が有る。
  • つまり同僚のスクリプト B にも、あなたのプログラム A の全オプションに関する辞書が必要になってしまう。

辞書で対応付けを作った箇所には、変更のバケツリレーも付いてくる

  • 同僚のスクリプト B に対して、誰かが更に別のラッパースクリプト C を書く時もある。 C にも A のオプション辞書が必要になる。 D, E, F… 止まらない保証はない。
  • あなたが元のプログラム A を改良してオプションを追加したら? スクリプト BC のオプション辞書にも改良が必要になる。オプションが増減する度に、この改良(?改良なのか?)の連鎖が起こる。これバケツリレーだ、 WWII の空襲記録で見たこと有るやつだ…

誰かが、この無限の連鎖を止めなければならない…誰かが…

一案としては、 A にオプションの辞書一覧を機械可読な形式で出力する機能を用意する。 B を書く人には、それを読み込んで処理する機能を書いてもらう。これなら処理を書くのは一度きりで済む。 ただし…ちょっとスクリプトを書けるようになり始めた位の人に、これを要求するのは、 ハードルが高すぎる。職場全体で少しでもスクリプトを書く人数が増えて欲しい時には、 ハードルを少しでも下げたい。

もっと簡単に書けて、処理も軽く、漏れがなく、一度書けば済む…そんな、貧者のアプローチがないか?

どうすればよい?

既に書かれた 他人のプログラムは、諦める とする。自分たちがこれから書く、職場限定のプログラムに限り、 オプションの与え方に一律の制限を課す(標準化する)。例えば:

  • --name=value
  • --name (value なし)
  • -c (一文字オプション。value なし)
  • オプション列の終わりは -- で与える

このように制限をすれば、辞書抜きで機械的にオプション列を切り出すプログラムを書けるようになる。辞書と異なり、この処理は一度書けばずっと使える。プログラム A にオプションが増えても、 B のオプション切り出し部分に変更は必要ない。 (もちろん、 B 自身のオプション名と被った場合は問題だが、少なくとも切り出しプログラムの問題ではなくなる)

規格化・標準化は自動化・省力化の友

諸君、私は自動化が好きだ。愛していると言ってもいい。 自動化出来るか否かに比べれば、使い方が流行のプログラムに似ているかなど、どうでも良い。 オプションを --name value のように書きたいばかりにあなたを辞書変更の無限地獄へ巻き込もうとする者たちからは、出来るだけ離れていよう。 多分、連中は雇用対策でそうしているのだろう。我々の人生はもっと有益なことに使うべきだ。

オチ

ラッパーから2個以上のコマンドを呼ぶ時、困るよね…段々辞書が欲しくなるよね… -- を書いてもらう?う〜ん…

『実行可能なモジュール』設計パターンについて…あるいはサブコマンドを持つコマンドを私はどう作るか

『実行可能なモジュール』と私が勝手に呼んでいる、ある種の設計パターン/コーディングイディオムについて、 私なりの意見を整理しておこうと思います。

(この設計パターンは Perl 以外の言語でもよく見かけるので、既に名前が付いているのでは?と予想しています。 教えて頂けるとありがたいです)

pm に shbang と unless caller を書く

unless caller

Perl スクリプトで、ファイルの最後にこんなコードを見たことが有る人は、いるでしょうか?

unless (caller) {
  ...何らかの処理...
}

この unless (caller) {...} のブロックは、このファイルをコマンドとして直接起動した時だけ 呼ばれる処理を記述したものです。私が初めてこの種の書き方に触れたのは 1996頃の Perl/Tk の文脈 で、

MainLoop unless caller;

と書いて

  • このスクリプト自体が起動された時は、 Tk::MainLoop() を呼び出す
  • それ以外のケース、例えば上記スクリプトを別プログラムからクリップボード経由で直接 eval したり do などでロードした時は、何もしない。

という動作を実現するために使われていました。

myscript.pl の代わりに MyScript.pm

さて、この unless caller というイディオムは、 Tk に限らず一般の Perl スクリプトでも役に立ちます。 例えばスクリプトを書く時、 myscript.pl の代わりに MyScript.pm という名前にして package 文も書いて、 正当なモジュールとしてロードできるようにします。その上で、最後に unless caller で、コマンドとして 起動された時の処理を書くのです。

例えば以下のように書きます。

#!/usr/bin/env perl
package MyScript;

...

unless (caller) {
   my @opts; 
   push @opts, split /=/, shift(), 2 while @ARGV and $ARGV[0] =~ /=/; # XXX:手抜き
   my $app = MyScript->new(@opts);
   $app->main(@ARGV);
}

1;

すると、この MyScript.pm は (chmodして) 直接コマンド行ツールとして起動するだけでなく、 モジュールとしてロードし、一部のメソッドだけを呼び出すことも出来るようになります。

# コマンドとして起動し、 MyScript->new(x=>100,y=>100)->main('foo','bar') を呼ぶ
% ./MyScript.pm x=100 y=100 foo bar

# モジュールとしてロードして new し、メソッド foo() を呼ぶ
% perl -I. -MMyScript -le 'print MyScript->new->foo'

サブコマンドをメソッドに対応付ける

先の例では unless caller 時には MyScript->new->main を呼ぶように決め打ちしてありました。 ここを

  • posix style の long option --name=value の列を new() の引数にする。
    • --name のみなら --name=1 として扱う。 --debug みたいに。
  • 次に来た引数をサブコマンドの名前に使う。

という動作にすればどうでしょう? こんなイメージです。

# 何らかのテキストファイルをパースして、DB にロードする
% ./MyScript.pm  --dbname=foo.db  import journal.tsv

# 上記 DB から特定の条件で検索をする
% ./MyScript.pm  --dbname=foo.db  list_accounts

早速、これを実現する unless caller ブロックを書いてみましょう。 parse_opts() は後で定義することにします。

unless (caller) {
   my @opts = parse_opts(\@ARGV);

   my $self = __PACKAGE__->new(@opts);
   
   my $cmd = shift @ARGV || "help";

   my $method = "cmd_$cmd"; # サブコマンドのメソッド名は cmd_... で始めることにする。

   $self->can($method) or die "No such subcommand: $cmd";

   $self->$method(@ARGV);
}
  • ここでサブコマンドのメソッド名に接頭辞 cmd_ を付けることにしたのは、 例えば import というメソッド名が Perl にとって特別な意味の有るメソッド名で、 これと被ると予期せぬ面倒を生みかねないからです。

発展:任意のメソッドをサブコマンドとして試せるようにする

先の unless caller ブロックで呼び出せるのは cmd_... で始まる名前のメソッドだけでした。 これを任意のメソッドまで呼べるように拡張すれば、内部的なメソッドも CLI から 簡単に呼び出して試せるようになります。特に Perl は REPL が弱いので、 これを使えば好きなメソッドを shell のヒストリ・エディタ上で反復的に試せるようになり、 REPL の弱さを補うことが出来ます。

ただし、普通のメソッドは結果を Perl のスタックに返すだけで画面には何も出しませんから、メソッドの結果を 出力する機能も作る必要があります。また戻り値は undef や [], {} ... を含みますから、 出力時はシリアライザーを通したほうが良さそうです。

以上を考えた unless caller ブロックは、例えばこんな感じでしょうか>

use Data::Dumper;

unless (caller) {
   my @opts = parse_opts(\@ARGV);

   my $self = __PACKAGE__->new(@opts);
   
   my $cmd = shift @ARGV || "help";

   # cmd_ で始まるメソッドがあるなら、そちらをそのまま呼び出す。
   if (my $sub = $self->can("cmd_$cmd")) {

     $sub->($self, @ARGV);
   }
   # それ以外でも、メソッドがあるなら、デバッグ目的で実行できるようにしておく。
   elsif ($sub = $self->can($cmd)) {

     my @res = $sub->($self, @ARGV);

     print Data::Dumper->new(\@res)->Dump;
   } 
   else {
     die "No such subcommand: $cmd";
   }
}

もちろん、もっと強化できる所はあります。

  • @res の内容に応じて終了コードを設定すると、シェルスクリプトから使う時に便利になります。
    • スカラーコンテキストとリストコンテキストをオプションで使い分けられると嬉しい人もいるでしょう。
  • 出力時のシリアライザーを JSON にする手もあります
  • 引数の文字列が {...} , [...] の形式の時に JSON としてデコードする手も、ありえます。

この辺りに興味の有る方は MOP4Import::DeclareMOP4Import::Base::CLI_JSON もどうぞ…

この設計パターンの使いどころ

あまり有効でないケース

先に、このパターンがあまり有効でない状況を挙げます。

  • 開発するプログラムの仕様が十分に確定しており、設計に時間を掛ける余裕が有る。
  • 開発者が十分に足りていて、モジュールを分割するほど、手分けして並列で開発を進められる。
  • 作ったけど使わない、という可能性を考えなくて済む。

この記事で挙げたパターンは複数のサブコマンドを一個のスクリプトファイルに書くので、 複数人で並行開発することは困難だろうからです。

有効に働くケース

逆に、

  • 何を作れば『ビジネス上の要求』を満たせるか分からない、探索的な開発をする必要が有る。
  • 顧客が要求仕様をまとめられず、どんなコマンドを何個作ることになるか、全然予測できない。
  • それを作っても使うか分からない、業務に投入してみないと何も言えない時。

という状況下で、それでも前進しないと駄目な時には、以下のメリットがあります。

  • 最初から OOP 用のクラス・モジュールとして書いているので、いつでも継承してメソッドの挙動を変える、など自由自在に OOP の技法を投入できる。
    • もし初手をコマンドとして書き後からクラスへ括り出す場合だと、そこでクラスの命名で悩む時間を取られる。
  • サブコマンドを増やし放題。メソッドへ括り出し放題。あらゆる無茶振りを一旦受け止めるための、汚れ役クラスとも言える。
  • CLI ベースで作って、後から Web 経由で (Fat な) Model として使う、という手も有る。
  • モジュールになっているので、テストも書きやすい。

とは言え、これで作ったものは巨大な一枚 pm になりがちなので、機会を見つけて整理をするのが大事、ではあります。 (…吐血…)

なんでこれ書いたのか

songmu さんの blog 記事

www.songmu.jp

私も UNIX 哲学は好きで、概ね同意できる、と思いつつ…

自分が Perl で仕事のツールを書く時によく使うスタイルの話も書いておいたほうが、誰かの役に立つかもな〜、と思ったからでした。

おまけ

parse_opts() の例です(既存のコードを解説用に簡略化したものなので、動作は未検証です)>

sub parse_opts {
  my ($list, $result) = @_;
  $result //= [];
  while (@$list and my ($n, $v) = $list->[0]
         =~ m{^--$ | ^(?:--? ([\w:\-\.]+) (?: =(.*))?)$}xs) {
    shift @$list;
    last unless defined $n;
    push @$result, $n, $v // 1;
  }
  wantarray ? @$result : $result;
}

更新履歴

  • 2018-07-11: 最初の例の手抜きオプションパーサーの無限ループバグを修正
  • 2018-0609: google groups の Perl/Tk 記事へのリンクを直した