「ふつうのHaskellプログラミング」読みます(7)

あいだ空きまくり。やばい。読むだけなら12章くらいまで読んだ。型とか型クラス、モナドとかもなんとなくわかった気だけはする。でも実際処理系とたわむれんとよくわからん。

というわけで順番にやる。8章、基本的な構文。

8.1 値としての関数

高階関数テスト。letつけてるのはghci内だから。コンパイル実行するときはいらない。

Prelude> let square n = n * n
Prelude> map square [1, 2, 3, 4]
[1,4,9,16]

でもsquareがここでしか使わない関数なら、いちいちsquareっていう名前を付けてあげて、その関数定義を後生大事に抱えてる必要も無いよね。というわけでHaskellでは無名関数というものを作ることができます。

Prelude> map (\x -> x * x) [1, 2, 3, 4]
[1,4,9,16]

無名関数は複数のパターンマッチを列挙することができない以外は普通の関数定義と同様のパターンマッチができます。

タプルパターン。

Prelude> map (\(x,y) -> x - y) [(5,3), (1,4)]
[2,-3]

データコンストラクタパターンとか

Prelude> import Char
Prelude Char> map (\(x:xs) -> toUpper x : xs) ["hoge", "fuga", "moge"]
["Hoge","Fuga","Moge"]

8.2 関数合成

うん、数学者に好まれそうなHaskellらしさが見えてきた気がします。関数f, gを合成したfgとかよく使いますよね。単射がどうして全射がどうこうで全単射がほげほげとか。よく憶えてないけど。

まあそういう話は、考えたいときにじっくり考えればいいんですよね。手を動かしてみます。

Prelude> let numberOfLines = length . lines
Prelude> numberOfLines "aaa\nbbb\nc\nd"
4
Prelude> let numberOfWords = length . words
Prelude> numberOfWords "hoge fuga\nmoge piyo"
4
Prelude> import List
Prelude List> let sortLines = unlines . sort . lines
Prelude List> sortLines "hoge\nmoge\nfuga"
"fuga\nhoge\nmoge\n"

じゅんちょー。

Prelude List> let tac = unlines . reverse . reverse . reverse . unlines

<interactive>:1:20:
    Couldn't match expected type `String' against inferred type `Char'
      Expected type: [String] -> [String]
      Inferred type: [String] -> [Char]
    In the second argument of `(.)', namely
        `reverse . reverse . reverse . unlines'
    In the expression: unlines . reverse . reverse . reverse . unlines

なんだこれ。こうじゃねえのか。

Prelude List> let tac = unlines . reverse . reverse . reverse . lines
Prelude List> tac "hoge\nfuga"
"fuga\nhoge\n"

だいたい、unlinesの型って[String] -> Stringじゃなかったか。

Prelude List> unlines "hogehoge"

<interactive>:1:8:
    Couldn't match expected type `String' against inferred type `Char'
      Expected type: [String]
      Inferred type: [Char]
    In the first argument of `unlines', namely `"hogehoge"'
    In the expression: unlines "hogehoge"

そんなんできないよ。

8.3 部分適用

関数型言語の醍醐味の一つですね。Lisp系だとちょっとまわりくどくやることになるけど。MLとかHaskellだとむしろそれが基本。
関数の型をよく見て考えるとわかるかもしれないけど、MLとHaskell複数の引数を受けとる関数は持たない。引数無しか、引数を1つとる関数しか定義できない。n個の引数を取っているように見えるのは1つの引数をとって、n-1個の引数を取っているように見える関数を返している。


じゃあ早速やってみよう。

Prelude> let addThree i j k = i + j + k
Prelude> addThree 5

<interactive>:1:0:
    No instance for (Show (t -> t -> t))
      arising from a use of `print' at <interactive>:1:0-9
    Possible fix: add an instance declaration for (Show (t -> t -> t))
    In the expression: print it
    In a 'do' expression: print it

できねえじゃんエラーでまくりだよ! と思ったけどちょっと待ってエラーメッセージを雰囲気で読む。「なんかprintできねえ値わたされたんすけど勘弁してくれ」とな。ためしに、そもそもaddThreeを渡してみる。

Prelude> addThree

<interactive>:1:0:
    No instance for (Show (a -> a -> a -> a))
      arising from a use of `print' at <interactive>:1:0-7
    Possible fix:
      add an instance declaration for (Show (a -> a -> a -> a))
    In the expression: print it
    In a 'do' expression: print it

ghciではそもそもたいていのschemeとかOCamlインタプリタでできる、関数の表示ができないみたいですね。なんかの正確性のためにこの辺の利便性を捨てたのかな。これはちょっと不便。以下のような書き方をしても大丈夫なことから、部分適用できてんだなーということがわかりますね。

Prelude> ((addThree 5) 4) 3
12

えーと、この先読んだら上に書いた「2つ以上の引数を受けとる関数なんて無いよ!」みたいなこと書いてあった。

中置記法の場合はセクションといって特別な機能が用意されてるそうな。「特別な機能」てなんか嫌だな。

Prelude> 3 / 6
0.5
Prelude> (/ 6) 3
0.5
Prelude> 10^2
100
Prelude> (^ 2) 10
100

