すごい Haskell 読書会 in 大阪 #2

アジェンダ

  1. 自己紹介
  2. 前回の復習(関数定義のとこだけ)
  3. 第3章 関数の構文
  4. 第4章 Hello 再帰!(という名の演習)

自己紹介 icon

twitter:  @koko_u

名前:   小崎つねあき

仕事:   SI屋さん

Haskell歴: 1ヶ月

注意事項

復習(関数の定義)

関数定義(p.5)は

    add :: Int -> Int -> Int
    add x y = x + y

のように、関数名と引数リストと定義する式を = で繋ぎます

引数がなくてもよい。その場合は定数(あるいは変数)ということ

"=" は他の言語での「代入」ではなく、同値関係を表している

復習(式とは)

第1章、第2章で登場した要素で式とは

数値、文字、文字列

10
'a'
"hello"

リスト、タプル

[1, 2, 3]
('a', "young")

if .. then .. else

if pi == 3 then "ゆとり" else "おっさん"

復習(式とは)

関数適用

max 3 7
(+) 10 2

let式

let incr n = n + 1 in incr 100

case式

case list of []  -> "empty"
             [x] -> "single list"
             _   -> "uncountable list"

復習(関数の型宣言)

質問

関数を定義する場合は型宣言

    add :: Int -> Int -> Int

をするのが良い習慣(p.24)と書かれていますが、型宣言をすることでどのような利点があるのでしょうか?

復習(許される文字)

関数と変数は基本的には同じで、許されるのは「英数字」「_」「'」

先頭文字は「英小文字」(大文字はだめ)「_」

関数とは別に演算子(中置演算子)が定義できて、これは逆に「記号」のみから構成される。

    x ★ y = x * y

とかユニコードの記号でもイケる。でもどの位までOKなのかよくわからん。

ここまでで何か

第3章 関数の構文

パターンマッチ

関数を定義する時に、引数の「構造」に応じて定義式を場合分けする。

今の時点では引数のデータ構造としては

程度しかないので、強力さを実感しにくい。

第3章 関数の構文

単純な値のマッチ

    lucky 7 = "lucky seven"
    lucky x = "normal number"

