Haskellメモ

入門Haskellプログラミング LESSON 15まで。続きはUNIT 3から。

コンパイル

ghc -o 実行ファイル名 haskellファイル.hs
main = do
  print "Hello World"

関数定義

関数名 引数 = 本文の書式で定義できる。

add a b = a + b
bmi w h = w / (h^2)

where句

関数内で利用できる変数を定義できる。

-- 健康体重との差を計算する
diff_healthy_weight w h =
  w - healthy_weight
  where
    square = h^2
    healthy_weight = square * healthy_weight_bmi
    healthy_weight_bmi = 22

他の言語の変数のように思えるが、定義順序に意味はない。他の言語的に考えるならローカルなメソッドや関数を定義していると考えても良い。

# Rubyコード
def diff_healthy_weight(w, h)
  w - healthy_weight(h)
end

def square(h)
  h ** 2
end

def healthy_weight(h)
  square(h) * healthy_weight_bmi
end

def healthy_weight_bmi
  22
end

let式

他の言語的にいえばブロックに相当するもの。新しいスコープを作る。

let 変数定義 in 本文という構文。

x a =
  (let
    a = 2
   in
    a) * a

-- 6が返る
x 3

上記の例の場合、let式によってaの返す値がlet式の中だけ2になる。let式はin aとなっているのでlet式内でのaの値、つまり2を返す。let式の外ではaは元の値になるのでx 32 * 3ということになる。

条件式

if式とcase式がある。

x a =
  if odd a then
    "odd"
  else
    "even"
  end

y a =
  case a of
    1 -> "1"
    2 -> "2"
    _ -> "other"

パターンマッチ

y a =
  case a of
    1 -> "1"
    2 -> "2"
    _ -> "other"

上記のようなcase式を使ったコードはパターンマッチで書き換えることができる。

y 1 = "1"
y 2 = "2"
y a = "other"

さらに、リストやタプルの場合、以下のように複雑なマッチングも可能。

my_sum [] = 0
-- my_sum [1,2,3] なら x は1、xs は[2, 3]
my_sum (x:xs) =
  x + (my_sum xs)

-- tuple_sum (1, 2) なら a は1、b は2
tuple_sum (a, b) = a + b
map tuple_sum (zip [1..5] [1..5]) -- [2,4,6,8,10]

ラムダ関数

\引数 -> 本文の書式で定義できる。即時利用する場合は(\引数 -> 本文) 実引数のようにする。

-- 二人のbmiの差を求める
diff_bmi aw ah bw bh =
  (bmi aw ah) - (bmi bw bh)
  where
    bmi = \w h -> w / (h^2)

ファーストクラス関数

関数は引数として渡すことも、戻り値として戻すこともできる。

x f a =
  if f a then
    odd
  else
    even

上記の関数でx odd 4とした場合、if f a thenif odd 4 thenとして評価される。4は偶数でodd 4Trueなのでoddがx関数の戻り値として返る。(x odd 4) 5を実行した場合、odd 5になるのでFalseになる。

高階関数

別の関数を受け取る関数のこと。map関数など。

x f a b = f a b

x (+) 1 2 -- 1 + 2 が実行される

クロージャ

func a = \x -> a * x

func2 a =
  (x3 a) + (x4 a)
  where
    x3 = func 3 -- \x -> 3 * x のラムダ関数
    x4 = func 4 -- \x -> 4 * x のラムダ関数

func2 3 -- 21が返る

func関数はラムダ関数を返す。ラムダ関数の本文にはfunc関数のa引数が使われており、func関数を読んだ時の引数が束縛されたままになる。結果としてfunc 3を呼び出せば\x -> 3 *xのラムダ関数が返る。

部分適用

Haskellでは引数の一部だけ指定済の関数を作ることができる。

bmi h w = w / (h^2)

-- 身長180cm
bmi_h180 = bmi 1.8

-- 身長180cm 体重60kgのbmiを計算
bmi_h180 60

リスト操作

リストの要素は全て同じ型でなければならない。

list = [1, 2, 3]
head list -- 1
tail list -- [2, 3]
0 : list -- [0, 1, 2, 3]
[0] ++ list -- [0, 1, 2, 3]

[1..3] -- [1, 2, 3]
[1,3..5] -- [1, 3, 5]
[1,1.5..3] -- [1.0, 1.5, 2.0, 2.5, 3.0]
[1,0.5 .. 0] -- [1.0, 0.5, 0.0]
[1..] -- 1から始まる無限リスト
[1..] !! 2 -- 3
length [1..3] -- 3
reverse [1..3] -- [3, 2, 1]

-- ruby: (1..3).include?(2)
elem 2 [1..3] -- True

take 2 [1..5] -- [1, 2]
drop 2 [1..5] -- [3, 4, 5]
zip [1,3..5] [2,4..6] -- [(1,2),(3,4),(5,6)]
cycle [1..3] -- [1, 2, 3, 1, 2, 3 ...] のように引数のリストを無限に繰り返す無限リスト

高階関数のリスト操作

map (2 *) [1..3] -- [2, 4, 6]
filter (3 <) [1,3..5] -- [5]

-- ruby: (1..3).inject(0, :+)
foldl (+) 0 [1..3] -- 6

遅延評価

Haskellでは必要になるまでコードの評価は行われない。

func x =
  if x == 0 then
    x
  else
    func (x - 1)

result = func 1000000

