LoginSignup
0
0

More than 3 years have passed since last update.

パイプの前のプロセスが成功したときだけ処理したい(Maybeっぽいパイプ)

Posted at

この記事の続きがなさそうだったので勝手に書く。
(この記事は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'
0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0