【haskell入門】関数型プログラミングでできることやメリット、環境構築を徹底解説

「関数型を学んでみたいということで関数型の傾向が強いhaskellを触ってみた。しかし、そもそも既存のものと違いすぎて挫折した」
「これを学んだところで何に活かせるのかわからない」

といった人は結構多いのではないでしょうか。
その上、現在だと圏論とセット語られることもあったりするので、更に概要の把握が困難になってしまって無意識に敬遠してしまっている人もいるのではないでしょうか?
なので、本記事ではhaskellの環境構築からその関数型の概念やメリットなどをわかりやすく解説していきます。

haskellとは

haskellとは関数型のプログラミング言語の一つで純粋関数型という関数型の思想に強く沿った文法や機構をもつプログラミング言語です。
現在であれば、フロントエンドのelmもhaskellの影響を大きく受けた言語です。
後ほど詳しく説明しますが、純粋関数型というだけあり、関数型の機能を多く受け持ち且つオブジェクト指向の書き方を制限しているのが特徴です。

関数型とは

まずは源流といえる関数型について解説しましょう。
関数型の考え方は一言で表すと 「関数の副作用を極限まで減らす」これにつきます。
そもそも。現在のプログラミングには関数の振る舞いが関数が呼び出されている部分ではブラックボックスという性質がありました。
その結果、その関数を実行した結果どこに影響を与えたのかわからず、バグを生み出す原因となってしまっていたのです。少なくとも僕にはたくさんの心当たりと実際にやっちまった経験があります。
少し例を出しましょう。

[code language=”php”]

$test = 10;
function change(){
$test = 100;
}

//main処理
change();
print ($test) // 100
[/code]

上記はグローバル変数があってそれを関数によって変更しています。これは関数の宣言までワンセットで書いているから良いのですが、

[code language=”php”]
//main処理
change();
print ($test) // 100
[/code]

の部分だけ切り取った場合はchange関数は一体何をやっているのか?と疑問に感じるでしょうしコードの概要を把握するのに苦労します。
これが関数の副作用とその弊害なのです。要は、関数を実行した前と実行した後に戻り地以外部分にの影響を与えることをいうのです。

無論、上記の例は適切なコメントや命名規則によってある程度はカバーできますが、悲しいことに人間はミスを犯してしまう生き物なのです。
特定のルールに則って行動することをやり続けるというのはヒューマンエラーの性質上難しく、そのエラーのチェックする手間でさらにコストが増えます。
なので、そういった関数の副作用減らすコードをプログラミングの文法で簡易的に書ける or 強制させて関数の副作用をへらそうというのが関数型の考え方になります。

haskell(そのメリット)

先程もいいましたが、haskellは既存のオブジェクト指向的な書き方をあえて制限させている特徴があります。
こうすることで、俗に言うアンチパターン(副作用の影響が大きすぎて保守しにくいなど)を実装しないように言語側が強制しているのです。
そのため、haskellの書き方や考え方を知っていると、普段のオブジェクト指向では意識しない副作用を強く意識することになるので、特に設計やテストの部分に
その知見を活かすことができます。

さてそれではオブジェクト指向や他の言語と何が違うのかという部分で有名どころを紹介していきましょう。

変数

  • 1. 再代入できない
  • 2. 初期化は一度しかできない
  • 3. 遅延評価

これは他の関数型言語でもそうですが、再代入の禁止の特徴があります。
また、これに加えて遅延評価という独特な機構をもっています。
これは、簡単に言うと、その変数が必要とされるまで評価されないという仕組みです。

この関係で、haskellは処理を余計な処理をせずに実行できます。
(そして、簡単に無限ループになったりもしますが・・・)

関数

  • 1. 左結合 右結合 中間結合などの 柔軟な関数の呼び出し方
  • 2. パターンマッチング、ガードによる引数の値から処理を分岐させる機能の充実

こちらの関数の特徴は実際に触ってみましょう。これらは、関数を階層的に呼び出したり、関数の定義を簡単にするための機能がそろっています。

モナド

  • 1.副作用の影響を隠蔽化する機能
  • 2. 1.のオブジェクトから値を取り出すことのみに特化したインターフェース

モナドについては ちょっと別個で解説をしておきます。

モナドとは?

さて関数型は副作用を減らすということを言いましたが、一つ問題があります。
それは副作用をを減らすことで一部のコードが冗長化することです。

そもそもの話として、例えばデータベースの接続やファイルの読み書きは関数が実行されるたびに、何らかの状態変数に影響を与えているので、すべて副作用を伴うものです。
ということは完全な関数型の言語では、これらの処理を使うことができなくなります。
(メイン関数にすべての処理をぶち込めばできなくないかもしれませんが、そんなクソコードを強制されるプログラミング言語は嫌すぎます)