タプルのマッチ

    addVector (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

再帰に便利

    mySum [] = 0
    mySum (n:ns) = n + mySum ns

第3章 関数の構文

ガード

関数を定義する時に、引数の性質に応じて定義式を場合分けする。普通に if 文を書くのと同じような感覚で

bmiTell bmi
  | bmi <= 18.5 = "You're underweight"
  | bmi <= 25.0 = "You're normal.\
                  \ I bet you're ugly!"
  | bmi <= 30.0 = "You're fat!"
  | otherwise   = "You're a whale!"

第3章 関数の構文

if 式で書くと残念な感じになるのでガードを使いましょう。

bmiTell bmi = if bmi <= 18.5
              then "You're underweight"
              else if bmi <= 25.0
                   then "You're normal.\
                       \ I bet you're ugly"
                   else if bmi <= 30.0
                        then "You're fat!"
                        else "You're a ..."

条件が並列に書けるのでガードの方が読みやすい

第3章 関数の構文

補足

ガードの定義で出てきた otherwise も関数で実体は True と同じ。そりゃそうだ。

文字列中で \ と \ の間の空白は無視される。長い文字列を折り返すのに使うみたい。

    pangram =
      "\
       \The quick brown fox\n\
       \jumps over the\n\
       \lazy dog.\n\
       \"

第3章 関数の構文

where

関数を定義する時、関数定義のスコープ内でしか使用しない変数(=関数)を定義するのに使う

bmiTell weight height
  | bmi <= 18.5 = "You're underweight"
  | bmi <= 25.0 = "You're normal.\
                  \ I bet you're ugly!"
  | bmi <= 30.0 = "You're fat!"
  | otherwise   = "You're a whale!"
  where bmi = weight / height ^ 2

第3章 関数の構文

where節で関数を定義してもいい。変数と関数は同じ

bmiTell weight height
  | skinny bmi  = "You're underweight"
  | normal bmi  = "You're normal.\
                  \ I bet you're ugly!"
  | fat bmi     = "You're fat!"
  | otherwise   = "You're a whale!"
  where bmi = weight / height ^ 2
        skinny x = x <= 18.5
        normal x = x > 18.5 && x <= 25.0
        fat    x = x > 25.0 && x <= 30.0

第3章 関数の構文

関数の定義は以上。

関数を定義する時に使う定義式(=右辺)の構成要素として新たに、「let式」と「case式」を使うことができる。

これらは「式」なので別段関数定義に特化した話ではない。 敢て言うなら、case 式は関数定義をするまでもないその場限りのパターンマッチと考えることもできる。

しかし p.49 は明らかに関数 what を定義した方が見易い。

ここまでで何か

リスト内包表記

関数定義とはあまり関係ないが、ちょいちょいリスト内包表記の話が差し込まれていて読み難い。

パターンマッチ

リスト内包表記のジェネレータでパターンマッチが使える。

マッチしない要素はスキップされる

    let xs = [(1,3), (4, 3), (2, 4)]
    [a + b | (a, b) <- xs ]

リスト内包表記

let式

let でリスト内包表記の出力部分やフィルターに使用できる変数や関数をバインドすることができる

   fatMen xs =
     [ p | p@(w, h) <- xs, let bmi = w/h^2; ⇨
           fat x = x > 25.0, fat bmi ]

よくある失敗

リスト内包表記でマッチしないパターンは除外される

    ghci> let ls = [(1,2),(3,4),(5,7),(3,8)]
    ghci> [ p | p@(3, _) <- ls ]
    [(3, 4), (3, 8)]

...

なるほど、でこれを利用して関数を定義してしまう

    choice :: Int->[(Int,Int)]->[(Int,Int)]
    choice n ls = [ p | p@(n, _) <- ls ]

よくある失敗

あぼーん(´・ェ・`)

    ghci> choice 3 [(1,2),(3,4),(5,7),(3,8)]
    [(1,2),(3,4),(5,7),(3,8)]

...

    choice :: Int->[(Int,Int)]->[(Int,Int)]
    choice n ls = [ p | p@(n, _) <- ls ]

リスト内包表記中の p@(n, _) <- ls は全てがマッチ

関数 choice の"引数" n とは別物!"代入"されたりしない

第4章 Hello再帰!

再帰がどんなものかわかればよい。

初めて「再帰関数」を見た人は各自で第4章の例を写経して下さい。

簡単ではない。特に遅延評価やら無限リストを考えると頭が痛くなる。

演習1

与えられた文字列を大文字に変換する関数を書きなさい

    ghci> upperCase "Hello World"
    "HELLO WORLD"

与えられた文字列から空白を除去する関数を書きなさい

    ghci> removeSpace "Hello World\nI'm John."
    "HelloWorldI'mJohn."

演習2

リスト lst1 が lst2 を含んでいるかどうかを判定する関数を書け

    ghci> [1..4] `cover` [2..4]
    True
    ghci> [1..4] `cover` [2..6]
    False

重複も考慮

    ghci> [1, 1, 2] `cover` [1, 1]
    True
    ghci> [1, 2] `cover` [1, 1]
    False

演習3

配列から重複する要素を削除する関数を書け

    ghci> uniq [1, 2, 1, 7, 6, 2, 4, 8]
    [1, 2, 7, 6, 4, 8]

順序は気にしなくてよいとしよう。

演習4

与えられた正数から二進表記の文字列を得る関数を書きなさい

    ghci> binExp 10
    "1010"

マイナスは、まあいいか。

演習5

hanoiハノイの塔

三つのポール 1 2 3 の内、ポール 1 に n 個の円盤が乗っているとし、すべての円盤をポール 2 に移動する手順を返す関数を書け。

手順はポール 1 からポール 2 に円盤を移動する場合にペア (1, 2) と表記し、そのようなペアのリストを手順と見なす

    ghci> hanoi 3
    [(1, 2), (1, 3), (2, 3), (1, 2), (3, 1), (3, 2), (1, 2)]

演習6

第 n 番目のフィボナッチ数を得る関数を書け、ただし 1番目と 2番目のフィボナッチ数は 1 とする

ダメな例

    fib :: Integer -> Integer
    fib 1 = 1
    fib 2 = 1
    fib n = fib (n-1) + fib (n-2)

fib 50 くらいで還らぬ人となります。

演習7

縦に n マス、横に k マスの碁盤目をAからBまで最短経路で移動する場合の数を求めよ

path

ダメな例...もういいよね。

    pathCount _ 0 = 1
    pathCount 0 _ = 1
    pathCount n k = pathCount (n-1) k +
                    pathCount n (k-1)