Mercurial > hg > Papers > 2014 > toma-master
view paper/chapter1.tex @ 10:a349b2c01cfe
add graffle
author | Daichi TOMA <toma@cr.ie.u-ryukyu.ac.jp> |
---|---|
date | Tue, 28 Jan 2014 06:40:52 +0900 |
parents | 70bda541eb1d |
children | a551888363cb |
line wrap: on
line source
\chapter{Haskellとは} \label{ch:haskell} Haskell とは純粋関数型プログラミング言語である。 Haskell では、変数の代入は一度のみで書き換えることはできない。 また、引数が同じならば関数は必ず同じ値を返すことが保証されている。 このような特徴から既存の手続き型言語と同じようにプログラムを書くことはできず、関数を組み合わせることでプログラミングを行う。 \section{関数型プログラミング} 関数とは、一つの引数を取り一つの結果を返す変換器のことである。 関数型プログラミング言語では、引数を関数に作用させていくことで計算を行う。 Haskell は純粋であり、引数が同じならば関数は必ず同じ値を返すことが保証されている。 これはつまり、関数の正しさを簡単に推測でき、また関数同士を容易に組み合わせられることを意味している。 実際、Haskell では小さな関数をつなぎ合わせることでプログラミングを行う。 関数型プログラミング言語は、関数を第一級オブジェクトとして扱うことができ、高階関数を定義することができる。 これは、引数として関数を取ったり返り値として関数を返すことができるということである。 高階関数は問題解決の強力な手段であり、関数型プログラミング言語にはなくてはならないものである。 Haskell では標準ライブラリに、リストを処理するための便利な高階関数がいくつも定義されている。 Haskell では、全ての関数は一度に一つの引数だけを取る。 複数の引数を取るようにみえる関数は、実際には1つの引数を取り、その次の引数を受け取る関数を返す。 このように関数を返すことで全ての関数を一引数関数として表すことをカリー化という。 カリー化によって、関数を本来より少ない引数で呼び出した際に部分適用された関数を得ることができる。 再帰もまた関数型プログラミング言語において必要不可欠な要素である。 再帰とは、関数を関数自身を使って定義することをいう。 関数型プログラミング言語では、ループといった処理を行う場合再帰を用いる。 また、リストの畳み込み関数といった再帰を用いた関数が標準ライブラリで提供されている。 \section{型} Haskell は静的型付け言語である。 全ての型に起因するエラーはコンパイル時に捕捉される。 コンパイル時に多くのエラーを見つけてくれるため、プログラマがプログラムの正しさを確認するのに役に立つ。 また、Haskell は型を明示しなくても、処理系により自動的に型が推論される。 型安全を保ちながら、ほとんどの部分で型宣言を省略することができる\footnote{明示的な型宣言は可読性の向上や問題の発見に役に立つため、トップレベルの関数には型を明記することが一般的である。}。 \subsubsection{データ型} Haskellの標準なデータ型には、Int 型や、Float 型、Bool 型、Char 型などが存在する。 data キーワードを用いることで、自作のデータ型を作ることもできる。 標準ライブラリにおける Bool 型は\ref{bool}のように定義されている。 \begin{lstlisting}[label=bool, caption=Bool型の定義] data Bool = False | True \end{lstlisting} この型宣言は、「Bool 型は True または False の値を取り得る」ということを表している。 \subsubsection{型変数} Haskell の型は多相的である。 型変数を使い、型を抽象化できる。 型変数は、型安全を保ちながら、関数を複数の型に対して動作するようにできる。 Haskell の型の名前は全て大文字から始まるが、型変数は小文字から始まる。 任意の型のリストを引数に取り、その型の先頭要素を返すという関数 head の型は\ref{type_variable}のように定義される。 \begin{lstlisting}[label=type_variable, caption=型変数] head :: [a] -> a \end{lstlisting} データ型も型変数を使うことができる。例えばリストといったデータ型は、その要素の型を型変数として定義する。 これらのデータ型は、型引数を受け取って具体型を生成する。 言うまでもなく、型推論があるため明示的に型引数を渡す必要はない。 \subsubsection{型クラス} 型クラスは型の振る舞いを定義するものである。 ある型クラスのインスタンスである型は、その型クラスに属する関数の集まりを実装する。 ある型を型クラスのインスタンスにしようとする場合、それらの関数がその型ではどのような意味になるのか定義する。 例えば、Eq 型クラスのインスタンスとなる型は等値性をテストできる。Show 型クラスのインスタンスとなる型は文字列として表現できる。 いくつかの関数には型クラスが制約となるものがある。 演算子 * の引数は、数を表す型クラスである Num 型クラスである必要がある。 \begin{lstlisting}[label=multiply, caption=演算子 *] (*) :: Num a => a -> a -> a \end{lstlisting} 1つの型はいくつもの型クラスのインスタンスとなることができる。 自作のデータ型を作成する際に型クラスを利用することで、既存の Haskell ライブラリを利用できる。 \section{モナド} Haskell では、さまざまな目的にモナドを使う。 モナドを用いることで I/O 処理を行うこともできる。 モナドとなる型は、型変数として具体型をただ1つ取る。 これにより何かしらのコンテナに包まれた値を実現する。 モナドの振る舞いは型クラスとして実装し、メソッドとして return および $>$$>$= を定義する。 return は値を持ち上げてコンテナに包む機能を実装する。 $>$$>$= は、「コンテナに包まれた値」と、「普通の値を取りコンテナに包まれた値を返す関数」を引数にとり、コンテナに包まれた値をその関数に適用する。 この2つの関数を利用することにより、状態を保ったまま関数を繋いでいくことができる。 Haskellで副作用を持つ処理を実行するには、IO モナドを利用する。 IO モナドは、mainという名前と関連づけることで初めて実行される。 IO モナド自体は単なる命令書であり、引数として渡したりするだけでは実行されない。 そのため、IO モナドをリストに格納したりすることも可能である。 \section{並列実行} Haskellはデフォルトではシングルスレッドで走る。 並列に動かしたい場合は、-threaded 付きでコンパイルし、RTS の -N オプションを付けて実行する。 -N オプションで指定された数だけ、OSのスレッドが立ち上がり実行される。 \begin{lstlisting}[language=bash, label=concurrent, caption=並列実行の様子] $ ghc -O2 par.hs -threaded $ ./par +RTS -N2 \end{lstlisting} 当然これだけでは並列に動かず、並列に実行できるようにプログラムを書く必要がある。 Haskell は遅延評価を行うため、必要となるまで式の評価が遅延される。 普段は気にする必要はないが、並列実行ではどのように評価されるか意識する必要がある。 並列に動くように処理を分割したとしても、値が必要になるまで評価されない。 この問題は、Control.DeepSeq モジュールにある deepseq 関数を使用し、式を即時評価することで解決できる。 Haskellの並列化手法はいくつかあるが、その中で最もシンプルなのは Eval モナドを利用することである。 Control.Parallel.Strategies モジュールをインポートすることで使用できる。 \begin{lstlisting}[label=eval, caption=Eval モナド] data Eval a instance Monad Eval runEval :: Eval a -> a rpar :: a -> Eval a rseq :: a -> Eval a \end{lstlisting} rpar は、引数が並列処理可能であることを示す。 rseq は、引数を評価し、結果を待つように示す。 どちらも式が完全に即時評価されるわけではないので、出力を挟んだり、deepseq 関数をうまく活用する必要がある。 runEval は、評価を実行し結果を返す操作である。 実際の利用方法として2つのパターンを紹介する。 \begin{lstlisting}[label=rpar/rpar, caption=rpar/rpar] runEval $ do a <- rpar (f x) b <- rpar (f y) return (a,b) \end{lstlisting} rpar/rpar パターンは単純に並列に動かしたい時に利用する。 即座に return される。 \begin{lstlisting}[label=rpar/rseq/rseq, caption=rpar/rseq/rseq] runEval $ do a <- rpar (f x) b <- rseq (f y) rseq a return (a,b) \end{lstlisting} rpar/rseq/rseq パターンは、f x および f y の結果を待ってから return する。 他の処理が結果を必要としている時に利用する。