本では+を使ってたけど、+て引数の順序が結果に反映しないから例として微妙じゃないかな。

高階関数と組み合わせて。

Prelude> map (^ 2) [1, 2, 3, 4]
[1,4,9,16]
Prelude> filter ('\n' /= ) "hoge\nfuga\nmoge"
"hogefugamoge"

zipLineNumberをちいさくする。zipLineNumberてなんだ。おぼえてねえ。

Prelude> let zipLineNumber xs = zip [1..] xs
Prelude> zipLineNumber ["hoge", "fuga", "moge"]
[(1,"hoge"),(2,"fuga"),(3,"moge")]

おもいだした。

Prelude> let zipLineNumber = zip [1..]
Prelude> zipLineNumber ["hoge", "fuga", "moge"]
[(1,"hoge"),(2,"fuga"),(3,"moge")]

うん。「Haskellには2つ以上の引数を取る関数は無い」ってことを考えると、自然に理解できる気がします。

8.4 ポイントフリースタイル

関数定義から変数が消えていく話。

firstNLinesの変数を減らす。

Prelude> let firstNLines n cs = unlines $ take n $ lines cs
Prelude> firstNLines 2 "hoge\nfuga\nmoge"
"hoge\nfuga\n"
Prelude> let firstNLines n = unlines . take n . lines
Prelude> firstNLines 2 "hoge\nfuga\nmoge"
"hoge\nfuga\n"

fgrep.hsから変数を減らす。fgrep.hsどんなんだったか。

import System
import List
main = do args <- getArgs
          cs <- getContents
          putStr (fgrep (head args) cs)

fgrep pattern cs = unlines (filter match (lines cs))
  where
    match line = any prefixp (tails line)

    prefixp line = pattern `isPrefixOf` line

こんなんか。さすがにghci上でやるのはあきらめます。さて今まで見てきたものからして、まず関数定義の一番右にでてくる変数ってのは簡単に削れますよねたぶん。

import System
import List
main = do args <- getArgs
          cs <- getContents
          putStr (fgrep (head args) cs)

fgrep pattern = unlines . filter match . lines
  where
    match = any prefixp . tails

    prefixp = pattern `isPrefixOf`

こんなんですね。

c:\hoge\docs\futkel\8>ghc fgrep.hs -o fgrep

fgrep.hs:12:0: parse error (possibly incorrect indentation)

駄目だった。なんでだろうとしばらくわからんかった。こうだ。

import System
import List
main = do args <- getArgs
          cs <- getContents
          putStr (fgrep (head args) cs)

fgrep pattern = unlines . filter match . lines
  where
    match = any prefixp . tails

    prefixp = (pattern `isPrefixOf`)
c:\hoge\docs\futkel\8>ghc fgrep.hs -o fgrep
c:\hoge\docs\futkel\8>fgrep prefix <fgrep.hs
    match = any prefixp . tails
    prefixp = (pattern `isPrefixOf`)

できました。やはり中置記法は邪道ですね。isPrefixOfをわざわざ中置記法にしなければ、余計な括弧もいらなかったのです。prefixpの定義がこんだけ小さくなれば、いちいちprefixpなんて名前に束縛すんのも面倒ですよね。そのまんまmatch定義の中に書いちゃいましょう。

import System
import List
main = do args <- getArgs
          cs <- getContents
          putStr (fgrep (head args) cs)

fgrep pattern = unlines . filter match . lines
  where
    match = any (isPrefixOf pattern) . tails

ついでにisPrefixOfを中置記法にするなどという黒魔術をやめました。

あと、where節を消します。where節で上の方で定義してる変数使うとか、結局グローバル変数みたいな駄目さありますよね。

import System
import List
main = do args <- getArgs
          cs <- getContents
          putStr (fgrep (head args) cs)

fgrep pattern = unlines . filter (match pattern) . lines
match pattern = any (isPrefixOf pattern) . tails
c:\hoge\docs\futkel\8>ghc fgrep.hs -o fgrep
c:\hoge\docs\futkel\8>fgrep pattern <fgrep.hs
fgrep pattern = unlines . filter (match pattern) . lines
match pattern = any (isPrefixOf pattern) . tails

やった、ずいぶん短かくなりましたね。

ちなみに本には「型宣言は省略しない方がいいよ!」とありますね。個人的にはもうちょっと型推論の妙技に感じいりたい気分なので略せるだけ略してみようかなと。

8.6 練習問題

1 lstrip
Prelude> let lstrip = dropWhile (== ' ')
Prelude> lstrip "        hoge"
"hoge"
2 rstrip
Prelude> let rstrip = reverse . lstrip . reverse
Prelude> rstrip "hoge                   "
"hoge"
3 strip
Prelude> let strip = rstrip . lstrip
Prelude> strip "         hoge        "
"hoge"
4 tail.hsをポイントフリースタイルで

こんなんかな。

main = do cs <- getContents
          putStr $ lastNLines 2 cs

lastNLines n = unlines . takeLast n . lines

takeLast n = reverse . take n . reverse
c:\hoge\docs\futkel\8>tail <tail.hs

takeLast n = reverse . take n . reverse

できた。ちょっと忘れてた。やっべ。この章は基本ですね。重要重要。

test