なので、これらを扱うために出てくるのがモナドやアクションといった機能です。
イメージとしては、副作用を持つ(要はなんらかの状態変数を持っていてそれを変更して処理をおこなう)関数をラッピングしてそこから、値を取ってくるようにするのです。
このラッピングで副作用の範囲を閉じ込めることによって、影響を最小限にするのです。

この話だけ聞くと、単純にクラスでカプセル化したように聞こえるかもしれません。
実際に思想自体はにており、副作用は特定の範囲内に閉じ込めることで、外部への副作用をなくすという考え方です。
ただ、これの特徴は値を取り出すということに特化している点です。

環境構築

では環境の構築をしてきましょう。
今回はできるだけ解説にフォーカスしたいため、
stackというツールを使用します。

これは、OS間やhakellのバージョンの差異を吸収して安定してhaskellを起動することができます。
無事に設定が完了すれば

Unix系(macを含む)タイプは

[code language=”scala”]
curl -sSL https://get.haskellstack.org/ | sh
[/code]

or:

[code language=”scala”]
wget -qO- https://get.haskellstack.org/ | sh
[/code]

をターミナルで叩いてください

windowsは下記の公式ドキュメントからwindows用のインストーラーをダウンロードして
起動してください。
https://docs.haskellstack.org/en/stable/README/#how-to-install

無事にインストールできれば、

[code language=”scala”]
stack –version
[/code]

でバージョンを確認できます。

そして実行方法ですが、

[code language=”scala”]
stack runghc `file-name`
[/code]

例としてこの test.hsというファイルを用意して

[code language=”scala”]
main = do
print “hello world”
[/code]

と書き込んで、terminalから

[code]
stack runghc test.hs
[/code]

で実行してみましょう。

[code]
“hello world”
[/code]

と表示されれば成功です。

main関数

では実際にhaskellの文法を解説していきます。
はじめから全部覚えようとすると混乱するため、関数型の概念に近いもののみに絞って解説していきます。

まずは関数を定義してみましょう。
haskellはすべてを関数で表す関係上、最初に実行する関数が最初から決まっています。
これをメイン関数と呼びます。

そしてこのように書きます。

[code language=”scala”]
main = do
// メイン関数処理
[/code]

メイン関数の処理が一行の場合は do を省略してこのようにかけます。

[code language=”scala”]
main = // メイン関数処理
[/code]

今後は特に指定がない限り、でてくる処理はmain関数の中に書いておけば大丈夫です。

変数

宣言

宣言はこのように変数と型を宣言します。

[code language=”scala”]
variable :: Int
[/code]

初期化

このように値をいれて初期化できます。hskqllは型推論を持っているので、宣言して型をつけなくても初期化できます。

[code language=”scala”]
variable = “string”
[/code]

トップレベル変数

haskell版グローバル変数みたいなもので

[code language=”scala”]
variable = 100 * 5
main = do
print variable
[/code]

このように、関数のスコープの外で宣言することで、どの関数からでも値を読み取ることができます。

ちなみに、このhaskellは他の言語のように値を記憶領域に保存するという性質を持たず式そのものを保存して呼び出されたときにその式を評価するのです。
例えば

[code language=”scala”]
variable = 100 * 5
print variable
[/code]

であれば、 variableは500の値を保存せずに 100 * 5 という式を持っているのです。
そのvariable をprint variableで評価、console画面に表示する関数の引数に渡す

このとき初めて、値が計算されて値が出されるのです。
その関係でhaskellは値を代入と言わずと束縛と呼称されることがあります。

関数

では本題の関数を見ていきましょう。
とはいっても、メイン関数のときにちょこっとかぶるのでそれを含めて見ていきます。
また、関数をメインに据えているだけあり、特別な文法が多いです。
ただ、今回は基本的、特徴的な機能のみに厳選して紹介します。

定義

一行の場合

[code languaget=”scala”]
function = // 処理
[/code]

複数行の場合

[code languaget=”scala”]
function = do
// 処理
[/code]

引数がある場合は

[code languaget=”scala”]
add x y = x + y
print (add 1 3)
[/code]

呼び出し

引数がある場合の宣言と呼び出し

関数の左結合、右結合

[code languaget=”scala”]
print (add 1 3)
[/code]

上の部分ですが、haskellでは関数は左から引数の順に一つずつ評価されていくので、もし print add 1 3 書いた場合はどうなるかというと
((print add) 1) 3 という順番で関数が評価されていきます。エラーとなります。

なので、関数 + 引数部分の引数部分を() でくくるか $ という演算子を使用します。
$はこれ以降の関数を 右から順に評価する という指定を行うので、print $ add 1 3 でも正常に評価されるようになります。

