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

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

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



haskellとは

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

関数型とは

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

 

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

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

 

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

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

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

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

haskell(そのメリット)

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

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

変数

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

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

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

関数

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

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

モナド

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

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

モナドとは?

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

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

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

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

環境構築

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

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

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

curl -sSL https://get.haskellstack.org/ | sh

or:

wget -qO- https://get.haskellstack.org/ | sh

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

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

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

stack --version

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

そして実行方法ですが、

stack runghc `file-name`

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

main = do 
    print "hello world"

と書き込んで、terminalから

stack runghc test.hs

で実行してみましょう。

"hello world"

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

main関数

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

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

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

main = do 
    // メイン関数処理

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

main = // メイン関数処理

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

変数

宣言

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

    variable :: Int 

初期化

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

    variable = "string"

トップレベル変数

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

variable = 100 * 5
main = do
    print variable

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

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

variable = 100 * 5
print variable

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

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

関数

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

定義

一行の場合

function = // 処理

複数行の場合

function = do
    // 処理

引数がある場合は

add x y = x + y
print (add 1 3)

呼び出し

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

関数の左結合、右結合

print (add 1 3)

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

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

パターンマッチング

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

printdata :: Integer -> String
printdata 10 = "ten"
printdata 100 = "hundred" 

variable = variable * 5
main = do
    print (printdata 10) 

ガード

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

printdata :: Integer -> String
printdata num
    | num > 10  = "ten"
    | num > 100 = "hundred"
    | otherwise = "???"

variable = variable * 5
main = do
    print (printdata 10) 

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

部分適用

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

main = do 
    print value 
    where 
        by3 = by 3
        value =  by3 4 --12

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

モナド

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

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

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

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

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

チェックするべきは

            | number > 50 -> return "high" :: IO String
            | otherwise -> return "low"  :: IO String

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

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

main = do
    print =<< randomNum
    var <- randomNum
    print var

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

var <- randomNum

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

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

print =<< randomNum

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

さらなる勉強のために

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

tutorial

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

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

フレームワーク

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

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

最後に

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

まとめ

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









この記事をかいた人

assa

京都でエンジニアをやっています。assaまたはえつーと覚えてください。 本業はwebで趣味でいろいろいじっています。よしなに