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

Sun Aug 3 16:28:36 JST 2014 (modified: Sun Oct 1 10:50:27 JST 2017)
views: 2173, keywords: この記事は最終更新日が7年以上前のものです。

出典:USPマガジン2014年5月号

2014年5月号:

各号の一覧へ

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

産業技術大学院・USP研究所・USP友の会 上田隆一 (脚注:順に助教、アドバイザリーフェロー、会長)

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

2.1. はじめに

皆さん、やさぐれてますか?ブラックエンジェル上田ちゃんです。最近、超ネガティブです。なぜ、ネガティブか。それは喋らずに本題に行きます(脚注:生きていればいろんなことがあるじゃないですか。)。

2.2. 前回の続き

さて、前回はまだ第1回の1問目について、解答を作ったところでした。ちょっと重複して申し訳ないんですが、問題と解答を書いてから話を始めます。

  • 問題(第1回第1問):(Linuxの) /etc/passwd から、

ユーザ名を抽出したリストを作ってください。

シェルのワンライナーだと以下のとおり。
ueda@ubuntu:~$ cat /etc/passwd | awk "-F:" '{print $1}'
   ...
   sshd
   ueda
   mysql
   postfix
   
  • 解答例
1
   2
   3
   4
   5
ueda@remote:~$ cat q1_1.hs
   main = getContents >>= putStr . main'

   main' :: String -> String
   main' cs = unlines $ map ( takeWhile (/= ':') ) ( lines cs )
   
  • 実行
ueda@remote:~$ ghc q1_1.hs
   [1 of 1] Compiling Main ( q1_1.hs, q1_1.o )
   Linking q1_1 ...
   ueda@remote:~$ cat /etc/passwd | ./q1_1
   ...
   ueda
   mysql
   postfix
   

んで、前回は map とか takeWhile (/= ':') を説明しようとして終わっていたのでした。

2.3. mapがないと困る

まず map から。 Haskellの対話的インタプリタGHCiで調べると、 型はこんなもんだと分かります。

1
   2
   3
   4
$ ghci
   Prelude> :t map
   map :: (a -> b) -> [a] -> [b]
   ###GHCiを抜けるときはCtrl+d###
   

map は、第一引数に型 a -> b関数 、 第二引数に型 a のリストを渡すと、型 b のリストを返すという意味になります。つまり、関数 map には別の関数とリストをひとつずつ指定して使います。 a とか b とかは、どんな型にもなれるもの(脚注:一定の条件あり)で、例えば a,b 共に String という型だとすると、

map :: (String -> String) -> [String] -> [String]
   

となります。解答のコードで使われる map はこのような型を持ちます。

こういう、普通の言語ではあまり見ないものを説明するには、なんでこんなもの必要なのかという切り口でいったほうがいいかなー(脚注:語尾に自信が感じられない。)。

GHCiを使って説明します。 GHCiでは、次のように変数を手で打って作ります。

Prelude> let str = "abcde"
   

文字列を操作する関数にreverseというものがあります。こいつに strをぶち込んでみます。

1
   2
Prelude> reverse str
   "edcba"
   

では、今度は次のようなリストを考えてみましょう。

1
Prelude> let strlst = [ "abcde", "fghij" ]
   

このリストの中の文字列両方に reverseをぶちかましたいときにどうするか?普通の言語ならfor文とかで一つずつ処理すればよいのですが、Haskellにはそんなもんありません。forなんか使っている言語は時代遅れの×▲□※♨︎です(脚注:言い過ぎ。)。

そこで、 map です。 mapは、リストの中の要素一つ一つに指定した関数をぶちかましてくれます(脚注:「ぶちかます」→「適用する」)。やってみまっしょい。

1
   2
Prelude> map reverse strlst
   ["edcba","jihgf"]
   

でけた。 Haskellにはforがありませんが、こんなふうに使わなくてもスマートにリストをさばく仕掛けがいろいろ用意されています。

