はじめに
こんにちは、キャスレーコンサルティングのSI(システム・インテグレーション)部の清水です。
私はこれまでの業務で、開発言語として主にC言語とC#を経験してきましたが、
より見識を広げるために今まで経験してきた言語とは違った領域のプログラミング言語についても
学びたいと考えておりました。
そんな折に、技術系ブログ等でしばしば目にする
「関数型プログラミング」
「Haskell」
といったキーワードに興味を惹かれ、学習を始めてみました。
本記事では学習した内容を元に、Haskellを例に取った関数型プログラミングの概要の説明と、
開発環境を構築してサンプルプログラムを動かしてみるところまでを扱います。
なお、説明については厳密な定義とは齟齬がある可能性があります。
難しい話はともかくとして、一般的にはこのように言われている/扱われている。
といったレベルの内容になっていることをご了承願います。
関数型プログラミングって?
そもそも一般的なプログラミング言語は、
機械語(0と1の羅列)→アセンブリ言語→高水準言語→オブジェクト指向型言語のように進化を遂げてきました。
それぞれ記述方法は大きく異なりますが、メモリ上の領域に変数や構造体を定義し、
「命令(問題を解くための手続き)」を実行してメモリの値を書き換えていくといった動作原理に変わりはありません。
上記はつまりCPUの動作原理であり、
主要なプログラミング言語はハードウェアの実装に即した形で発展してきたといえます。
このようなプログラミングパラダイムを命令型プログラミングと呼び、
命令型プログラミングを基本スタイルとする言語を命令型言語と呼びます。
現在広く使用されているプログラミング言語の大半は命令型言語と言えるため、馴染みのある方が多いかと思います。
さて、命令型プログラミングは上記の通りコンピュータへの命令であるため、
「問題を解く手順」(アルゴリズム)の記述に労力を割く必要があります。
しかし例えば人間が数学の問題を解くときにいきなり紙にアルゴリズムを書き始めることはしないでしょう。
コンピュータを使う立場からすれば、問題の性質(どのような結果が欲しいか)を記述するだけで
アルゴリズムは勝手に最適化して解いてくれるのが理想的です。
このような数学的要請から、関数型プログラミングが登場します*1。
ここで言う関数とは命令型言語の関数やメソッドとは異なる、
数学的な意味での関数(入力値から出力値を求める規則)を指します。
命令型プログラミングがハードウェアの実装をベースとしているのに対し、
関数型プログラミングは数学の考え方をベースとしているわけです。
*1:正確にはもう少し広い意味の「宣言型プログラミング」が挙げられますが、本記事では割愛します。
関数型プログラミングでは全ての処理を関数として定義し、
入力値から出力値を求める以外の動作(代入、入出力など。副作用と呼ぶ)を極力排除します。
関数型プログラミングを基本スタイルとする言語を関数型言語、特に副作用が全くない言語を純粋関数型言語と呼びます。
具体的な関数型言語には以下のようなものがあります。
-
-
- OCaml : INRIAが開発した、オブジェクト指向的な機能を取り入れた関数型言語。
- F# : MicrosoftがOCamlを元に開発した、.NET Framework上の関数型言語。VisualStudio2010を使用しているとちょくちょく名前を目にします。
- Scala : JVMで動作する、関数型とオブジェクト型のハイブリッド言語。弊社技術ブログでも何度か取り上げられています。
- Haskell : 関数型言語のオープンな標準として作られた純粋関数型言語。以下もう少し説明が続きますが、「ややこしい話は後にしてどんなもんか見せてくれ!」という方は「実際に使ってみる」章まで読み飛ばし願います。
-
Haskellの(あるいは純粋関数型言語の)特徴
Haskellは純粋関数型言語であるため、基本的には副作用がありません。
例えば関数の途中で入力値ではないグローバル変数(のようなもの)を参照したり書き換えたりすることはできず、
そもそも一度値を定義した変数に違う値を代入することができません。
副作用が無いことによって、同じ関数はいつでも同じ結果を返すことが保証されていることになります。
一度値を定義した変数に違う値を代入することができないことのメリットとして、
変数の中身を確認したいときは定義した行のみを見ればよいのでコードリーディングが容易になることや、
コードを書き換える時も他の箇所に対する影響を最小限に抑えられることが挙げられます。
さらに関数の出力値は入力値のみに依存し、他の関数とは互いに影響しないため関数の部品化が容易です。
例えば関数を引数にとる関数(高階関数)が用意されており、これを使って簡単に関数を組み合わせることができます。
また関数の独立性が高いことはテストのしやすさにも繋がります。
また、マルチコアCPUの性能を活かすため並行/並列プログラミングを行う際には
競合状態やデッドロックへの対処が課題となりますが、Haskellでは上記の通り、
関数はいつどのように実行しても同じ結果が得られることが言語仕様上保証されていることから、
命令型言語に比べ並列化の対応が容易になります。
(…と説明されていることが多いのですが、実際のところ真に高速な並列処理を実現することは他の言語と同様、
一朝一夕でできるほど簡単ではなさそうです。)
さて、そうは言ったものの入出力がないとなるとどうやって実用的なプログラムを書くんだ
という疑問は当然湧いてきますが、Haskellには入出力を扱うための仕組み(IOモナド*2)が用意されており、
全ての入出力はIOモナドを経由して処理されることになります。
このように副作用の発生しうる箇所を限定させておくことで、それ以外の箇所の独立性を強固に保てるわけです。
*2:モナドって何だ、という話はHaskellを学ぶ上で必ずぶつかるややこしい概念で、
私もまだふんわりとしか理解していないため詳細はここでは踏み込みません。今後の課題とさせてください。
Haskellを使用するメリット
ここまでに述べたHaskellのメリットを改めて挙げると、以下のようになります。
-
-
- 変数の中身が処理の途中で変わってしまうことがないため、可読性・保守性が高い
- 関数が相互に影響しないため、テスト・組み合わせがしやすい
- 関数が相互に影響しないため、並行/並列化がしやすい(と、言われている)
-
3点目について実例を本記事内にて扱うことは難しいですが、
1点目、2点目のメリットについてはサンプルコードを通じて実感いただければと思います。
実際に使ってみる
それでは実際に開発環境を構築し、プログラムを動かしてみましょう。
まずはHaskellのビルドツールであるstackをインストールします。
上記から、使用OSの手順に従ってインストールしてください。
なお、筆者の実行環境はMac OS X 10.11.3となります。
Macでhomebrewを導入済みの場合は、
$ brew install haskell-stack
でインストールできます。
インストールが完了したら、コマンドプロンプトまたはターミナルにて
$ stack setup
を実行すると、HaskellのコンパイラであるGHCやパッケージ管理ツールのCabalがインストールされます。
“stack ghci”でGHCの対話モードであるGHCiが起動し、Haskellの構文をコマンドラインで実行できます。
$ stack ghci Prelude> 2 + 3 * 5 17 Prelude> 1 == 1 True Prelude> 1 /= 1 False
関数を使用するには、関数名に続けてスペース区切りで引数を渡します。
組み込み関数でいくつか試してみます。
Prelude> max 5 8 --5と8の大きい方ほう 8 Prelude> mod 10 3 --10を3で割った余り 1 Prelude> sqrt 3 --3の平方根 1.732050808 Prelude> floor 12.345 --小数点以下切り捨て 12 Prelude> floor $ sqrt 3 --sqrtの結果をfloorに渡す。 floor (sqrt 3) と同じ。 1
[ ]の中にカンマ区切りで要素を並べるとリストになります。
レンジ (..) を使用すると要素の列挙を省略できます。
Prelude> [1..10] [1,2,3,4,5,6,7,8,9,10] Prelude> [2,4..16] [2,4,6,8,10,12,14,16]
文字列は文字のリストとして表現されます。
Prelude> ['h','e','l','l','o'] "hello"
リストの先頭に要素を追加するには:演算子を使います。
Prelude> 3:[4,5,6,7] [3,4,5,6,7] Prelude> 'a':'b':'c':['d'] "abcd"
リスト内包記法(数学における集合の内包的記法に似た記法)によってリストのフィルタリングや変換ができます。
例えば「1から10までの整数」から要素を取り出して「3で割った余りが1になる」条件でフィルタリングする場合、
以下のように書けます。
Prelude> [ x | x <- [1..10], mod x 3 == 1] [1,4,7,10]
高階関数 map を使うと、リストの各要素に関数を適用できます。
関数はラムダ式を用いてその場で定義することもできます。
Prelude> [ x | x map (*2) [1..10] [2,4,6,8,10,12,14,16,18,20] Prelude> map sqrt [16, 81, 100] [4.0,9.0,10.0] Prelude> map (\x -> x * 10 + x) [4,5,6,7] [44,55,66,77]
( )の中にカンマ区切りで要素を並べるとタプル(組)になります。
リストの場合全ての要素が同じ型でなければなりませんが、タプルは異なる型でも組み合わせることができます。
Prelude> (2.44, 6.0) (2.44,6.0) Prelude> (1,"abc") (1,"abc")
GHCiを終了するには “:q” とタイプします。
Prelude> :q Leaving GHCi.
さて、次はstackを使い新規プロジェクトを作成してみましょう。
stack new [プロジェクト名] [テンプレート名]
を実行するとプロジェクトを作成できます。
今回はsimpleテンプレートを使用します。
haskell $ stack new sample-project simple Downloading template "simple" to create project "sample-project" in sample-project/ ... The following parameters were needed by the template but not provided: author-email, author-name, authorName, category, copyright, github-username, year You can provide them in /Users/shimizumakoto/.stack/config.yaml, like this: templates: params: author-email: value author-name: value authorName: value category: value copyright: value github-username: value year: value Or you can pass each one as parameters like this: stack new sample-project simple -p "author-email:value" -p "author-name:value" -p "authorName:value" -p "category:value" -p "copyright:value" -p "github-username:value" -p "year:value" Writing default config file to: /Users/shimizumakoto/haskell/sample-project/stack.yaml Basing on cabal files: - /Users/shimizumakoto/haskell/sample-project/sample-project.cabal Checking against build plan lts-5.8 Selected resolver: lts-5.8 Wrote project config to: /Users/shimizumakoto/haskell/sample-project/stack.yaml
作成されたプロジェクトフォルダに移動し、 “stack build” でビルド、 “stack exec [プロジェクト名]” で実行できます。
haskell $ cd sample-project/ sample-project $ stack build sample-project-0.1.0.0: configure Configuring sample-project-0.1.0.0... sample-project-0.1.0.0: build Preprocessing executable 'sample-project' for sample-project-0.1.0.0... [1 of 1] Compiling Main ( src/Main.hs, .stack-work/dist/x86_64-osx/Cabal-1.22.5.0/build/sample-project/sample-project-tmp/Main.o ) Linking .stack-work/dist/x86_64-osx/Cabal-1.22.5.0/build/sample-project/sample-project ... sample-project-0.1.0.0: copy/register Installing executable(s) in /Users/shimizumakoto/haskell/sample-project/.stack-work/install/x86_64-osx/lts-5.8/7.10.3/bin sample-project $ stack exec sample-project hello world
おっと、ご丁寧にもhello worldまで実現してしまいました。
ソースファイル src/Main.hs を見てみます。
module Main where main :: IO () main = do putStrLn "hello world"
とてもシンプルですね。ここで終わっては少し物足りないのでもう少しいじってみます。
Main.hsを編集し、コマンドラインから数値を受け取り、入力値以下の素数のリストを表示するプログラムを
作成してみました。
--入力した数値以下の素数のリストを出力する module Main where main :: IO () main = do putStr "最大値を入力してください: " input <- getLine --入力値を保持 let x = read input :: Double --入力値をDouble型に変換 putStrLn $ display x --display関数にxを渡した結果を画面表示 --画面表示用文字列を作成する関数 --引数、返り値の定義(Double型を受け取りString型を返す) display :: Double -> String --primes関数に2からxまでの整数のリストを渡し、結果をString型に変換 display x = show $ primes [2..floor x] --素数を抽出する関数 primes :: [Integer] -> [Integer] --整数のリストを受け取り、整数のリストを返す primes [] = [] --引数が空リストの場合、空リストを返す primes (x:xs) = x : primes [y | y <- xs, mod y x /= 0] --(x:xs) は先頭がx,残りの要素がxsのリストを表す。 --「先頭xを除外し、残りの要素のうちxで割った余りが0でない物を抽出」する処理を再帰的に定義
display関数とprimes関数を定義し、組み合わせて使用しています。
素数を抽出する箇所は、命令型言語であれば出力用のリストを定義してfor文で回して…とやっていきたいところですが、Haskellにはfor文は無いのでリスト内包表記と再帰を用いて実現しています。実際の処理を追うと以下のようになります。
[2,3,4,5,6,7,8,9,10,11,..] → 2:[3,5,7,9,11,..(2で割り切れない数)] → 2:3:[5,7,11,..(2でも3でも割り切れない数)] → ..
逐次的なアルゴリズムを書くことなく定義による記述が可能なので、
かなりコード量が少なくなっていることがお分かりいただけるでしょうか。
実行結果は以下のようになります。ちなみに先程はビルドしてから実行しましたが、
“stack runhaskell [ソースファイル]”を使うと1コマンドで実行可能です。
src $ stack runhaskell Main.hs 最大値を入力してください: 12 [2,3,5,7,11] src $ stack runhaskell Main.hs 最大値を入力してください: 45.6 [2,3,5,7,11,13,17,19,23,29,31,37,41,43] src $ stack runhaskell Main.hs 最大値を入力してください: 0 [] src $ stack runhaskell Main.hs 最大値を入力してください: a Main1.hs: Prelude.read: no parse
期待通りに動きましたが、入力値が数値以外だとエラーになってしまうので入力値チェックを入れたいところです。
ついでに、求めた素数とその素数を半径とする円の面積をペアで出力する機能を追加してみます。
--入力した数値以下の素数のリストを出力する --数値変換に失敗した場合はエラーメッセージを表示 module Main where main :: IO () main = do putStr "求める素数の上限値を入力してください: " input <- getLine --入力値を保持 let x = maybeRead input :: Maybe Double --入力値をMaybe Double型に変換 putStrLn $ display x --display関数にxを渡した結果を画面表示 --画面表示用文字列を作成する関数 display :: Maybe Double -> String --Maybe Double型を受け取り、String型を返す display Nothing = "数値を入力してください" --Nothingが渡された場合はエラーメッセージを表示 display (Just x) = show $ zip plist alist --Justが渡された場合は(素数,面積)のリストを表示 where plist = primes [2..floor x] -- 素数のリスト alist = map (\x -> fromIntegral x ^ 2 * 3.14) plist --素数のリストに円の面積を求める関数を適用 --素数を抽出する関数 primes :: [Integer] -> [Integer] primes [] = [] --引数が空リストの場合、空リストを返す primes (x:xs) = x : primes [y | y <- xs, mod y x /= 0] --「リストの先頭の要素をxとし、残りの要素のうちxで割った余りが0でない物を抽出」する処理を再帰的に表現 --変換時の例外を捕捉し、Maybe型を返す関数 --正常にパース出来た場合はJust、パースに失敗した場合はNothingを返す maybeRead :: Read a => String -> Maybe a maybeRead s = case reads s of [(x, "")] -> Just x _ -> Nothing
実行結果は以下のようになります。
src $ stack runhaskell Main.hs 求める素数の上限値を入力してください: 11.9 [(2,12.56),(3,28.26),(5,78.5),(7,153.86),(11,379.94)] src $ stack runhaskell Main.hs 求める素数の上限値を入力してください: abc 数値を入力してください
変換時の例外を捕捉するため、maybeRead関数を新たに定義しました。
ここで使用しているMaybe型は失敗の可能性を扱う仕組みで、
「Just 正常値」または「Nothing」のいずれかの状態をとります。
今回はdisplay関数でNothingを受け取るとすぐに失敗と判断していますが、例えばもっと複雑な処理を行う場合、
処理のどこかで失敗しても例外を発生させずに持ち越して、最終的な結果をNothingとするような使い方ができます。
(つまり、あちらこちらにNullチェックを書いたりする必要がないわけです。)
reads関数はString型から任意の型への変換を試みて、変換できた箇所までをタプルの第1要素、
変換できなかった箇所を第2要素に入れて返します。実行例は以下のようになります。
Prelude> reads "123" ::[(Int,String)] [(123,"")] Prelude> reads "123abc" ::[(Int,String)] [(123,"abc")]
display関数では上記の失敗判断に加え、素数のリストと面積のリストをzip(2つのリストを引数に取り、
各要素をペアにしてくれる組み込み関数)に渡した結果を返すように変更しています。
zipの実行例は以下のようになります。
Prelude> zip [1,2,3,4] [7,6,5,4] [(1,7),(2,6),(3,5),(4,4)]
面積のリストを作る際はラムダ式で定義した関数をmapしていますが、
もちろんもっと複雑な関数を他の場所で定義して使用することもできます。
このように、既存のコードには極力手を入れずに関数の追加・組み合わせによって機能追加を実現できました。
先に述べたHaskellのメリットが少しでも実感いただけたでしょうか。
おわりに
ここまでHaskellを使用して関数型プログラミングの基礎を学んできましたが、
実際の業務で関数型言語を扱う機会はまだまだ少ないと思います。
しかし命令型言語での開発においても、関数型プログラミングの考え方を取り入れた実装を行うことにより
関数型のメリットを部分的にでも享受することは可能なので、
今後関数型言語の知識を生かす機会を伺っていきたいと思います。
次回はHaskellを使用した、より実用的なwebアプリケーションの作成に挑戦してみたいと思います。
最後までお読みいただきありがとうございました。