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

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

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

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

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

5.1. はじめに

 こんにつわ。富山のホワイトシュリンプ上田のり平 [3] です。 今、締め切りが過ぎているのに気づいて 出張先の富山からお送りしております。 帰省ではありません出張です。 ちなみに、茹でるとプラスチックみたいに変態します。 これ豆な。

さて、そんな私信はどうでもいいんです。 HaskellですHaskell。やりましょう。 今回は第1回シェル芸勉強会の3問目です。

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

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

うわ・・・難しい・・・。どうしよ?リスカしよ [4] 。 ・・・じゃなくて、とりあえずやりましょう。

5.2. ディレクトリいじり

 さて、まずは /etc/ 下をまさぐるコードを書かねばいけません。 とりあえずリスト1のようなコードを書いてみました。 ls /etc/ に相当するプログラムです。 今のところ ls のように1段目のディレクトリしか調べられませんが、 この後で2段目3段目とディレクトリを降りていって 全てのファイルのリストを作るコードにしていきます。

  • リスト1: lsのようなもの(q1_3_1.hs)
1
   2
   3
   4
ueda@remote:~/GIT/USPMAG/SRC$ cat q1_3_1.hs
   import System.Directory

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

実行してみましょう。リスト2のようにずらずらとファイルや ディレクトリの名前が出てきます。

  • リスト2: q1_3_1の実行
 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
ueda@remote:~/GIT/USPMAG/SRC$ ghc q1_3_1.hs
   ueda@remote:~/GIT/USPMAG/SRC$ ./q1_3_1 | head -n 3
   insserv
   blkid.tab
   tidy.conf
   ###ls -f /etc/と同じ出力###
   ueda@remote:~/GIT/USPMAG/SRC$ ls -f /etc/ | head -n 3
   insserv
   blkid.tab
   tidy.conf
   

 リスト1のコードですが、次のような意味があります。 まず、 getDirectoryContents の型はリスト3のように、 FilePath を引数にとって、 IO [FilePath] を返すというものです。 FilePath というのは String の別名なのですが、 それはリスト3の後の二行のように、 :i (info) というコマンドで調べることができます。

  • リスト3: getDirectoryContentsの型とFilePathの情報
1
   2
   3
   4
   5
   6
   7
   8