ちなみに、あまり難しい用語を使いたくはないのですが、このような関数を「高階関数」と呼びます。考え方自体はあまり難しいものでもなくて、シェル芸人ならいつもやるような(脚注:国内で2,3人と思われる。)次のようなワンライナーと一緒です。

1
uedambp:USPMAG ueda$ find . | xargs -I@ cp @ @.org
   

find(1)から流れてきたファイル名一つ一つに cp(1)を適用して .org という拡張子でバックアップファイルを作るという処理ですが、Haskellのコードとよく見比べてみると、xargsmap の役割を果たしていることが分かります。

2.4. また高階関数かよ

さて、次に、 map 関数に放り込まれている takeWhile (/= ':') について。 まずは takeWhile の型を調べてみましょう。

1
   2
Prelude> :t takeWhile
   takeWhile :: (a -> Bool) -> [a] -> [a]
   

これも、第一引数に関数をとります。もうわけがわかりません。連載を続ける自信を無くしてきましたが、先に進みます。第二引数はなんらかのリストですね。細かい説明は放棄してとりあえずつこうてみましょーか。

 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
Prelude> let str = "aaabbcaabb"
   Prelude> :t str
   str :: [Char] <- 文字列はCharのリスト
   Prelude> takeWhile ( == 'a' ) str
   "aaa"
   Prelude> takeWhile ( /= 'c' ) str
   "aaabb"
   ### == 'a'や/= 'b'も関数 ###
   Prelude> :t (== 'a')
   (== 'a') :: Char -> Bool
   Prelude> :t (/= 'c')
   (/= 'c') :: Char -> Bool
   ### 「takeWhile (== 'a')」 は、文字列をとり文字列を返す
   Prelude> :t takeWhile (== 'a')
   takeWhile (== 'a') :: [Char] -> [Char]
   

つまり、条件を表す関数 == 'a'/= 'b'Bool 型の真を返すところまでのリストを返す 関数ということになります。したがって、解答のコードのtakeWhile (/= ':')は、「文字列を受け取ってコロンの前までの文字列を返す関数」ということになります。

2.5. んで /= ':' って何だよ。

/= ':' 」は、関数です。 上の例にもありましたがGHCiで型を調べてみましょう。 まず、 /= の型を調査。

1
   2
Prelude> :t (/=)
   (/=) :: Eq a => a -> a -> Bool
   

とりあえず Eq a => という部分は無視すると、第1引数に型 a 、第2引数に a をとって、Bool を返すことになってます。第1引数と第2引数を比較して同じならTrue 、違ったら 死ね と返します。間違えた。 False を返します。

1
   2
   3
   4
Prelude> "祝" == "祝"
   True
   Prelude> "祝" == "呪"
   False
   

/= ':' 」というのは、第1引数に ':' を指定しただけの関数です。

 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
###まずは型を調査###
   Prelude> :t (/= ':')
   (/= ':') :: Char -> Bool
   ###よく分からんので「f」という名前をつけてみる###
   Prelude> let f = (/= ':')
   Prelude> :t f
   f :: Char -> Bool
   ###確かに関数として振る舞う###
   Prelude> f ':'
   False
   Prelude> f ';'
   True
   

このようにHaskellの関数は、関数に中途半端に引数を与えて別の関数を作ることができます。これで、たくさん引数をとるような関数も、maptakeWhile で使えるようになるわけです(脚注:型が合えば。)。便利便利。

ここまで説明して、map ( takeWhile (/= ':') ) というのが、 「文字列のリストを引数にとり、リストの各要素のコロンより前の文字列だけを再度リストにまとめて返す関数」であることが分かります。やれやれ。

2.6. $ のパワーを利用した緩和政策について

さて、では次に map の左にある $ について。 $ というのは、 $ の右側を括弧で囲んでいるつもりになる記号です。 私のアホな説明ではよく分からんと思いますので、 例をお見せします。下の三つのコードは、 互いに全く同じ意味になります。