パターンマッチング

特定の引数のときに特定処理をやりたいというのはごくごく当たり前にあります。
haskell はそれを文法として組み込んでいます。
パターンマッチングは引数が特定の値の場合という条件で処理を分岐させます。

[code language=”scala”]
printdata :: Integer -> String
printdata 10 = “ten”
printdata 100 = “hundred”

variable = variable * 5
main = do
print (printdata 10)
[/code]

ガード

ガードは特定の引数を範囲に基づいて処理を分岐させます。

[code language=”scala”]
printdata :: Integer -> String
printdata num
| num > 10 = “ten”
| num > 100 = “hundred”
| otherwise = “???”

variable = variable * 5
main = do
print (printdata 10)
[/code]

使用する変数の一行下から
| 式 = 値
という形で処理を分岐させます。
そしてどの範囲にも当てはまらない場合は
| otherwise = 値
で処理を記述します。

部分適用

オブジェクト指向型の関数は必要とされる引数がすべて揃っていないと、関数を実行することができません。
ただ、haskell は関数を部分的に適用させて、あらたに生成したりできます。

[code language=”scala”]
main = do
print value
where
by3 = by 3
value = by3 4 –12
[/code]

このように、第一引数だけ渡して、値を固定させて新しい関数を生成できます。

モナド

機能だけ解説すると理解しにくい部分があると思うので、
今回は例として乱数から値を受け取って文字を返すものをモナドで書いてみます。

[code language=”scala”]
import System.Random

randomNum = do
number <- getStdRandom ( randomR (0,99)) :: IO Int
case number of
_
| number > 50 -> return “high” :: IO String
| otherwise -> return “low” :: IO String

main = do
print =<< randomNum
var <- randomNum
print var
[/code]

ちょっと例を簡単にするため、既存の乱数生成ライブラリを使用していいます。

[code language=”scala”]
randomNum = do
number <- getStdRandom ( randomR (0,99)) :: IO Int
case number of
_
| number > 50 -> return “high” :: IO String
| otherwise -> return “low” :: IO String
[/code]

上記の部分は乱数を生成して50以上であれば high 50以下であれば lowという文字列を返し、
その結果を文字列として返しています。

チェックするべきは

[code language=”scala”]
| number > 50 -> return “high” :: IO String
| otherwise -> return “low” :: IO String
[/code]

のreturn の部分です。
これはモナドとして値を返すということを定義しており、
randomNumは関数ではなく、モナドとなっています。

そしてデータを取り出すのが以下の部分です。

[code language=”scala”]
main = do
print =<< randomNum
var <- randomNum
print var
[/code]

見ての通り、関数とは違い独特な演算子を用いて値をとりだします。
単純に値を取り出し、変数に束縛する場合は

[code language=”scala”]
var <- randomNum
[/code]

のように <-という演算子を使っています。

対して取り出した値をそのまま別の関数の引数に渡す場合は

[code language=”scala”]
print =<< randomNum
[/code]

という独特な演算子を使います。

さらなる勉強のために

今回紹介したのは、開発環境、haskellなど特徴的なもののみをピックアップしています。
なので、これ以降の詳しい勉強するための資料を載せておきます。

tutorial

Learn You a Haskell for Great Good!
英語ですが、例と詳しい仕様を解説が豊富に揃っているので、読んでいるだけでも
結構勉強になります。

ウォークスルー Haskell
日本語で書かれているhaskellの情報です。
どちらかというと、ドキュメントに近いですが、仕様+サンプルコードという構築で非常に読みやすいです。
概要だけを見たいのであればさっと読めるこちらが良いでしょう。

フレームワーク

haskellは未だマイナーな言語ですが、いくつかのフレームワークがあります。
有名所としてはフロントエンドの miso
ウェブアプリケーションの yesod
また、 今話題の elmもhaskellの影響を受けています。

おそらくやろうと思えば、フロント、サーバーすべてがhaskellで固めたアプリケーションができると思います。

最後に

最後に注意点ですが、関数型は確かに素晴らしいものですが、決して万能の杖ではありません。
実際、関数型が注目されたの最近ですし、そのために、メジャー言語に比べて、haskellなどはまだ開発環境が整っていません。
なので、これをプロジェクトに持ち込もうとすると、ドキュメントが少なくて問題を解決できなくなります。
しかし、関数型に触れることでオブジェクト型に触ったときに、副作用を産まないということを改めて意識できるようになり、その結果保守性が高いく、テストが書きやすいコードを書けるようになることは十分期待できるでしょう。

まとめ

いかがだったでしょうか
関数型やモナド、圏論と結構な情報が錯綜していて、無駄にハードルが上がっているというのを感じているので、それらのハードルを少しでも下げられれば幸いです。