Mercurial > hg > Papers > 2014 > toma-master
changeset 63:d5ebba127662
fix
author | kono |
---|---|
date | Wed, 12 Feb 2014 17:56:08 +0900 |
parents | d11f4c6c7657 |
children | 13535fc08357 |
files | paper/chapter1.tex |
diffstat | 1 files changed, 104 insertions(+), 47 deletions(-) [+] |
line wrap: on
line diff
--- a/paper/chapter1.tex Wed Feb 12 16:13:49 2014 +0900 +++ b/paper/chapter1.tex Wed Feb 12 17:56:08 2014 +0900 @@ -7,19 +7,31 @@ \subsubsection{関数の定義} 既存の手続き型言語と異なり, 手順を記述するのではなく, この関数が何であるかということを記述する. -例えば, Haskell でフィボナッチ数列を定義するにはソースコード\ref{src:fib}のように記述する. -Haskell は, 関数を引数を適用するという考えに基づいて, 抽象的なプログラミングを可能とする. +例えば, Haskell でフィボナッチ関数を定義するにはソースコード\ref{src:fib}のように記述する. -\begin{lstlisting}[label=src:fib, caption=フィボナッチ数列] -fib :: Int -> Int +\begin{lstlisting}[label=src:fib, caption=フィボナッチ関数] fib 0 = 0 fib 1 = 1 fib n = fib (n-2) + fib (n-1) \end{lstlisting} -fib :: Int -$>$ Int は, 関数の型宣言である. -この関数は, Int を受け取って Int を返す関数ということを示す. +% 関数の引数への適用とは何か? Haskell の計算とは何か? フィボナッチ数列の関数が三行に渡って書かれているが, これは Haskell のパターンマッチを活用している. + fib 3 +を受け取ると、Haskellは、fib のパターンマッチを行う。3 は0でも1でもないので、三行目の定義、 + fib n = fib (n-2) + fib (n-1) +にマッチする。ここで、変数n は3となる。この式は、 + fib 3 = fib (3-2) + fib (3-1) +つまり変数nが3に置換された式が生成される。この変換がHaskellの計算の一段である。 + +(3-2)と(3-1)は、それぞれ、1と2になる。これはHaskellの内蔵された計算によって行われる。 + fib 3 = fib 1 + fib 2 +fib 1は、... + fib 3 = 1 + (fib (2-2) + fib (2-1)) + fib 3 = 1 + (fib 0 + fib 1) + fib 3 = 1 + (0 + 1) + fib 3 = 2 + 引数の値が 0 ならば 2 行目が呼び出され, 関数の結果は 0 となる. 引数の値が 1 ならば 3 行目が呼び出され, 関数の結果は 1 となる. 上から順に引数と一致する行がないか調べていき, 引数が 0 でも 1 でもなければ引数は n に束縛され, fib (n-2) + fib (n-1) が計算される. @@ -28,19 +40,34 @@ 再帰は, 関数型プログラミング言語において必要不可欠な要素である. 手続き型言語では, 配列とループを主に用いてプログラミングを行うが Haskell ではリスト構造と再帰を用いる. +Haskell で、上記のfib関数を定義すると、Haskell は以下の型を推論する。 + +\begin{lstlisting}[label=src:fib-type, caption=フィボナッチ関数の型] +fib :: Num a => a -> a +\end{lstlisting} + +fib :: Int -$>$ Int は, 関数の型宣言である. +この関数は, Int を受け取って Int を返す関数ということを示す. + +\begin{lstlisting}[label=src:fib, caption=フィボナッチ関数] +fib :: Int -> Int +fib 0 = 0 +fib 1 = 1 +fib n = fib (n-2) + fib (n-1) +\end{lstlisting} + + \subsubsection{変数の代入} -純粋関数型プログラミングでは, 変数の代入は一度のみで後から書き換えることはできない. -フィボナッチ数列の関数でも, 一度引数を束縛した n を書き換えることはできない. -変数への代入が一度のみのため, 関数にできることは何かを計算してその結果を返すことだけであり, 引数が同じならば関数は必ず同じ値を返すことが保証されている. -この性質は関数の理解を容易にし, プログラムの証明を可能にする. -正しいと分かる単純な関数を組み合わせて, より複雑な正しい関数を組み立てていくのが関数型言語のプログラミングスタイルである. +このように、 純粋関数型プログラミングでは, 変数の代入は式の書き換えによって行われるので、 +一つの変数(例えばn)を二度書き換えることはできない。なぜなら、書き換えられた変数は式から +消えてしまうからである。 +フィボナッチ関数でも, 一度引数を束縛した n を書き換えることはできない. +変数への代入が一度のみのため, 関数にできることは何かを計算してその結果を返すことだけであり, 引数が同じならば関数は必ず同じ値を返すことが保証されている. これを参照透過性と言う。 \subsubsection{高階関数とカリー化} 関数型プログラミング言語は, 関数を変数の値として扱うことができる. Haskell は, 引数として関数を取ったり返り値として関数を返すことができる高階関数を定義できる. -高階関数の例として Haskell のカリー化が挙げられる. -Haskell では, 複数の引数を取るようにみえる関数でも, 実際には全て一度に一つだけの引数を取る. 2 つのInt を受け取り, 大きい方を返す max はソースコード\ref{src:max}のように定義できる. \begin{lstlisting}[label=src:max, caption=max関数] @@ -50,12 +77,22 @@ else x \end{lstlisting} -このmaxの型宣言に現れる-$>$は左結合であり, -複数の引数を取るようにみえる関数は, 実際には 1 つの引数を取り, その次の引数を受け取る関数を返す(ソースコード\ref{src:max_curry}). - +このmaxの型宣言に現れる-$>$は右結合であり, \begin{lstlisting}[label=src:max_curry, caption=max関数のカリー化] max :: Int -> (Int -> Int) \end{lstlisting} +と読むことになっている。 + +関数の引数は左結合であり、 + max 3 4 +は + (max 3) 4 +と解釈する。 + +ここで、max 3 は、Int -> Int の関数「3と大小を比較する」である。このように、 +複数の引数を取るようにみえる関数は, 実際には 1 つの引数を取り, その次の引数を受け取る関数を返す(ソースコード\ref{src:max_curry}). +max 3は、 高階関数の例になっている。複数引数の関数を高階関数と考えることをカリー化という。 + このように関数を返すことで全ての関数を一引数関数として表すことをカリー化という. カリー化によって, 関数を本来より少ない引数で呼び出した際に部分適用された関数を得ることができる(ソースコード\ref{src:partial}). @@ -68,13 +105,15 @@ \clearpage \section{型} Haskell では, すべての式, すべての関数に型がある. -値の型は, その値が同じ型の別の値と何らかの性質を共有していることを示す. +Int とか Char などは、値の型であり、 +その値が何らかの性質を共有していることを示す. 例えば, 数値は加算できる, 文字列は表示できるといった性質である. -型はプログラムに抽象をもたらす. -型を導入することで, 低水準の詳細を気にせずプログラミングが可能になる. -例えば, 値の型が文字列ならば, どのように実装されているかということを気にせず, -その文字列が他の文字列と同じように振る舞うとみなすことができる. +% これは、型クラスあるいは型変数の話。 +% 型はプログラムに抽象をもたらす. +% 型を導入することで, 低水準の詳細を気にせずプログラミングが可能になる. +% 例えば, 値の型が文字列ならば, どのように実装されているかということを気にせず, +% その文字列が他の文字列と同じように振る舞うとみなすことができる. \subsubsection{型の定義と型検査} Haskell は静的型検査によりエラーを検出することができ, 評価の際に型に起因するエラーが起きないことを保証している. @@ -85,13 +124,26 @@ また, リスト型の定義とあわせてデータ型の定義についても説明する. リストとは, 角括弧で包まれた同じ型の要素の並びである. [1,2,3] や ['a','b','c'] などと表現する. -リストはソースコード\ref{src:list}のように定義されている. + +Haskell では, [1,2,3] という様にリストを表すが, これは単なるシンタックスシュガーであり, +内部では 1 : 2 : 3 : [] のように表現される. + : + 1 : + 2 : + 3 [] + + +つまりリストの型の値は, [] もしくは a : [a] になることが分かる. +このリストをHaskellでは\ref{src:list}のように定義する。 \begin{lstlisting}[label=src:list, caption=Haskellのリスト定義] data [] a = [] | a : [a] \end{lstlisting} +等号の後は, 型の値となるものを列挙する. +$|$ は, もしくはという意味である. data というのは新しい型を定義する際に利用するキーワードである. + 等号の前に型の名前, 等号の後に型が取り得る値の種類を指定する. 等号の前にある [] a というのが型名である. @@ -99,9 +151,6 @@ リスト型は, Intのリスト, Floatのリストといった様々な型のリストを作ることができ, 型変数を用いてそれを実現する. 型変数が何の型になるのかという情報は実行時には決まっており, 関数に渡される際に型の不整合が起きることはない. -等号の後は, 型の値となるものを列挙する. -$|$ は, もしくはという意味である. -つまりリストの型の値は, [] もしくは a : [a] になることが分かる. 型の値 [] は空のリストを表す. 型名と同じであるが, 型とは名前領域が異なるため問題ない. 型名はプログラム中では注釈としてしか使われないためである. @@ -112,8 +161,6 @@ 型の値の定義 [] $|$ a : [a] は, 展開すると a : a : a : .. : a : [] となる. a が任意個続いた後に, [] が来ることとなる. つまり, リストは無限に繋げることができ, リストの終端は空のリスト [] で終わる. -Haskell では, [1,2,3] という様にリストを表すが, これは単なるシンタックスシュガーであり, -内部では 1 : 2 : 3 : [] のように表現される. このように定義した型であっても, Haskell は型検査を行う. リストは : を使うことで新しい要素を加えることができるが, 全ての要素は同じ型の要素である必要がある. @@ -136,7 +183,16 @@ 型推論とは, 型の宣言をせずともそれを導くのに使われた関数の型シグネチャなどから自動的に型を決定する機構のことである. 型推論のない静的型付け言語は, プログラマが型の宣言を行うことが強制されるが Haskell では型の宣言は必須ではない. -どのように型推論が行われるのかの例として, 開発したデータベースで実装した getChildren という関数で調べる(ソースコード\ref{src:getchildren}). +どのように型推論が行われるの例をみてみよう。 +まず、前提として以下の型の定義がある。 (ソースコード\ref{src:type2}). + +\begin{lstlisting}[label=src:type2, caption=getChildrenに含まれる関数の型] +getNode :: Node -> Path -> Node +elems :: Map k a -> [a] +children :: Node -> Map Int Node +\end{lstlisting} + +開発したデータベースで実装した getChildren という関数で調べる(ソースコード\ref{src:getchildren}). \begin{lstlisting}[label=src:getchildren, caption=getChildren関数] getChildren node path = elems (children (getNode node path)) @@ -153,14 +209,6 @@ そうすると, 推論された型情報 Node -$>$ [Int] -$>$ [Node]が得られる. この型情報は期待する型の定義と一致する. -この情報はgetChildrenに含まれるいくつかの関数の型を確認することで導き出すことができる(ソースコード\ref{src:type2}). - -\begin{lstlisting}[label=src:type2, caption=getChildrenに含まれる関数の型] -getNode :: Node -> Path -> Node -elems :: Map k a -> [a] -children :: Node -> Map Int Node -\end{lstlisting} - 例えば, getChildrenの引数である, node と path は getNode に渡されているため, getNode の型である Node 型と Path 型であることが分かる. 返り値となる型は, elemsの[a]となる. このaは, elemsが受け取るMapの2つ目の型と一致するため, childrenの返り値であるMap Int Node より, [Node]ということが分かる. @@ -245,7 +293,7 @@ Maybe モナドの return は, 値をJustで包む. これがコンテナに包む機能という意味である. -$>>$= (bind) は, 「コンテナに包まれた値」と, 「普通の値を取りコンテナに包まれた値を返す関数」を引数にとり, コンテナに包まれた値をその関数に適用すると説明した. +$>>=$ (bind) は, 「コンテナに包まれた値」と, 「普通の値を取りコンテナに包まれた値を返す関数」を引数にとり, コンテナに包まれた値をその関数に適用すると説明した. Maybe モナドの場合, コンテナが Nothing なら, そのまま Nothing を返す. コンテナがJustならば, Just に包まれた値を取り出し, 「普通の値を取りコンテナに包まれた値を返す関数」に適用する. @@ -264,7 +312,16 @@ up の場合, 引数として4が渡された場合は上がれないため失敗, down の場合, 引数として0が渡された場合は下がれないため失敗と考える. -3 という値にdown, down, up, up 繰り返していく時, モナドを使わない場合ソースコード\ref{src:up_without_monad}のように定義することになる. +3 という値にdown, down, up, up 繰り返していく時, + +\begin{lstlisting}[label=src:upmonad, caption=Maybeモナドを用いてupとdownを行う] +return 3 >>= down >>= down >>= up >>= up +\end{lstlisting} + +% モナドを使わない場合ソースコード\ref{src:up_without_monad}のように定義することになる. +% これを文脈を保ったまま, 関数を繋げられるモナドを使えばソースコード\ref{src:upmonad}のように記述できる. + +これは、以下のように展開される。 \begin{lstlisting}[label=src:up_without_monad, caption=Maybeモナドを使わずにupとdownを行う] updown :: Maybe Int @@ -283,14 +340,8 @@ 例えば, 3 行目は down 3 の結果が Nothing なら, Nothing を返すという意味である. Justの場合, 値をplace1という変数に束縛しその後の処理を続ける. -ソースコード\ref{src:up_without_monad}では, 毎回失敗したか成功したか確認するために非常に煩雑なコードとなってしまった. -これを文脈を保ったまま, 関数を繋げられるモナドを使えばソースコード\ref{src:upmonad}のように記述できる. - -\begin{lstlisting}[label=src:upmonad, caption=Maybeモナドを用いてupとdownを行う] -return 3 >>= down >>= down >>= up >>= up -\end{lstlisting} - -Maybe モナドを使うことで, この計算は失敗しているかもしれないという文脈を保ったまま処理を繋げていくことができる. +% ソースコード\ref{src:up_without_monad}では, 毎回失敗したか成功したか確認するために非常に煩雑なコードとなってしまった. +% Maybe モナドを使うことで, この計算は失敗しているかもしれないという文脈を保ったまま処理を繋げていくことができる. \subsubsection{IO モナド} Haskellで副作用を持つ処理を実行するには, IO モナドを利用する. @@ -365,10 +416,16 @@ y は未評価のthunkとして同じ x を参照する. そのため, x が2回評価されることはない. + (fib 100) ..... (fib 100) + + ^ ^ + | | + +------------------+ 同じものだと思う。 + \newpage \begin{lstlisting}[label=memo, caption=メモ化] Prelude> let x = 1 + 2 :: Int -Prelude> let y = (x,x) +Prelude> let y = (x,x) <---- これは共有 Prelude> :sprint y y = (_,_) \end{lstlisting}