USP Magazine 2014年9月号「シェル芸勉強会後追い企画 Haskellでやってはいかんのか?

Mon May 4 12:41:01 JST 2015 (modified: Sun Oct 1 10:50:27 JST 2017)
views: 2410, keywords: この記事は最終更新日が6年以上前のものです。

6. シェル芸勉強会後追い企画: Haskellでやってはいかんのか?

産業技術大学院・USP研究所・USP友の会 上田隆一 [2]

USP友の会のシェル芸勉強会 (脚注:シェルのワンライナー勉強会)は、 日々、他の言語からの他流試合に晒されているのである。 そこで上田は、Haskellで自ら他流試合を行い、 さらにシェル芸勉強会をいじめる自傷行為に手を 染めるのであった。

6.1. はじめに

 こんにちは。本も出たことだしもうちょっと 大人にならなければならないような気がしないでもない上田です。 [1]

 大学にいるとしょっちゅう高専生と間違われます。 間違われるのは楽しいんだけどたまに横柄な扱いを受けます。 私はいつもそんな目にあっているので、 若い人をちゃんと敬う気持ちが欲しいなと日頃、思っております。 自分の子供を見ていると分かりますが、 若い世代の方が効率的に勉強してますしね。 私のような年寄りは早くホロン部した方がよいような気がします。

6.2. 今回の問題

 さて、いつになく真面目に [3] スタートしましたが、 今回は第1回シェル芸勉強会の3問目の途中からです。 [4]