上記のようなコードの場合、resultの値を参照しない限りfunc 1000000は実行されない。適当なファイルに保存してGHCiで:l ファイルをした上で、resultを指定すると多少時間がかかって0が返ってくるのがわかる。これは、list = [1..]のような無限リストの場合でも同じであり、head(tail [1..])のようなコードを実行しても、無限リスト全体が評価されることなくすぐに2が返ってくる。

変数の書式は変数名 :: 型になる。関数の書式は関数名 :: 引数の型 -> 戻り値の型になる。

x :: Integer
x = 100

func :: Integer -> Integer
func x = x^2

-- 引数が複数の場合
-- 関数名 :: 第一引数の型 -> 第二引数の型 -> 戻り値の型
func2 :: Integer -> Integer -> Integer
func2 x y = x * y

-- 高階関数の場合
-- 関数名 :: ( 引数の型 -> 戻り値の型 ) -> 引数の型 -> 戻り値の型
func3 :: ( Integer -> Integer ) -> Integer -> Integer
func3 f a = f (a * 2)

func3 (\x -> x * 3) 2

型変数

特定の型に限定したくない場合や任意の型だが同じ型に制限したい場合に型変数が使える。

-- func関数の第一引数の関数の引数とfunc関数の第二引数は同じ型
-- func関数の第一引数の関数の戻り値とfunc関数の戻り値は同じ型
func :: ( a -> b ) -> a -> b
func f a = f a

func (\x -> (fromIntegral x) / 2) 100 -- 50.0

型シノニム

型にエイリアスを作る。type シノニム名 = 元の型の書式。

type Height = Integer
type Width = Integer
type Area = Integer

area :: Height -> Width -> Area
area h w = h * w

ただし、シノニム名と一致しているかはチェックされないため、以下のようなコードでもエラーにならない。

-- Height型の変数
h = 10 :: Height

area h h -- 100が返る

新しい型を作る

data Code = Code1 | Code2 | Code3

func :: Code -> String
func Code1 = "code1"
func Code2 = "code2"
func Code3 = "code3"

func Code1 -- "code1"を返す

上記の例の場合、Codeが新しい型で、Code1Code2はデータコンストラクタという。

以下のように別の型を利用して新しい型を作ることができる。

-- data Code の Code は型名
-- Code Int String の Code はデータコンストラクタ名
data Code = Code Int String

func :: Code -> String
-- (Code i s)の部分はパターンマッチ
func (Code i s) = foldl (++) [] (take i (cycle [s]))

-- (Code 3 "abc") の部分は
-- Code がデータコンストラクタで
-- 3 が Int に
-- "abc" が String に対応する
func (Code 3 "abc") -- "abcabcabc"を返す

レコード構文

オブジェクト指向言語でいうところのgetter、setter的なものを定義できる。 name (Code _ name) = nameのように関数を定義することもできるが、レコード構文を使うと記述が簡略化できる。

data Code = Code {
   no :: Int,
   name :: String
  }

code = Code 200 "OK"

no code -- 200
name code -- "OK"

code2 = code { name = "OK!" }

-- 元の変数は変わっていない
name code -- "OK"

-- 新しい変数code2のnameだけ変わっている
no code2 -- 200
name code2 -- "OK!"

型クラス

一連のメソッド(型クラスの関数はメソッドという)を定義して、その関数に渡せる型と実装を定義できる。

-- 型クラス Foo を定義 a は型変数
class Foo a where
  -- 型クラス Foo のメソッド
  hoge :: a -> String
  piyo :: a -> a
  fuga :: a -> String
  -- デフォルト実装
  fuga n = hoge (piyo n)

-- 型クラス Foo のインスタンス
instance Foo Int where
  -- メソッドの実装
  hoge 1 = "1!"
  hoge 2 = "2!"
  hoge n = "many!"
  piyo n = n * 2
  -- fuga はデフォルト実装があるので実装しない

hoge (1::Int) -- "1!"
hoge (3::Int) -- "many!"
piyo (3::Int) -- 6
fuga (1::Int) -- "2!"
fuga (2::Int) -- "many!"

fuga (1.1::Float) -- Foo Floatのインスタンスがないのでエラー

型クラスはJavaなどのインターフェースと比較されることが多いようだが、インターフェースは「型に関数が所属する(ないしクラスにインターフェースが所属する)」が型クラスの場合は「関数に型が所属する(ないし型クラスに型が所属する)」と考えた方が良さそうである。

インターフェースの場合はすでに定義済のクラスに後付けでインターフェースを追加することはできないが、型クラスの場合はそのような制限はない。

型クラスを関数定義で使う

関数の引数などが型クラスのインスタンスの型であることを明示したい場合、以下のようにする。

-- 引数が型クラス Foo と Bar のインスタンスの型であることを強制している
baz :: (Foo a, Bar a) => a -> String
baz a = fuga (bar n)

型クラスを型クラスで使う

関数定義で型クラスを利用したように型クラスでも利用することができる。

-- 型変数 a は型クラス Eq のインスタンスの型でなければならない
class Eq a => Foo a where
  -- ...

型クラスの派生

新しい型も型クラスのインスタンスにすることができる。

data Code = Code1 | Code2 | Code3

-- Show は組込の型クラス
instance Show Code where
  -- showメソッドは文字列を引数に変換して返す
  show Code1 = "Code1"
  show Code2 = "Code2"
  show Code3 = "Code3"

show Code1 -- "Code1"

ただ、デフォルト実装でよければインスタンスの宣言を省略できる記法がある。

data Code = Code1 | Code2 | Code3 deriving (Show)