最近,Haskell
を少し触っていたので,少しHaskell
に関してアウトプットしようと思います.
はじめに
Haskell
はすごく良い言語であり,他の言語にないような様々な概念を有しています.
しかし学習コストが高く,結構大変な言語だとも思います.
なので,本記事では,よりざっくりしたHaskell
の概念と小さなコードを紹介していきなんとなく理解できるような文章を目指します.
想定読者
-
Haskell
をざっくり知りたい人 - CやJavaの手続き型言語を軽くやったことがある人
- 再帰的にいろいろあれこれする考え方のある人
実行環境
Haskell
の実行には,Stackというツールを利用します.
以下のコマンドでセットアップします.(Mac)
brew install stack
stack setup
すこし時間がかかるので,待ちます.
Hello World
何はともあれHello world
です.
main = putStrLn "Hello world!"
-- output: Hello world!
実行は以下のコマンドでします.
stack runghc -- helloworld.hs
Haskell
では,まずmain
関数があり,そしてその処理内容を右辺に書きます.
ここではputStrLn
関数を利用して,文字列を出力しています.(printlnのようなもの)
関数定義
関数はmain
関数のように定義します.
関数f
は,ただ文字列を返すだけの関数です.
Haskell
にreturn
文は必要ではないので,より簡潔に関数を定義できます.
f = "Hello world!" -- 関数定義
main = putStrLn f
-- output: Hello world!
パターンマッチ
Haskell
では,よく処理を再帰的に書くことがあります.
なので再帰を簡潔に書けるように,パターンマッチというものがあります.
パターンマッチは,関数の定義を複数列挙し,マッチした関数の引数のパターンの右辺を実行するという動作をします.
以下に階乗計算をするコードを載せます.
factorial 0 = 1 -- 引数が0の時
factorial n = n * factorial (n - 1); -- 引数が0以外の時
main = print (factorial 10)
-- output: 3628800
factorial
関数が2行分定義されていることがわかると思います.
これは引数が0
,n
のときの2つの分岐を書いていることになります.
なので,普通の言語だとif (n == 0)
と書くことが多いですが,
Haskell
ではどういう値が来た時にどういった値を返すかを直感的に書け,より簡潔に関数を定義できます.
同じ動きのJava
のコードを載せておくので比較してみると面白いかもです.
import java.io.*;
import java.util.*;
class Main {
static int factorial (int n) {
if (n == 0) {
return 1;
}
return n * factorial (n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(10));
}
}
Listのパターンマッチ
パターンマッチを利用して,Listの状態を判断することができます.
以下にただListの要素を足し続けるだけの関数を定義してみます.
f :: [Integer] -> Integer
f (x:xs) = x + f xs -- 先頭の要素がある時 (リストの長さが1以上)
f [] = 0 -- リストが空
main = print $ f [1..5]
-- output: 15
関数f
では,(x:xs)
というパターンと[]
というパターンを判断しています.
(x:xs)
ですが,これは先頭の要素があるかどうかを判断します.
そして[]
は,見た通り空のリストにマッチします.
もう一つ,関数定義の1行目のところにf :: [Integer] -> Integer
という記述があります.
これは関数の型を定義しており,[Integer](IntegerのList)
を受け取り,Integer
を返す関数ということを示しています.
Haskell
は,->
の最後の型が戻り値でそれ以外が引数という形で定義します.(一部例外あり)
余談ですが,上のコードは,sum [1..5]
でも一緒の動きをします.
普通はこちらを使うと思います.
foldlとfoldrという関数
Haskell
には,foldl
とfoldr
という関数があります.
これは両方とも畳み込む関数ですが,方向が違います.
ここでの畳み込みという表現ですが,初期値とリストを与えリストの要素をどんどん初期値に足し合わせていくイメージのものを指します.
foldl
は,左から畳み込みます.
そしてfoldr
は,右から畳み込みます.
用途的には結合則を満たさない演算(左から計算した結果と右から計算した結果が同じ結果にならないもの)で使い分けたり,
foldr
はリストの生成順序と同一なので,リストを連結していく時に使ったりします.(多分)
以下に例を示します.
main = print $ foldr (:) [] [1, 2, 3] -- 引数は,結合する関数, 初期値, 与えるリスト
-- output: [1,2,3]
これをfoldl
でやろうとすると以下のようなエラーが出ます.
• Occurs check: cannot construct the infinite type: a ~ [a]
Expected type: [a] -> [a] -> [a]
Actual type: a -> [a] -> [a]
この理由として,リストに要素を追加する:
は,a : [b]
のように利用します.
しかしfoldl
では[b] : a
のように利用しようとするのでエラーを吐きます.
これはfoldl
が左から結合していくからです.
なので使い分けが大事になってきます.
代数的データ型 (Listを自分で実装してみる)
ここでHaskell
の代数的データ型を用いて,Listの動きを実装してみましょう.
以下のコードは,Listというデータ型を定義しListの中身を合計する関数を定義したものです.
data List a = Nil | Cons a (List a) -- Listを定義
initList :: List Integer -- 初期リストを定義
initList = Cons 5 (Cons 4 (Cons 3 (Cons 2 (Cons 1 Nil))))
sumList :: List Integer -> Integer -- リストの中身の合計を計算する関数
sumList (Cons a b) = a + sumList b
sumList Nil = 0
main = print $ sumList initList
-- output: 15
ここで,List
は**Nil
もしくはCons a (List a)
**という型の両方を表現しています.
そしてa
は,型パラメータといい何型が入って来ても良いということを表現しています.
sumList
では,List
型を受け取りパターンマッチをしています.
やっていることは,Cons a b
にマッチしたらa
,b
を取り出し,右辺の処理を実行します.
そしてNil
にマッチしたら0
を返します.
このようにデータ型のパターンマッチも簡単に実装できます.
型クラス
Haskell
には型クラスという概念があります.
感覚としては,インターフェースが近い気がします.
型クラスで定義されている関数をデータ型に実装することで,データ型がその型クラスのインスタンスになります.
そしてその型クラスの制約のもとで,そのデータ型を取り扱えます.
さっそく先ほどのList型をインスタンス化しましょう.
今回はfoldr
を実行するためにFoldable class
の関数を実装し,インスタンス化します.
なぜインスタンス化しないといけないか?なのですが,まずはFoldable
の定義を見てみましょう.
class Foldable (t :: * -> *) where # 型パラメータ t
Data.Foldable.fold :: Monoid m => t m -> m
foldMap :: Monoid m => (a -> m) -> t a -> m
foldr :: (a -> b -> b) -> b -> t a -> b # 3つ目の引数にt型を要求している
...
ここでは,class Foldable
と書かれており,その次に(t :: * -> *)
と書かれています.
このt
というのは,型を見ると何かを受け取り,何かを返す関数ですね.
ここで関数と言うと若干わかりづらいのですが,データ型も関数として機能します.
data List a
は定義としては,a -> List a
になるので* -> *
と同一になりますね.(おそらく)
そしてList
をt
として受け取り,foldrでは3つ目の引数にt
型すなわちList
型に包まれた引数を要求します.
このように型クラスは,ユーザが定義したデータ型を受け取り,それに対応したfoldr
を実行できます.
こういった制約のことを型クラス制約と呼び,これにより既存の関数に自身の型を適応させていくことが比較的容易にできます.
以下が完成形のコードです.
data List a = Nil | Cons a (List a) deriving (Show) -- Show classのインスタンスを作成する
initList :: List Integer -- 初期リスト
initList = Cons 5 (Cons 4 (Cons 3 (Cons 2 (Cons 1 Nil))))
instance Foldable List where -- Foldable classのインスタンスを作成する
foldr f z (Cons a b) = f a (foldr f z b)
foldr f z Nil = z
main = print $ foldr Cons Nil initList
-- output: Cons 5 (Cons 4 (Cons 3 (Cons 2 (Cons 1 Nil))))
ここでは,instance
キーワードを利用しFoldable class
の関数を実装します.
そして先ほどと違い,deriving
キーワードを利用しShow class
を実装します.
deriving
は実装が自明の場合に利用できます.
今回はただList
をそのまま出力したいがために実装しています.
ちなみにFoldable class
のインスタンスを作成しないと以下のようなエラーが出ます.
なのでエラーが出たらインスタンス化しないといけないんだなぁと思っていく感じが良いです.
• No instance for (Foldable List) arising from a use of ‘foldr’ # Foldable classのインスタンスではない!?
モナド
Haskell
では,モナドという概念が存在しています.
そしてモナドはHaskell
の中でだいぶ重要な概念ではあるものの,
少しわかりづらいもので,Haskell
の学習コストをある程度あげている気がします.
モナドを箱として説明している箱で考えるFunctor、ApplicativeそしてMonad
というすごく良い記事があるので,オススメです.
そしてモナドには様々な種類が存在しており,今回はそれを少し紹介します.
早速ですが,main
関数は実はIO
モナドを返す関数でした.
詳しくmain
の定義を書くと以下のようになります.
main :: IO () -- 返り値がIO
main = print "hello"
そしてprint
関数の定義は以下のようになります.
Prelude> :t print
print :: Show a => a -> IO () # print関数の定義
print
関数がIO
モナドを返していることがわかります.
ではmain
関数がIO
モナドを返さなければどうなるでしょうか.
実際に実行してみると以下のようになります.
main = "hello"
-- エラー内容
-- • Couldn't match expected type ‘IO t0’ with actual type ‘[Char]’
-- • In the expression: main
-- When checking the type of the IO action ‘main’
当然IO
モナドを返してもらいたいのに,[Char]
型が返ってきていると怒られています.
ここでモナドを返り値に持つ関数というのは,そのモナドに包まれた関数であると思ってもらえればいいと思います.
ここでのmain
関数は,IO
モナドに包まれた関数であるということです.
次にprint
以外の関数で,標準入力から1行取得する関数getLine
があります.
これの定義は以下のようになっています.
Prelude> :t getLine
getLine :: IO String
こちらはprint
関数と違い,IO String
型を返します.
そしてこのIO
に包まれたString
型をどうやって取得するかというと,>>=
関数を利用します.
ではgetLine
で取得した値をprint
関数に渡して出力してみましょう.
コードは以下のようになります.
main = getLine >>= print
-- input:
-- hello
-- output:
-- "hello"
>>=
関数の定義は以下のようなもので,モナドの中身の値を取り出して次の関数に適応します.
(>>=) :: Monad m => m a -> (a -> m b) -> m b
>>=
を無限に横に繋げていくことで処理を連続させることができますが,Haskell
にはもっと良いやり方で同じことが書けます.
それが以下です.
main = do
s <- getLine
print s
-- input:
-- hello
-- output:
-- "hello"
do
キーワードを利用します.
そしてs <- getLine
は,getLine
の結果から値を取り出し,それをs
という変数に束縛します.
変数定義のような感覚で書けるので,より手続き型風に書けます.
モナドが利用できるケースでは,do
キーワードをよく利用すると思います.
ここでIO
モナドの動きが気になった方は,Haskell IOモナド 超入門という記事が非常にわかりやすいので,読んでみることをオススメします.
Stateモナド
Haskell
では,関数内で利用する状態を表現するState
モナドが存在します.
簡単な例を示してみます.
import Control.Monad.State
count :: State Integer ()
count = do
s <- get -- 値を取得する
put (s + 1) -- 1を足して,Stateを更新する
-- modify (+1)でもオッケー
count10 :: State Integer String
count10 = do
count -- countを呼ぶ
s <- get -- 値を取得する
case s of -- 分岐
10 -> return $ show s -- 10の時,数値を文字列にしてモナドに包んで返す
_ -> count10 -- 再帰
main = print $ runState count10 0
-- output: ("10",10)
ここでcount10
関数でのState
モナドの定義は,State Integer String
となっています.
これは状態としてInteger
型を持っていて,結果としてString
型を返すということを示しています.
次のcount
関数を見ると,State Integer ()
となっており,これは何も返さないことを意味します.
では動きを少しずつ見ていきましょう.
まずmain
関数がrunState
という関数を呼びます.
この関数の定義は以下のようになっています.
runState :: State s a -> s -> (a, s)
第一引数には,State
モナドの関数,第二引数に初期状態を入れることにより,最終結果をタプルとして得る関数です.
では次にcount10
を見てみましょう.
count10
では,count
関数を呼んでますが,count
関数ではget
関数で状態を取り出し,それに1足した値をput
関数で再代入を行っています.
State
モナドの関数なので,関数内で状態をこねくり回すことができちゃいます.
まるでグローバル変数のような振る舞いをしてくれます.
そしてcount
を呼び出したあとでは,count10
関数はget
関数で値を取り出しています.
最後にcase
を用いて分岐を行い,State
モナドの中身が10
になったら,show
関数で数値を文字列化しreturn
関数でモナドの形にし返します.
ここでやっといろんな言語で利用されているreturn
関数が登場しましたが,ここでの意味合いはだいぶ違います.
Haskell
でのreturn
関数の定義は以下です.
return :: Monad m => a -> m a
a
を受け取って,モナドに包んで返していますね.
このようにHaskell
では,値をモナドに包む時にreturn
を利用します.
ここで値をモナドで包む理由としては,count10
がモナドを返す関数だからです.
その後,返ってきた返り値を出力してプログラムは終了します.
Haskell
にはグローバル変数がないらしいので(それらしいものはある),状態を扱う際はStateモナドなんかを利用する方向でプログラムを組んだらいいと思います.
まとめ
ここまで,パターンマッチ,データ型,型クラス,モナドとHaskell
の概念を紹介してきました.
しかしながら本記事で,Haskell
の全てが理解できる人は当然いないと思うので,だいたい感覚で理解してもらえればと思います.
そして少しでもHaskell
に興味をもったら何かしら実装してみるのもいいかもしれません.