/etc の下にあるすべてのbashスクリプト ( #!/bin/bash で始まるもの) について以下の操作をしてください。

  • ~/hoge というディレクトリにコピー
  • その際、 「 #!/bin/bash 」を「 #!/usr/local/bin/bash 」 に変更

前回は、リスト1のように /etc 直下のファイルを列挙するところまで来ていました。

  • リスト1: ファイルのリストを作るプログラムと実行結果
 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
###こんなコード###
   ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ cat q1_3_2.hs
   import System.Directory

   main = ls "/etc/" >>= putStr . unlines

   ls :: String -> IO [FilePath]
   ls dir = getDirectoryContents dir >>= return . filter (`notElem` [".",".."])
   ###実行!!###
   ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ ./q1_3_2 | head
   insserv
   blkid.tab
   tidy.conf
   logrotate.conf
   avserver.conf
   gshadow-
   iproute2
   localtime
   systemd
   dhcp
   

 「 /etc 下」というのは子のディレクトリ、孫のディレクトリ・・・ と下のディレクトリのファイルも全部含むつもりで出題しました。 ですので、再帰的にディレクトリを降りて行く処理の実装が必要になります。

6.3. パスを作る

 今回はまず、 q1_3_2 の出力の頭に /etc/ をくっつけるところから始めましょう。 このままだと、再帰的にディレクトリを降りてファイルのリストを作って行くとき、 どのディレクトリのファイルか分からなくなってしまいます。 リスト2のようにプログラムを書きかえます。

  • リスト2: パスをちゃんとするq1_3_3.hs
 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ cat q1_3_3.hs
   import System.Directory

   main = digdir "/etc" >>= putStr . unlines

   ls :: String -> IO [FilePath]
   ls dir = getDirectoryContents dir >>= return . filter (`notElem` [".",".."])

   digdir :: FilePath -> IO [FilePath]
   digdir dir = ls dir
    >>= mapM (\\x -> return $ dir ++ "/" ++ x)
   ###実行してみましょう。/etc/、ついてますね###
   ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ ./q1_3_3
   /etc/insserv
   /etc/blkid.tab
   /etc/tidy.conf
   /etc/logrotate.conf
   (以下略)
   

 変更点は、4行目の mainls 関数 を使っていたものを digdir に代えました。 ディレクトリを下がって行く準備です。

  digdir ですが、 ls と同様、 >>= (バインド演算子)で処理をつないだ作りになっています。 右にどんどんつないでいくと長くなりすぎるので、改行を入れました。 インデントは、10行目の ls のところに合わせました。

  digdir の中身には初めて出て来たものが多いので、 ゆっくり説明して行きます。 まず、11行目の mapM ですが、こんな型です。

1
   2
Prelude> :t mapM
   mapM :: Monad m => (a -> m b) -> [a] -> m [b]
   

Monad m => とありますが、 とりあえず mIO であると考えましょう。

 リスト2のコードにおいては、実際の型はこうなります。

1
(FilePath -> IO FilePath) -> [FilePath] -> IO [FilePath]
   

つまり、 mapM の入出力を説明すると、

  • 第一引数に「FilePath型をとってIO FilePath型を返す関数」をとる
  • 第二引数に「FilePathのリスト」をとる
  • 出力には「FilePathのリストにIOをつけたもの」

となります。動作は、 「FilePathのリストを受け取って、第一引数の関数に次々と突っ込み、 次々と突っ込んだ結果を再度リストにまとめてIOをくっつけて出荷する」 ということになります。 第二引数の「FilePathのリスト」は、 ls dir の出力です。ただ、 ls dir の出力の型は IO [FilePath] ですが、 >>= を介すと IO を取って演算できるのでした。

 ちなみにこれまでも出てきた map の型は、

1
   2
Prelude> :t map
   map :: (a -> b) -> [a] -> [b]
   

でした。 mapM は、 map のように振る舞いながら、 さらに IO を頭にくっつけるという動作になります。

 次、 mapM の第一引数の

\\x -> return $ dir ++ "/" ++ x
   

ですが、これは

somefunc x = return $ dir ++ "/" ++ x
   

という関数と等価です。 return とかややこしいことが書いてありますが、 それは関係なく、単に関数を書くときには リスト3のように、二つの書き方ができるということです。 上の「 somefunc 」やリスト3の下の例 「 f 」のように名前をつけてしまうと mapM に直接渡せないので、 「 \\x -> ... 」のような書き方をしています。 このように書かれた関数は「無名関数」や「ラムダ式」 と呼ばれます。

  • リスト3: 二つの関数の書き方
1
   2
\\x -> 1 + x
   f x = 1 + x
   

 で、最後の重要な話に return 関数があります。 これは、 dir ++ "/" ++ x の型FilePathにIOをくっつけて IO FilePath にするための関数です。 >>= でつながれた演算の中では、 型を合わせるためにしばしば登場します。 普通のプログラミング言語の return とは違うものですのでご注意を。 型を示しておきます。

1
   2
Prelude> :t return
   return :: Monad m => a -> m a
   

6.4. 再帰的にディレクトリを調査

 次にやる事は、 /etc/ 下の子供、孫、ひ孫、玄孫・・・ のディレクトリも調査することです。 digdir をいじって、次のような処理を組み込みます。

  • FilePathのリスト中の要素が、ファイルならばそのまま残す
  • FilePathのリスト中の要素が、ディレクトリならばそのディレクトリ下のリストでその要素を置き換える

ということで実装したものをリスト4に示します。 digdir に2行加えて、あとは digdir' という関数を定義しています。

  • リスト4: 再帰的にディレクトリを検索してファイルパスを返すq1_3_4.hs
 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ cat q1_3_4.hs
   import System.Directory

   main = digdir "/etc" >>= putStr . unlines

   ls :: String -> IO [FilePath]
   ls dir = getDirectoryContents dir >>= return . filter (`notElem` [".",".."])

   digdir :: FilePath -> IO [FilePath]
   digdir dir = ls dir
    >>= mapM (\\x -> return $ dir ++ "/" ++ x)
    >>= mapM digdir'
    >>= return . concat

   digdir' :: FilePath -> IO [FilePath]
   digdir' path = do b <- doesDirectoryExist path
    if b then digdir path else return [path]
   ###実行!!!(rootになってください)###
   ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ sudo ./q1_3_4
   /etc/blkid.tab
   /etc/tidy.conf
   /etc/logrotate.conf
   /etc/avserver.conf
   /etc/gshadow-
   /etc/iproute2/rt_dsfield
   /etc/iproute2/rt_realms
   (中略)
   /etc/dhcp/dhclient-enter-hooks.d/resolvconf
   /etc/dhcp/dhclient-enter-hooks.d/debug
   (以下略)
   

6.5. ヘイ!you!doしちゃいなよ

 まず、 digdir' から説明して行きます。 do の説明をしないといけないでしょう。 理屈でなく単純に説明すると、 do>>= のように一直線にアクションをさばくとややこしいときに、 >>= の代わりに使うものです。 例えば、リスト5の二つのコードは等価です。

  • リスト5: doとbind演算子の書き方比較
 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ cat bind.hs
   main :: IO ()
   main = getContents >>= putStr
   ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ cat do.hs
   main :: IO ()
   main = do str <- getContents
    putStr str
   ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ echo aho | ./bind
   aho
   ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q3$ echo aho | ./do
   aho
   

  bind.hs の方は今までも使っていたバインド演算子を使ったやり方です。 型を調べておきましょう。リスト6のようになります。

  • リスト6: bind.hsに関する関数の型
1
   2
   3
   4
   5
   6
Prelude> :t getContents
   getContents :: IO String
   Prelude> :t (>>=)
   (>>=) :: Monad m => m a -> (a -> m b) -> m b
   Prelude> :t putStr
   putStr :: String -> IO ()
   

ここから読み取るのは大変かもしれませんが、 引数に String をとる putStr 関数を使うために >>=IO String から IO を取っ払っています。

do.hs でもやっていることは全く同じです。 基本的な考え方は、 do の中では IO の部分を気にしなくてよいということです。 str <- getContents<- (左向き矢印演算子)は、 getContentsIO String の出力から、 String の部分だけ str に関連づけます。 その下の行で、 putStr str してこの do は終わりますが、 終わったときの putStr の出力の型は IO () なので、 main の型と一致します。

 ところで、なんで digdir' 関数で do を使わなければならないかというと、 パスがディレクトリかどうか判断する doesDirectoryExist 関数の出力が、 残念なことに Bool ではなく、

1
   2
   3
Prelude> import System.Directory
   Prelude System.Directory> :t doesDirectoryExist
   doesDirectoryExist :: FilePath -> IO Bool
   

・・・と IO Bool なので、 そのまま if 関数に突っ込めないからです。 if に突っ込むには IO を取らなければいけないのですが、 それをやるために16行目で左向き矢印演算子を使っています。 16行目のように b <- を使うと、 bIO Bool のうちの Bool に相当するものだけを指すようになります。 で、17行目で bif 関数に突っ込んでいます。 if 関数は、 bTruepath がディレクトリを指す) ならば digdir path を返し(再帰となる)、 そうでなければそのまま path を要素1個のリストにして、 returnIO をくっつけて返します。

 最後、13行目の return . concat を説明しておきます。 concat は、リスト7のように、 リストがリストになっているものを一つのリストに再構成するものです。

  • リスト7: concatの型と挙動
