メモ: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 の組み合わせを使う方法の話とかも有るのですがそれは別の機会に、ということで…
こんな長い記事に最後までお付き合い頂き、ありがとうございました。
私信:CS にも過渡現象論みたいな位置づけの理論があると良いのかも?みたいな…
この記事は、この方のつぶやき
あと、前のツイートとは関係ないですが、副作用さえ無ければいいという考え方、妹さえいればいい。という発想と同じな気がするんですよね、手段が目的化してるというか。。。
— も~ひずむ (@yuwki0131) 2017年11月14日
に対する、@pokarim さんの、
— ぽかりむ (@pokarim) 2017年11月14日
Immutability にこだわる方向にも限界があると思いますね。
— ぽかりむ (@pokarim) 2017年11月14日
immutability の重視とか副作用の排除の目的はなんだったのかというと、やっぱりモジュール性の向上というのが考えられると思う。
— ぽかりむ (@pokarim) 2017年11月14日
この辺りのツイートに、私がこう、ぽやん…
.oO(…この辺りは、電気工学の過渡(現象)と定常、みたいに理論を二本立てにするのが良さそう、みたいに思っています…) https://t.co/jAa5gALzos
— hkoba (@hkoba) 2017年11月15日
先日の過渡現象と定常の話にぴったりな話題を話しておられたので…
— hkoba (@hkoba) 2017年11月17日
この表計算の、再計算が走ってセルが順次書き換わっていく過程と、
全ての計算が完了して全セルの値が確定した状態の関係が、
過渡現象と、定常状態の関係に似ているなぁ、という話なのです。
(他に効率の観点もありますが https://t.co/g1VClYp3uo
と感想をぶら下げて始まった対話の、お返事です。
ぽかりむさんへ
私の考えがどんなものか、なぜその考えに至ったか(特に 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
さんに突っ込んでもらえて、私の誤解を正してもらえました。…誤解したままだったら…と考えると、肝が冷えます。さとうさん、有難うございました。
.oO(…SSLのクライアント証明書って、オレオレにしちゃったら、認証として全く意味が無くなると思うのだけど、違うのかしら?…)
— hkoba (@hkoba) 2017年10月4日
違いますね。
— ふみやす@シェルまおう(自称でない)🚲 (@satoh_fumiyasu) 2017年10月5日
むむむ?どんな使い方だと、オレオレなクライアント証明書でも安全になるのでしょう?
— hkoba (@hkoba) 2017年10月5日
サーバーへのログインユーザ名にクライアント証明書の CN を使うというケースだと、他の人が勝手に同じ CN でオレオレ証明書を作って接続してきても、通ってしまうのでは、と思うのです…
当然、条件はあります。要は相手が示した証明書の署名が、自分の信頼している CA の証明書で発行したもの(かつ期限内、CRL などに載ってない)であればいいわけですから。オレオレ証明書でも同じです。
— ふみやす@シェルまおう(自称でない)🚲 (@satoh_fumiyasu) 2017年10月5日
認証する側が自己署名証明書を信頼している (信頼する CA 証明書として設定してある) なら大丈夫だったかと。実装や証明書に付与した何かしらの制約(constraints)に依るかも。
— ふみやす@シェルまおう(自称でない)🚲 (@satoh_fumiyasu) 2017年10月5日
なるほど、該当 self-signed cert 自体をサーバー側に信頼する CA として登録する、と…
— hkoba (@hkoba) 2017年10月5日
確かにそこまですれば、別人が勝手に同じ CN の証明書で繋げてきても拒否できますね。
ありがとうございます~
その後、実際に postgresql で試してみた所、CNだけ同じの成り済ましクライアント証明書で接続した時はこんなエラーが出る、と確認できました。
psql: SSLエラー: tlsv1 alert unknown ca
じこひはん
クライアント側も CA として扱う、という発想が出てこなかったのが、自分の頭の硬い所だったなぁと。お互いがお互いを Certificate Authority として信頼する…おお、peer to peer だ。どうも私の発想はサーバーに偏りすぎてたようです…
ssh 先で sudo 実行するコマンドに渡すワンライナーを zsh に quote させる
いつもの通り、ツッコミ歓迎です。
その quote, zsh に任せると幸せ…かも??
笹田さんのこのツイを見て、おっと zsh 宣伝チャンス、と。
なんで a b 消えちゃうのん?
— _ko1 (@_ko1) 2017年9月29日
$ sh -c "echo a b; echo c d"
a b
c d
$ ssh host sh -c "echo a b; echo c d"
c d
ssh host -t sudo ... ってやりたかったので、それがいかんのですよね、残念ながら
— _ko1 (@_ko1) 2017年9月29日
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 を使います。 が、その話はまたいつか…