ueda@remote:~/GIT/USPMAG/SRC$ ghci
   GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
   (略)
   Prelude> import System.Directory
   Prelude System.Directory> :t getDirectoryContents
   getDirectoryContents :: FilePath -> IO [FilePath]
   Prelude System.Directory> :i FilePath
   type FilePath = String -- Defined in `GHC.IO'
   

>>= の右側の putStr . unlines は、リストを unlines で1レコード1行にして、 putStr で標準出力に吐き出すということです。

5.3. IOってなんじゃい

 んで、とうとう IO の説明をするときがやってきてしまいました。 これが、Haskellを勉強しようとした100人のうち、 90人が死亡する原因となっているものです。

 今回はこれでおしまい。comming soon。

 ・・・じゃなくて、真面目にやってきます。 えーっと、 IO というのは、 外から何か読み込んだものにつけるレッテルみたいなものです。 外というのは例えば標準入力であったり、 あるいは q1_3_1 みたいにファイルの情報 だったりのことで、要はHaskellの世界に元々ないものです。 Haskellの世界には関数とその入出力しかないわけですが、 「外から読んだもの」は、そのどちらでもありません。 ですので、腫れ物のように扱われます。 その「腫れ物」である印が IO です。 外から読み込んで計算が修了するまで、 型についた「 IO 」は取り払うことができず、 普通の関数は IO hogehgoe という型を扱う事ができません。 しかし、少しだけ例外があり、 その例外の中でコソコソと普通の関数を使います。

 リスト1の4行目について、 行全体(main関数)の型を見てみましょう。 リストdddのようになります。

リストddd: q1_3_1.hsのmain関数の型

1
   2
   3
Prelude> import System.Directory
   Prelude System.Directory> :t (getDirectoryContents "/etc/" >>= putStr . unlines)
   (getDirectoryContents "/etc/" >>= putStr . unlines) :: IO ()
   

IO () という型であると分かりました。 関数としては何も引数にとらず、 ただ IO () を返すという意味になります。 () は「何にもねえよ」という意味です。 つまり、何も入出力しない関数であるとも解釈できますが、 プログラムを実行するとファイルのリストを標準出力に表示します。 実は、 IO () というのは、 「標準出力に何か表示する」という行為のことを指しています。 IO hogehoge という型を持っているものは、 「アクション」 [5] と呼ばれます。

 まだちょっと良く状況が分からんというところだと思いますが、 とりあえず次に行きます。 getDirectoryContents の型はリスト3で調べたので、 次は putStr . unlines について。

  • リスト4: putStr . unlinesの型
1
   2
   3
   4
   5
   6
Prelude System.Directory> :t putStr
   putStr :: String -> IO ()
   Prelude System.Directory> :t unlines
   unlines :: [String] -> String
   Prelude System.Directory> :t (putStr . unlines)
   (putStr . unlines) :: [String] -> IO ()
   

リスト4のように、 putStr . unlines の型は「文字列のリストを受け取ってアクションを返す」 です。

 次、謎の記号 >>= ですが、リスト5のような感じです。 ちなみに >>= はバインド演算子と呼ばれます。

  • リスト5: >>=の型
1
   2
Prelude System.Directory> :t (>>=)
   (>>=) :: Monad m => m a -> (a -> m b) -> m b
   

Monad m => というのはまた別の機会に説明するとして [6] とりあえず m a -> (a -> m b) -> m b を解釈してみましょう。 これは m aa -> m b という関数をとって、 m b を返すということです。 リスト1の4行目について、書き換えてみましょう。 >>= を演算子ではなく関数のように扱ってみます。 リストgggのように、ちゃんと動きます。

  • リストggg: >>= を関数のように扱う
1
   2
   3
   4
   5
   6
   7
   8
Prelude System.Directory> (>>=) (getDirectoryContents "/etc/") (putStr . unlines)
   (中略)
   .
   ..
   afpovertcp.cfg
   aliases
   aliases.db
   ...
   

リスト5の型とリストgggを比べてみましょう。 m agetDirectoryContents "/etc/" の型 IO [FilePath] に相当するので、 mIOa[FilePath] ということになります。 そして、 a -> m bputStr . unlines の型 [String] -> IO () に対応します。 a[String] つまり [FilePath]mIOb() となります。ちょっと紙に書いて確かめていただきたいのですが、 型は互いに矛盾していません。

5.4. アクションを避けて普通の関数を使う

 型は矛盾していないのですが、また疑問が浮かびます。 リスト6のように、 unlines 関数はアクションとは無関係の関数です。 が、アクションだらけのところで使う事ができています。 これはなぜでしょう?

  • リスト6: unlinesの型
1
   2
Prelude System.Directory> :t unlines
   unlines :: [String] -> String
   

 もう一度 >>= の型を見てみます。

(>>=) :: Monad m => m a -> (a -> m b) -> m b
   

これの mIO に対応するので、 置き換えてみましょう。

IO a -> (a -> IO b) -> IO b
   

関数の型 a -> IO b のところで、 a から IO が取れています。 結局これが >>= の役割で、 アクションの中身( IO aa の部分) に普通の関数を適用できるようにしています。 ただし、 a -> IO b とあるように、 普通の関数を適用した後の出力には再び IO をかぶせなければなりません。

5.5. 親と自分を消し去る

 さて、 q1_3_1 の出力のなかには、 リスト7のように親を指す .. と、自分を指す . が混ざっています。 もう一度やりたいことをおさらいすると、 今はこのコードを加筆して /etc/ 下のファイルのリストを再帰的に作りたいのですが、 ... が混ざっていると再帰処理が終わりません。

  • リスト7: ”.”と”..”が混ざりんぐ
1
   2
   3
ueda@remote:~/GIT/USPMAG/SRC$ ./q1_3_1 | grep '^\\.\\.*$'
   .
   ..
   

ということで、少し書き進めてリスト8のようなコードを作りました。 ls という関数を作りました。

  • リスト8: q1_3_2.hs
1
   2
   3
   4
   5
   6
   7
ueda@remote:~/GIT/USPMAG/SRC$ cat q1_3_2.hs
   import System.Directory

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

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

... が消えていることを確認しましょう。 リスト9のように、ドットで始まる行を抽出することで確認しました。

  • リスト9: q1_3_2の実行
1
   2
   3
ueda@remote:~/GIT/USPMAG/SRC$ ghc q1_3_2.hs
   ueda@remote:~/GIT/USPMAG/SRC$ ./q1_3_2 | grep '^\\.'
   .pwd.lock
   

 さて、リスト8の6,7行目の型を追っていきましょう。 6行目のように関数 ls [7] の型は String -> IO [FilePath] ということで、 getDirectoryContents と同じです。 7行目の ls の中身は、 getDirectoryContents dir >>= で、 IO [FilePath] から IO をはぎ取って、 return . filter (`notElem` [".",".."]) に渡しています。 この部分(「右辺」と呼びましょう)の型は、 リスト10のようになります。 [[Char]][FilePath] に置き換えて読むと、 ファイルパスのリストを受け取って、 ファイルパスのリストに m つまり IO をかぶせて出力するというものになっています。

  • リスト10: return...の型
1
   2
   3
Prelude> :t (return . filter (`notElem` [".",".."]) )
   (return . filter (`notElem` [".",".."]) )
    :: Monad m => [[Char]] -> m [[Char]]
   

右辺で面白いのは return です。 これも関数で、普通の言語の return とは全然違うものです。 リスト11のように、普通の型に IO をかぶせて出力します。 q1_3_2.hs では、「 >>= の出力には IO がかぶさっていないといけない」 という条件を満たすために利用しています。

  • リスト11: returnの型
1
   2
Prelude> :t return
   return :: Monad m => a -> m a
   

ということは、右辺から return を除いた filter (`notElem` [".", ".."]) の部分は、 リスト12のように IO の呪縛から解放され、 文字列を処理することに専念できるようになります。

  • リスト12: IOの呪縛から解放された部分
1
   2
Prelude> :t filter (`notElem` [".", ".."])
   filter (`notElem` [".", ".."]) :: [[Char]] -> [[Char]]
   

notElem の使い方をリスト13に示して、 今回はおしまいとします。

  • リスト13: notElem関数
 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
Prelude> :t notElem
   notElem :: Eq a => a -> [a] -> Bool
   Prelude> notElem 1 [1,2,3]
   False
   Prelude> notElem 4 [1,2,3]
   True
   ###関数を演算子として使うときには``で関数を囲む###
   Prelude> 1 `notElem` [1,2,3]
   False
   ###filterと併用するとリストから指定した要素を削除できる###
   Prelude> filter (`notElem` [1,2,3]) [1,2,3,4,5]
   [4,5]
   

5.6. おわりに

 今回は第1回シェル芸勉強会の3問目で、 ファイルの一覧をHaskellで処理する部分を書きました。 「アクション」面倒ですね・・・。 慣れが必要です。 ただ、私も完全に理解してはいないのですが、 数学的に正しいことを突き詰めた結果、 面倒くさいことになっているようです。 この手のものは慣れたらかえって自然に感じるものです。 こうやって、慣れた人と慣れていない人の溝は、 深まるばかりなんだなあ・・・。

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

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

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

脚注

[1]USP友の会の幹事Uさんの夢の中でこのペンネームが誕生。
[2]順に助教、アドバイザリーフェロー、会長。ただし、今回はペンネームなので、名乗ってよいのかどうかは微妙。
[3]蛇足であるが本名は富山のブラックエンジェル上田隆一である。
[4]お菓子ではない。おかしいことは認める。
[5]当然、漫画雑誌のことではない。
[6]先月号で show の型を説明するときにも => が出てきました。
[7]紛らわしい・・・
ノート   このエントリーをはてなブックマークに追加 
 

やり散らかし一覧

記事いろいろ