1
   2
   3
   4
Prelude> :t concat
   concat :: [[a]] -> [a]
   Prelude> concat [[1,2],[3],[4,5,6]]
   [1,2,3,4,5,6]
   

ですので、 mapM digdir' >>=[[FilePath]] が渡って来たものを [FilePath] にならし、 returnIO をくっつけて digdir の最終的な出力型 IO [FilePath] を実現しています。

6.6. おわりに

 今回は第1回の3問目で、 /etc 下のファイルリストを、 複数階層下までファイルを探して作るまでを行いました。 次回以降、探したファイルをコピーしたり書き換えたりと、 ゴリゴリ処理していきます。

 ・・・しかし、これシェル芸だと一瞬で終わってしまうことを 何ヶ月もかけてやっているような。 シェル芸って本当に便利ね・・・。

6.7. お知らせ:コード募集

 本稿で出た問題のHaskellのコードを、 名前あるいはハンドルネームと共に送ってください。 短いもの、あるいは変態的なものをお願いいたします。

email: 編集部のメールアドレス

脚注

[1]これ、働き始めた26歳からずーっと言い続けているような気がしないでもない。
[2]順に助教、アドバイザリーフェロー、会長。肩書き好きの天下り官僚のような並び方である。
[3]真面目に見せかけて酷いことを書いている。
[4]問題は /?page_id=684 から。
ノート   このエントリーをはてなブックマークに追加 
 

やり散らかし一覧

記事いろいろ