関数

【Haskell】関数の基本

【Haskell】関数の基本

Haskellの関数が満たすべきルールとHaskellでどのように関数を定義するかといった関数の基本について解説します。

Haskellの関数

Haskellは、純粋関数型プログラミング言語のひとつです。Haskellでのプログラミングでは関数が中心となるため、関数についてしっかりと理解することが重要です。

本記事では、Haskellの関数が満たすべきルールを確認したうえで、Haskellでどのように関数を定義するかといった関数の基本について紹介します。

Haskellの関数が満たすべき基本ルール

Haskellの関数の基本構成などについて説明する前に、Haskellの関数が従わなければいけないルールについて確認しておきましょう。

  1. 全ての関数は引数(0個以上)を受け取らなければならない。
  2. 全ての関数は値を返却しなければならない。
  3. 関数が同じ引数で呼び出されたときは常に同じ値を返却しなければならない。

これらは、数学における関数の基本定義の一部となっているものです。また、3番目の項目については「参照透過性」として有名な特徴です。

関数型プログラミング言語は、理解が難しいと言われますが、その基本となっている関数は上記のように非常に単純なルールに従っています。これらは非常に簡単なルールではありますが、Haskellの関数はこれらの基本的な制約が守られて動作するために、見通しの良い性質をもったプログラミング言語となっています。

一方でこの制約は、C/C++、Java、Python等のプログラミング言語に慣れ親しんだプログラマには厳しい制約に感じられるでしょう。Haskellで関数を実装するにあたっては、常に上記の基本ルールを意識するようにしてください。

関数の基本

関数の基本構成

Haskellにおける関数の基本構成は以下のようになっています。

関数の基本構成
関数名 引数1 引数2 ... = 式

最初に関数名があり、その後に与えられる引数が、引数1 引数2 …というように順番に列挙し、「=」の右側に返却値となる式を記載します。以下に関数の例をいくつか挙げてみましょう。

simple x = x

この関数は、引数xを受け取ってそのままxを返却する非常に簡単な関数です。関数を呼び出す際には、関数名と引数に渡す値を指定します。例えば、対話型シェルのghciで実行してみると以下のように使うことができます。

ghci> simple 10
10

引数x10を渡しているので返却値として10が返ってきています。上記はghciで対話型で実行していますが、もちろん他の関数から呼び出して実行する場合も同様です。

複数の引数が必要な場合は、以下のように関数名の後に引数を列挙し、それらを使った式を定義します。

addAndDouble x y = (x + y) * 2

この関数は、引数xとyを受け取って、加算した後に2倍した値を返却する関数です。こちらもghciで実行してみた結果は以下のようになります。

ghci> addAndDouble 5 10
30

このようにしてHaskellの関数は定義し、使用することができるようになります。

引数について

Haskellの関数が満たすべき基本ルールで「全ての関数は引数(0個以上)を受け取らなければならない。」というものを紹介しました。ここで「引数(0個以上)」と記載して十分に説明していなかったので補足をします。

Haskellでは、引数なしの関数を「nullary function(引数なし関数)」と呼んだりします。例えば以下のようなものが挙げられます。

x = 1 + 2
y = 3

上記のxyは、一般的なプログラミング言語では変数への値の代入となるでしょう。Haskellでは上記のようなxyを引数なしの関数と解釈できます。

xは、引数なしで呼び出され1+2が計算結果である3が返却される関数です。ただし、xは、最初に呼び出された際に1度評価され、その後関数呼び出しで毎回計算されるわけではない点には注意してください。yは、引数なしで3を返却する定数関数と解釈できます。この部分は、後述する変数の束縛と似ていますが厳密には異なる考えのものであり、なかなか理解しづらい部分かもしれません。

基本的には、Haskellの関数は引数を受け取って、返却値を必ず返すもので、返却値は引数にのみにより決まるというのが理解できていればよいですが、引数なしの関数というものもHaskellの関数であることは理解しておく必要があります。

where節による変数の束縛

プログラミングで処理を書いているときには、変数に値を代入して計算に使用していくというのが一般的な考え方です。ただし、Haskellでは、一度変数に設定した値は後で変更することができないため、変数の束縛となります。この不変であることをイミュータブル(immutable)といいます。