main' cs = unlines $ map ( takeWhile (/= ':') ) ( lines cs )
   main' cs = unlines ( map ( takeWhile (/= ':') ) ( lines cs ) )
   main' cs = unlines $ map ( takeWhile (/= ':') ) $ lines cs
   

Haskellのコードは $ なしで書こうと思ったら書けるのですが、上記2番目のコードのように括弧が増えると読みにくくなるので、適度に $ を使います。3番目のコードはちょっとやり過ぎのように個人的には思いますので、解答では1番目のコードを使いました。要はですね、これはLispをdisっているわけです。違いますか。違いますね。

最後、 unline は、文字列のリストを改行をはさんで連結する関数です。 $ の右側の関数は /etc/passwd に書いてあるユーザ名をリストで返してくるので、 unline で1行1ユーザ名でシュチュ力(脚注:出力。タイポが面白かったのでこのままで(おい)。)されてめでたしめでたしということになります。

2.7. 若干量が余ったので解答2行目も説明しておく

さて、どの問題でも共通の書き方になるので 触れるつもりがなかった解答の2行目ですが、 ちょっと余白ができそうなので解説しておきます (脚注:どうも手を抜いているように見えるかもしれませんが、 USP Magazineならではのフリーダムを味わっているだけです。)。

main = getContents >>= putStr . main'
   

main' からは String (厳密には [Char] ) が返ってきます。他の関数、記号の型を見てみましょう。

1
   2
   3
   4
   5
   6
   7
   8
   9
Prelude> :t getContents
   getContents :: IO String
   Prelude> :t putStr
   putStr :: String -> IO ()
   ### 演算子っぽいものにも型がある徹底っぷり ###
   Prelude> :t (>>=)
   (>>=) :: Monad m => m a -> (a -> m b) -> m b
   Prelude> :t (.)
   (.) :: (b -> c) -> (a -> b) -> a -> c
   

・・・説明する気が失せました。 しかし、本当に「型」というものが徹底されていますね、 Haskellは。

機能面からちゃんと説明しておくことにします。getContents は標準入力からデータを読む関数です。んで、よくよく考えると「標準入力からの入力」と、「関数に引数を入力」というのは同じ入力でも全く違うものです。このような違うものは関数をイコールでつないで扱えないので、このコードでは >>= という記号で右側の関数に標準入力から読んだものを投げています。

>>= の右側にある putStr . main' は、二つの関数がつながったもので、機能としては main'の返す文字列を標準出力に放出するという関数です。main' の型はGHCiで調べられないので、別の例を。reverseunwords という別々の関数をつなげて、「リストの文字列をひっくり返して空白区切りで連結する関数」を作り出します。

 1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
Prelude> :t reverse
   reverse :: [a] -> [a]
   Prelude> :t unwords
   unwords :: [String] -> String
   ### unwords と reverse をくっつける ###
   Prelude> :t unwords . reverse
   unwords . reverse :: [String] -> String
   ### 使う ###
   Prelude> (unwords . reverse) ["abc","def"]
   "def abc"
   ### 使う(その2) ###
   Prelude> let f = unwords .reverse
   Prelude> f ["abc","def"]
   "def abc"
   Prelude> :t f
   f :: [String] -> String
   

2.8. おわりに

では、本稿を締めたいと思います。諦めではありません。締めです。

  • Haskellでは関数を引数にとる関数がある(たくさん)。
  • このような関数がfor文がないHaskellでは重要な役割を担う。
  • map 関数はHaskellにおける xargs(1) である。
  • xargs(1) はシェル芸におけるmapである。
  • 高階関数とか難しいこと考えずにさらっと書けるようになりたいなあ(小並感)。
  • シェル芸って、ここまで説明する側に負担がないから楽だね。

次回、第1回の2問目に入ります!遅っ!

各号の一覧へ

ノート   このエントリーをはてなブックマークに追加 
 

やり散らかし一覧

記事いろいろ