(最後しか聞けなかったけど)吉祥寺pm16感想とりいそぎ
昨夜は 吉祥寺pm#16に参加してきました。
と言っても、私は仕事のトラブルで大遅刻!
勉強会って遅刻すると、すっごく行きづらいと言うか、気持ちが落ちませんか? 私は落ちます。でも懇親会の参加者が減ると主催者さんを困らせちゃうし…と思い、会場へ。
そんな落ち気味な気持ちで会場に着き、部屋に入ると、なぜかプロジェクターでゲーム…???
#kichijojipm 起業クエストww pic.twitter.com/mUhow0FKpO
— teckl (@teckl) November 22, 2018
自作ゲームでプレゼンらしく、こりゃ凄いと思いました。
なんとかエンドロールまでいけて良かった! ありがとうございました! #kichijojipm
— :craftsman/kawasima (@kawasima) November 22, 2018
その後は休憩から LT タイム
どの LT も良かったのですが、個人的に一番刺さったのはこの LT、 スタートアップの一人技術者の立場からの採用の話でした。 曰く 『面接で高印象だった人に、お試しで働いてもらう』 作戦です。
- 転職か副業を希望している人で、面接で高印象だった人に、
- 業務委託として、お試しで働いてもらう。
- リモートワーク+週一のミーティング
昨日のLTのスライドです!遅くなりましてすみません... スタートアップで開発速度を上げるためにやった事 https://t.co/2VHdD67rSk #kichijojipm
— kakakazuma (@kakakazuma20) November 23, 2018
確かに、マッチするか否かは一緒に仕事をしてみないと分からない部分が大きいので、これは合理的だと思いました。
実際にどんな仕事をリモートワークで依頼しているのかという質問には、(Webの)フロントエンドとスマホのアプリを例に挙げておられました。確かにそれなら仕事として切り出して発注しやすそうだなと。
なお、週一のミーティングは(相手の都合に合わせるので)どうしても土日どちらかに入れることになるそう。 経営・管理側でないと出来ない技だなあと思いつつ、体調管理についてうかがうと、職住近接で8時間?睡眠を死守しているとのことでした。
たまたまその後の懇親会でもお隣の席だったので、幾つもアドバイスを頂けました。
私も(私がお客さんに提供している)技術スタックのコア部分が、自分にしか扱えない、完全なトラック係数1、SPOF になっています。ビジネスの継続性のために、この技術の継承者をどう育てるかという点が、ずっと懸案でした。
それに対して頂いたアドバイスは、ずばり、それって弟子取りですよねと。そして>
弟子を取るなら、最初の面接のハードルを上げるべしというアドバイスを頂いた。
— hkoba (@hkoba) November 22, 2018
愛すると決めてから取るべしと
相手をよく知らないまま採用して、後から相手の良い所を探しながら教育するのは、大変だから…と。
そうではなく、先に相手の中の「尊敬できる所」「愛せる所」「見どころ」を見出しておく。見いだせた人だけを、 採用する。つまり、採用する側の、「愛する覚悟」が定まった時だけ、採用するように…しないと辛いよねと。
…これは、とても響く話でした…
そうして、愛すると決めた弟子が出来たら、以外に仕事の切り出し・委譲は出来てくるものだ、という話を聞いている
— hkoba (@hkoba) November 22, 2018
上赤さん、有難うございました。
吉祥寺pm15 で Runnable Module パターン(仮)の話をしてきました
コードを2つに分けて書く - 一つは自分のため、もう一つは顧客のために
はじめに
この記事は、身近なプログラマー仲間に向けて書かれたまとめです。 私的な、身内向けの解説文ではあるのですが、 仕事でプログラムを組む人全般に通じるように書く、つもりです。
主張
私は基本的に Perl プログラマーです。ですが仕事では、 Perl で 1ファイルで全部書けるような小さな案件でも、
のように、2つのプログラムに分けて書いたほうが良いと考えています。 この記事ではその理由を解説します。(この考えは Perl と Zsh の組み合わせ以外にも、応用できます)
どんなコード量・規模を想定した話か分からないと納得できない人もいると思うので、 参考として、この考えに基づいて書いた昔のコードを載せておきます。 (コードの解説はありません。要望があれば書きます) github.com
仕事のコードを2つの層に分けるメリット
1. 適材適所
一つの理由は、言語毎の得意領域の違いです。 例えばファイル名操作やプロセスの単純な起動とデータのやり取りは、少なくとも私にとっては、 Perl よりも Zsh の方が短時間で書け、 かつ動作検証もコマンド行で出来て楽である、といった風に。
ですが、理由はそれだけではありません。
2. 汎用を目指すコードと、汚れ役になるコードをファイル単位で隔離できる
もっと大きな理由は、ファイルを分けることにより、プログラマーとしての自分を納得させるためのコーディングと、 顧客第一でオーバーエンジニアリングを避けるコーディング、 2つのコーディング方針を使い分けることが出来るからです。
言うまでもないことですが、仕事のコードは顧客・案件に特化した定数 (ファイル名・ディレクトリ名・アカウント名・サーバー名…)を扱う必要が有ります。 汎用的なプログラムを書きたいなら、こうした定数を何らかの形で プログラムの外部に追い出す必要が有ります。
早くからプログラムをファイルとして2つに分けておくことにより、
- 『汎用性があるよう、キレイに書きたいコードを入れるファイル』
- プログラマーが納得できる抽象度のコードを書くためのファイル
- 『定数をハードコードして良い、 汚れ役 ファイル』(雑事吸収層 と呼んでいます)
という風に、2つのプログラムで別々のゴール・価値基準を追求できるようになる、という考えです。 一つのプログラムで満たそうとすると無理が出るので、別々のプログラムに分ける、ということです。
脱線しますと、私がこの方針に至った背景には、よく知られたフレーズの一つ↓の影響が大きいように思います。2つの直交した問題領域を扱うなら、間にマッピングをかまさないと、片方に漏れ歪みが出るのは自然な現象ですよね。
『計算機科学のあらゆる問題は別のレベルのインダイレクション(間接参照)で解決できる』
好き>
— hkoba (@hkoba) 2018年3月14日
"All problems in computer science can be solved by another level of indirection," is a famous quote attributed to Butler Lampson, the scientist who in 1972 envisioned the modern personal computer.
Beautiful Code: Another Level of Indirection https://t.co/hRLxSe3HYv
この記事によれば誰が言い出したのかは諸説あるぽいですね>
設定ファイルは無限に拡張され、半端なプログラミング言語に成り果てる。なら最初から…
ここで、定数のハードコードを避けたいだけなら、 yaml や ini, json などの設定ファイルの読み込み機能を提供するだけで十分ではないか? プログラムをもう一つ用意するほどではないのでは?と疑問に思う人も多いかもしれません(要出典)。 これに対する私の意見は、(例え設定ファイル読み込み機能を提供する場合でも) ユーザーに渡す汚れ役プログラムを別途作成したほうが、費用対効果が良い、というものです。
その理由は、
- 本来、設定ファイルは定数を書くためのもの。ロジック・ダイナミックな値を書くために設計されたものではない。
- 顧客の事情にもロジックはあるし、ダイナミックな値を使って記述したいことは沢山有る
- 共通の値(変数にしたい)、条件式と分岐、繰り返し、ワイルドカード・パターン…
- 故に、設定ファイルで無理にロジックを表現しようとすると、あちこちに Ad-Hoc な eval を行う拡張ルールを定める必要が出る。
- それは実質的に、不完全なプログラミング言語を作っているようなもの
- その拡張?が延々と続く…
ここでもう一つの名フレーズが思い出されます。
「十分に複雑なCまたはFortranの プログラムは全て、後付けで、正式な仕様がなく、バグがてんこもりの、 遅い、CommonLispの半分の実装を含んでいる」
(訳はこちらの脚注より)
Greenspun's tenth rule - Wikipedia
http://practical-scheme.net/wiliki/wiliki.cgi/Lisp:GreenspunsTenthRule
あくまで私の解釈ですが、このフレーズが言う後付の出来損ない common lisp というのは、 設定ファイル機能の成れの果てを指しているのではないか、と思うのです。
いずれそうなる定めなら、最初から(チューリング完全な)プログラミング言語を持ち出したほうが良いのでは、と。
最後に
ここまで読んで下さりありがとうございました。
文章を書くのって、本当に時間がかかりますね…
メモ:Zsh で TSV を読みたいとき、どうするか
(いつも通り、ツッコミ歓迎です)
問題
例えば TSV (タブ区切りテキスト)形式の正誤表があり、そこから SQLite のデータベースファイルへ更新をかけたい… そんな時、皆さんだったらどうしますか? なお、レコード数は1,000行程度とします。
私はこういうケースだと Zsh を使って雑に UPDATE 文の列を作ることが多いです。 ただ、雑に書きすぎると入力の形式によっては上手く行かないので、その辺りをまとめてみました。
TL;DR IFS で分けるな、一行丸々 IFS= read -r line
で読み込んでから col=( "${(@ps:\t:)line}" )
で配列に分けよう。
なお、SQLite は文字列をシングルクォートで囲って表現しますが、その中にシングルクォートが出現する時は ''
のように二重にする必要があります。
今回は、この処理に以下の関数を使います。この SQL文字列をクォートする仕組みの解説は以前の日記を参照して下さい
function zqsql { local val=$1 print -nr "${${(qq)val}//'\''/''}" }
部分的にしか動かない方法: IFS を使う
まず、read と IFS の組み合わせを使う方法を書いてみました。
入力ファイル input1.tsv
はこんな内容です。
id old new 1111 ああああ かかかか 2222 いいいい きききき 3333 うううう くくくく
実行
% i=0; while IFS=$'\t' read -r id old new; do ((i++)) || continue print "update t set name = $(zqsql $new) where id = $(zqsql $id);" done < input1.tsv update t set name = 'かかかか' where id = '1111'; update t set name = 'きききき' where id = '2222'; update t set name = 'くくくく' where id = '3333';
残念ながら、このやり方だと次の input2.tsv
のように、入力に空文字列が入るケースで困ります。
id flag1 old flag2 new 1111 foo ああああ bar かかかか 2222 1 いいいい きききき 3333 うううう 2 くくくく
実行の様子です。 1111
は正常ですが、 2222
, 3333
の update 文は期待と違います。
% i=0; while IFS=$'\t' read -r id _ old _ new; do ((i++)) || continue print "update t set name = $(zqsql $new) where id = $(zqsql $id);" done < input2.tsv update t set name = 'かかかか' where id = '1111'; update t set name = '' where id = '2222'; update t set name = '' where id = '3333';
これは $IFS
に(タブなどの)空白文字を設定した場合、複数の空白文字の連なりを一つのフィールド区切りとして認識しているから、でしょう。
# IFS にタブを設定した場合 % IFS=$'\t' read -A foo <<<$'foo\t\t\tbar' # 配列の中は2要素 % print $#foo 2 % print -l "$foo[@]" foo bar % # IFS にカンマを設定した場合 % IFS=, read -A foo <<<"foo,,,bar" # 配列の中は 4 要素 % print $#foo 4 % print -l "$foo[@]" foo bar %
解決策:IFS では分割せずに、読み込み後に分割する
変数展開フラグの s:string:
を使いました。
以下 zsh 公式マニュアルの引用です。
s:STRING: Force field splitting at the separator STRING. Note that a STRING of two or more characters means that all of them must match in sequence; this differs from the treatment of two or more characters in the IFS parameter. See also the = flag and the SH_WORD_SPLIT option. An empty string may also be given in which case every character will be a separate element. For historical reasons, the usual behaviour that empty array elements are retained inside double quotes is disabled for arrays generated by splitting; hence the following: line="one::three" print -l "${(s.:.)line}" produces two lines of output for one and three and elides the empty field. To override this behaviour, supply the '(@)' flag as well, i.e. "${(@s.:.)line}".
今回の例に適用すると、こんな感じになります。
% i=0; while IFS= read -r line; do ((i++)) || continue # 変数 $line の中身を s(split) フラグで分割 # タブをエスケープシーケンスで \t と書きたいので p (print) フラグを立てる # 空文字列を残したいので全体を "" で囲みつつ @ フラグも立てる col=("${(@ps:\t:)line}") print "update t set name = $(zqsql $col[5]) where id = $(zqsql $col[1]);" done < input2.tsv update t set name = 'かかかか' where id = '1111'; update t set name = 'きききき' where id = '2222'; update t set name = 'くくくく' where id = '3333';
謹賀新年/2018目標
あけましておめでとうございます
昨年度ご縁の有った方々、仲良くして下さった皆様、ありがとうございました。
今年も一年、よろしくお願いします。
2017の良かったこと
- やっと引っ越し先が決まったこと
- (多少は) fsharp, docker, golang, nodejs, jupyter notebook をかじれたこと…本当に多少だけど…
2017の反省点
- 若手プログラマーに魅力的な場を提供できず、他社に引き抜かれてしまったこと
- オレオレ言語の試作が停滞したこと
2018の目標
- この本の内容の実践を試みる
- 作者: 田原祐子
- 出版社/メーカー: 秀和システム
- 発売日: 2017/09/27
- メディア: 単行本
- この商品を含むブログを見る
- 仕事の手離れ改善する
- 自分の担当業務を Job Description へと書き起こし、業務内容の言語化(明文化)とモジュール化を進める。
- タスク毎に、手順書、チェックリスト、(背景の)解説書を分けて書く。
- OSS作り, Life work に充てる時間を少しでも増やす
- Tcl/Tk 方面の成果もアピールしていきたい。特に sshcomm と tkhtml3
- YATT::Lite 、取っつきやすくする(でも誰に?)
- オレオレ言語の意味論練りのため、試作の数を打つこと
- 仕事が忙しくなっても生活が荒れないような対策を取る
- 筋肉つけたい
- ルンバとかハウスキーパーさん頼む?
私信:つい同期と自分を比較してしまう、新人さんへ
先日お話したこと、まとめておきますね。 (以下は医学的な裏付けのある話ではなく、私の個人的な体験に基づくアドバイスです)
考えることも体力を使う、と認識する
まず『(全力で)思考する・考える』こと自体が『脳を疲れさせる』という点に気づくことが大事です。脳に疲れが溜まると、筋肉と同じで、平常時のようには動かなくなっていきます。 (ゲームの HP や MP みたいに考えても良いかもしれませんが、どちらかと言えば、 疲労ゲージ ・ダメージゲージが溜まっていく風に、私は感じます)
これはつまり、『同期と自分とを比較する』ことに脳を使うのも、 『プログラミングの演習問題を一心不乱にこなす』ことに脳を使うのも、 脳を一定量、疲れさせる行為であるという点は、同じだ、ということです。 限られた資源である思考体力を、どちらに割り当てるか、という問題と考えて良いでしょう。
もっと言うと、(私の個人的な経験ですが)、 同じことをグルグルと結論も無しに考え続けた時、脳は物凄く消耗する、 という問題も有ります。無理矢理でも思考に割り込んで、結論を出して思考ループを停止させること、 そして残った体力で現実を変える行動を始めること、その切り替えこそが大事です。
もちろん人間なので、自分より進みの速い同期とつい比較してしまうことは、 ゼロには出来ないかもしれません。でも、そればかり見ていては 肝心の演習に使う脳の体力まで消耗してしまい、本末転倒なのです。
ですから、 如何にして『思考を切り替える』か が、今のあなたの人生のテーマ、 課題、天の?試練なんだ、と考えては如何でしょうか?
思考を切り替える技を身につける
私のお勧めは、頭の中身をメモに書き出すこと、です。 頭の中だけで考えると歯止めがかからないので、代わりに、 実際に紙やコンピューター上に考えをメモや日記として書き出す習慣を 付けましょう、ということです。
書き出すことにはいくつかメリットが有ります。
- 思考が生む疲れを知覚できるようになる(沢山書くと体が疲れるので、気づきやすくなる)
- 書く速度に合わせて思考がスローダウンし、その分、脳の疲れが抑えられる (ループ時は消耗を抑えることが大事)
更に、後でメモを読み返すことで得られる効果もあります。
- 繰り返しに気づける、飽きる
- 自分の考えていること自体を客観視しやすくなる
- 事実と、自分による評価とを、分ける・整理するきっかけになる
文章として書く以外に、マインドマップのように、図的に書き出す方法も、良いかもしれません。
書き出したら、それについて考えることは一旦やめる(よう努力する)のが大事です(多分)。また同じことを考えてしまうかもしれませんが、そこで自分を過度に責めず、はいはい、程々にね…くらいにして、思考の切り替えを少しずつ練習してみて下さい。
最後に
それでも挫けそうな時が有ったら…
私の心の支えを貼っておきますね。
- メディア: Amazonビデオ
- この商品を含むブログを見る
snit で Tcl/Tk が少し楽に書けようになるのでご紹介
この記事はOSS紹介 Advent Calendar 2017 の 13日目の記事です。
概要
snit という OOP ライブラリを使えば Tcl/Tk のプログラミングも大分マシになる…そういう話を書きます。(今どき Web じゃないの? Electron じゃないの?というツッコミは無視します ;-) 古臭い Tcl/Tk 製 GUI wrapper を snit で書き直すとどうなるか、具体的なコードを挙げてご紹介します。(紹介記事なのに長すぎてすみません) 記事のコードは Tcl 8.5 以上を前提としますが、 snit 自体はそれ以前の Tcl/Tk でも利用可能です。ソースは gist にもあります。
なお、ここでは Tcl/Tk 自体の解説は省略しますが、日本語だと もっと Tcl/Tk のTcl/Tk入門 、はじめよう、 Widget の節がとっつきやすくて良いでしょう。英語で良ければ Tcler's Wiki も貴重な情報源です。
目次
- はじめに - 今さら何に Tcl/Tk を使うのか
- snit とは
- 生の Tcl/Tk で GUI を作ると、何が辛いか
- snit で書くと、どう改善されるか
- 最後に
はじめに - 今さら何に 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 を組む時に悩むこと
snit はこの2つ両方に役立ちますが、今回は前者に焦点を当てて解説します。
v1. 最初は widget path 直書きでも苦にならない
はじめに、 ls
コマンドを実行するボタンと、その実行結果を表示するテキスト領域だけからなるスクリプトを書いてみます。
至って平凡な、雑な 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 直書きではレイアウト変更が辛くなる。どうするか。
さて、このツールのテキスト領域にスクロールバーを足したくなりました。ここでは BWidgetの ScrolledWindowを用います。
コードはどう変わるでしょうか? .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 アプリでは、
の二通りの解決策があります。
引数仕様を変える方法
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 宣言が増えていく
次はコマンドの成功・失敗をボタンの近くにラベルで表示するようにしてみましょう。(ボタンとラベルはツールバー風に横向きに並べたいので、フレームを作ってその中にボタンとラベルを置くことにします)
このコードで注目するべき点は 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
自体は複数個実体化することが可能になっています。後は実行するコマンドをインスタンス毎にオプションで設定できるようにするだけです。
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 の組み合わせを使う方法の話とかも有るのですがそれは別の機会に、ということで…
こんな長い記事に最後までお付き合い頂き、ありがとうございました。