これは、C/C++、Java、Python等のプログラミング言語に慣れている人には非常に厳しい制約に思えますが、この特徴により変数の値が変わらないことが保証されるため、プログラムの見通しがよくなることがHaskellの重要な特徴の一つです。

Haskellにおいて変数に値を設定する方法として、where節を使用する方法があります。where節の使用方法を以下の簡単な例で見てみましょう。

multiplyAndScale x y = scaleFactor * product
    where
        product = x * y
        scaleFactor = 10

上記のプログラムは、xyを引数として受けとり、掛け算した後に10倍するような関数になっています。この時、where節を使って、productという変数にx * yを、scaleFactor10を設定して、関数の返却値の式としてscaleFactor * productを定義しています。

where節で定義した変数は、その関数からしか見えません。こういった範囲のことをスコープといいます。where節で定義した変数は、他の関数の名前空間に影響を与える(汚染する)ことはありません。

命令型のプログラミング言語では、変数を設定してから計算するのが一般的ですが、Haskellのwhere節を使った方法では、宣言が後に来る点が異なります。Haskellでは一度変数に値が設定されると値はその変数に束縛されて変更されないため、計算の後に記載してあっても何も問題ないわけです。

where節を使用すると、関数が返却する式の定義が先に来ることから関数がどういった値を返すのか分かりやすいというメリットがあります。

let式による変数の束縛

変数に値を束縛する方法についてwhere節を紹介しましたが、変数を束縛する方法としては、let式を使う方法があります。この方法は、変数の宣言が先に来るため、命令型プログラミング言語の宣言の順番に似ています。

let式の使用方法をwhere節の説明と同様のプログラムで見てみましょう。

multiplyAndScale x y = let product = x * y
                           scaleFactor = 10
                       in product * scaleFactor

上記例のようにletの後で変数を定義します。letで設定した変数は、inの範囲内のスコープでのみ有効となります。

where節とlet式の違いとどちらを利用するか

変数を束縛する方法としてwhere節とlet式を見てきました。これらの違いとして明確なことはwhereは「」であるのに対してletは「」であるという点で異なります。

let式は、その名の通り式であるため、関数の定義だけでなく、コード中のどんなところでも使用することが可能です。例えば、ghciを使って計算する式の内で以下のようにlet式を使うことも可能ということです。

ghci> 2 * (let x = 10; y = 20;  in x + y + 30) 
120

let式では上記のようにセミコロン(;)で区切ることもできるので、複数変数を1行で束縛するといったことも簡単にできます。また、他にも、let式はリスト内包表記で使うなど色々な場面で局所的に変数を束縛することに使えます。

では、関数の変数の束縛はwhere節とlet式のどちらを使って行うのが良いでしょうか。where節でもlet式でもできることに違いはありません。Haskellはコンパイル型の言語ですので、コンパイル時に最適化され、これら宣言方法の違いが性能に大きく影響を与えることもありません。

そのため、関数の変数の束縛において、where節かlet式かどちらを採用するのかはスタイルの問題です。プロジェクトごとに統一感を持たせて使うと良いかと思います。

個人的には、where節の方が関数の宣言が先に来ることから見通しがよくなるため分かりやすいかなと感じています。

カリー化と部分適用

Haskellの関数については「すべての関数は、引数を1つだけ取る」という特徴があります。これは、カリー化という考え方を理解する必要があります。また、カリー化は、部分適用に非常に重要な考え方です。

カリー化と部分適用については「カリー化と部分適用」でまとめているので参考にしてください。

まとめ

Haskellの関数が満たすべきルールとHaskellでどのように関数を定義するかといった関数の基本について解説しました。

Haskellは、純粋関数型プログラミングで習得は難しい言語であると言われていますが、その中心となる関数が満たすべき基本ルールは「全ての関数は引数(0個以上)を受け取らなければならない。」「全ての関数は値を返却しなければならない。」「関数が同じ引数で呼び出されたときは常に同じ値を返却しなければならない。」といったシンプルなものです。

この基本ルールを認識したうえで、Haskellの関数の構文を学ぶとすっきりと理解が進むと思います。本記事では、関数の基本的な構成と関数で変数に値を束縛する方法としてwhere節やlet式を紹介しました。

Haskellでは、関数中心としてプログラミングが進められるため関数の基本は非常に重要です。ぜひ基本をしっかり理解するようにしてほしいと思います。