この記事の続きがなさそうだったので勝手に書く。
(この記事はshのパイプをmaybeモナドに見立てるにはどうするかという話だ。)
前の処理が成功したときだけ処理したい場合、shだと以下のように書く。
command1 && command2 && command3 && ...
ただこれだと処理結果を後ろに渡せない。一方パイプの処理は戻り値を参照しない。
command1 | command2 | command3 | ...
したがって以下を全部満たすようなケースではパイプは使用できない。
- 異常復帰する場合も標準出力に出力する場合がある
- 以下のような理由でコマンドが異常復帰する場合は、処理したくない
- 出力が信用できない場合
- 出力が行志向ではなく、不完全になる場合
- ロールバックがめんどくさい、などなど
標準出力は処理中に得られるが、戻り値は処理後に得られるのでこれはどうしようもない。
上記のような場合は以下のように処理することになるだろう。
command1 > file1 &&
command2 < file1 > file2 &&
command3 < file2 > file3 &&
...
実際にはファイル(または変数)の名前をいちいち考えないといけないし、コピペでファイルを取り違えると面倒なことになる。
こう書けると嬉しい(ことにする)。
may command1 |
fmap command2 |
fmap command3 |
...
cmd
ここで
- mayは引数のコマンドから標準出力と戻り値を取り出して、出力をmaybe的文脈付きに変換する関数
- fmapは文脈付き標準入力を通常の標準入力に変換してコマンドに渡し、標準出力に再度文脈を付加して出力する関数
- cmdは特殊な文脈付き標準入力を通常のコマンドの文脈(標準出力+戻り値)に変換する関数
fmapとcmdは型に応じてディスパッチさせたいので、入力には型情報が必要である。
標準入出力に流す文脈付きデータ構造は以下のような感じにする。
<型情報>
<値> ...
いつもの出力
...
例)
$ may echo a # コマンド -> maybe(成功)への変換
maybe <--型情報
0 <--戻り値(成功)
a <--データ
$ may false # コマンド -> maybe(失敗)への変換
maybe <--型情報
1 <--戻り値(失敗)
$ may echo a | cmd # maybe(成功) -> コマンドへの変換
a <--データ
$ may false | cmd # maybe(失敗) -> コマンドへの変換
$ # 値は得られない
$ echo $?
1 <--戻り値がリストアされる
$ may echo x | fmap grep x
maybe <--型情報
0 <--戻り値(成功)
a <--データ
$ may false | fmap grep x
maybe <--型情報
1 <--戻り値(失敗)
実装例)
may() {
: `mktemp`
{ rm $_
"$@" >/dev/fd/3 && just cat /dev/fd/3 || nothing $?
} 3<>$_
}
just() {
echo maybe
echo 0
"$@"
return 0
}
nothing() {
echo maybe
echo ${1:-1}
return ${1:-1}
}
monad_dispatcher() {
local type value f=$1
shift
read type
${f}_$type "$@"
}
alias fmap='monad_dispatcher fmap'
alias cmd='monad_dispatcher cmd'
fmap_maybe() {
bind_maybe may "$@"
}
bind_maybe() {
local value
read value
case "$value"
in 0) "$@"
;; *) nothing "$value"
esac
}
cmd_maybe() {
local value
read value
case "$value" in 0)
cat -
esac
return "$value"
}
性能上の配慮から引数のコマンドの復帰値を気にしないfmap_を定義しよう。
alias fmap_='monad_dispatcher fmap_'
fmap__maybe() {
bind_maybe just "$@"
}
$ may : | fmap grep x
maybe
1
$ may : | fmap_ grep x
maybe
0
少し複雑な例
すごいH本のあれをやろう。
landleft() {
local left right
read left right
left=$((left+$1))
if diff_lt $left $right 4; then
just echo $left $right
else
nothing
fi
}
landright() {
local left right
read left right
right=$((right+$1))
if diff_lt $left $right 4; then
just echo $left $right
else
nothing
fi
}
banana() {
nothing
}
diff_lt() {
if [ $1 -gt $2 ]; then
[ $(($1-$2)) -lt $3 ]
else
[ $(($2-$1)) -lt $3 ]
fi
}
alias bind='monad_dispatcher bind'
ここでは組み合わせる関数がすでにmaybe的なものなので、fmapではなくbindを使う。
$ just echo 0 0 | bind landleft 4
maybe
1 <--- 失敗!
$ just echo 0 0 | bind landleft 2 | bind landright 2 | bind landleft 2
maybe
0 <--- 成功!
4 2
$ just echo 0 0 | bind landleft 1 | bind banana | bind landleft 1
maybe
1 <--- 失敗!
余談
just関数はhaskellのjustには対応せず、正確には just cat - が対応物になるが、無駄にフォークすることになるので今の実装にした。
また、maybeの文脈と通常のコマンドの文脈を行き来したかったので、nothingが値(失敗したときの戻り値)を持つようになっており、either型の方が近いかもしれない。
monad則の対応物は恐らく以下だと思っているが、よくわからない。
return x >>= f ⇔ f x
m >>= return ⇔ m
(m >>= f) >>= g ⇔ m >>= (\x -> f x >>= g)
just cat | bind f ⇔ f
m | bind just cat ⇔ m
{ m | bind f; } | bind g ⇔ m | bind eval 'f | bind g'