プログラミングHaskellを読む (4)

第4章 関数定義

関数定義で使えるワザ

  • 既存の関数を利用する
  • if文
    • 一番単純なパターンマッチ
    • thenとelseが必須
      • then, else節は型が同じ必要がある
      • 必須なのでいわゆる「ぶらさがりelse問題」が発生しない
-- 絶対値
abs1 n = if n >= 0 then n else -n
-- 整数の符号を返す
signum1 n = if n < 0 then -1 else
              if n == 0 then 0 else 1
  • ガード付きの等式
    • ガードという論理式が並ぶ
    • ガードに、=をつけて処理を書く
    • 上から評価してTrueの部分の処理が実行される
      • otherwiseはTrueと等価。
abs2 n | n >= 0 = n
       | otherwise = -n

signum2 n | n < 0 = -1
          | n == 0 = 0
          | otherwise = 1
  • パターンマッチ
    • 引数でパターンを表現
    • ワイルドカード'_'が使える
    • タプル・パターン、リスト・パターン(cons演算子':'を利用)、n+k・パターン
-- タプル・パターン
fst :: (a, b) -> a
fst x _ = x
snd :: (a, b) -> b
snd _ y = y
-- リスト・パターン
null :: [a] -> Bool
null [] = True
null _ = False
head :: [a] => a
head (x : _) = x
{-
head x : _ = x
だとどうなるかというと、関数のほうがcons演算子より結合度が高いので
(head x) : _ = x
と解釈される。なので括弧は必須。
-}

-- n+k・パターン
pred 0 = 0
pred (n + 1) = n
{-
k以上の整数の時にマッチする。
n+kは将来使えなくなるかもしれない。
-}
  • λ式
    • 要するに無名関数
    • カリー化を明確に表現できる
add x y = x + y
add' = \ x -> (\ y -> x + y)

-- λを使わない場合
const :: a -> b -> a
const x _ = x
{-
a = const 10
カリー化されているのでaを呼び出せば常に10が返る
-}
-- λを使うと意図が明確
const' x = \ _ -> x
  • セクション
    • 演算子:中置の関数
    • ここでは書かれていないが、2文字以下で一部の記号で構成された関数は、カッコを付けて宣言することで、バッククオートを付けずに中置に出来るらしい。
    • 演算子は括弧をつければ普通の関数なので、foldrのようなライブラリ関数にも使える
-- λで形式化出来る。?は何らかの記号
(?) = \ x -> (\ y -> x ? y)
(x ?) = \y -> x ? y
(? x) = \x -> x ? y

練習問題

1. ライブラリ関数を使ったhalveの実装
halve :: [a] -> ([a], [a])
halve x = (take hl x, drop hl x) where hl = (length x) `div` 2
2. safetailを定義
-- 条件式
safetail :: [a] -> [a]
safetail x = if null x then []
             else tail x

-- ガード付きの等式
safetail' :: [a] -> [a]
safetail' x | null x = []
            | otherwise = tail x
            
-- パターンマッチ
safetail'' :: [a] -> [a]
safetail'' [] = []
safetail'' (_ : xs) = xs

私は一番パターンマッチが自然に感じます。

3. 論理和演算子or
or1 :: Bool -> Bool -> Bool
True `or1` True = True
False `or1` True = True
True `or1` False = True
False `or1` False = False

or2 :: Bool -> Bool -> Bool
False `or2` False = False
_ `or2` _ = True

or3 :: Bool -> Bool -> Bool
False `or3` b = b
True `or3` _ = True

or4 :: Bool -> Bool -> Bool
b `or4` c | b == c = b
          | otherwise = True

ついでに確認するための関数を作成してみた。

checkOr :: (Bool -> Bool -> Bool) -> [Bool]
checkOr or = [a `or` b | a <- [True, False], b <- [True, False]]
*Main> checkOr (||)
[True,True,True,False]
*Main> checkOr or1
[True,True,True,False]
*Main> checkOr or2
[True,True,True,False]
*Main> checkOr or3
[True,True,True,False]
*Main> checkOr or4
[True,True,True,False]
4. 論理演算子を条件式で再定義 (1)
{-
True && True = True
_ && _ = False
-}
and1 :: Bool -> Bool -> Bool
and1 a b = if a then
             if b then
               True
             else
               False
           else
             False

答えをチェックするに当たり、よく考えたら、前問のcheckOrがそのままつかるので、汎用メソッドにしてみました。

checkBool :: (Bool -> Bool -> Bool) -> [Bool]
checkBool op = [a `op` b | a <- [True, False], b <- [True, False]]
*Main> checkBool (&&)
[True,False,False,False]
*Main> checkBool and1
[True,False,False,False]
5. 論理演算子を条件式で再定義 (2)
{-
True && b = b
False && _ = False
-}
and2 :: Bool -> Bool -> Bool
and2 a b = if a then
             b
           else
             False
6. カリー化された関数をラムダ式で表現
-- mult x y z = x * y * z
mult = \x -> (\y -> (\z -> x * y * z))