技術
Scalaも採用した関数型言語の考え方とは何か?その特徴を純粋関数型言語Haskellで学ぶ
2023.05.16
著者:井上 健太
関数型言語とは
関数型言語をはっきり定義することは難しいですが、本記事では以下のように考えます。
主に文や命令を組み合わせてプログラムする手続き型言語に対し、主に式や関数を組み合わせて記述するのが関数型言語である。
それでは手続き型言語と関数型言語の違いを比較するために、整数n1
とn2
の最小公倍数を計算するプログラムを見ていきましょう。
"文"で記述する手続き型言語
まずは手続き型言語であるjavaを見ていきます。
public class GCD {
public static void main(String[] args) {
final int n1 = 15;
final int n2 = 10;
int n = n1;
int m = n2;
int k;
System.out.println(n); // n == 15
while (m > 0){
k = m;
m = n % m;
n = k;
}
System.out.println(n); // n == 5
}
}
代入文やwhile
文などの文を組み合わせてプログラムを作っていきます。
また、代入によって変数の値が変化するため、上のSystem.out.println(n)
の出力結果がwhile
文の前では15
、後では5
になるように、同じ命令文でも異なる出力になる場合があります。(後に詳しく説明します)
これに対し、関数型言語では以下のようになります。
"式"で記述する関数型言語
関数型言語Haskellでの最小公倍数を求めるプログラムは以下のようになります。
n1 :: Int
n1 = 15
n2 :: Int
n2 = 10
euclid :: Int -> Int -> Int
euclid n m =
if m == 0
then n
else euclid m $ n `mod` m -- euclid m (n `mod` n)
main :: IO ()
main = print $ euclid n1 n2 -- print (euclid n1 n2)
関数型言語ではwhile
文などはなく、式や関数の組み合わせで記述していきます。
上の例で説明すると、main
関数のeuclid
やprint
が関数であり、euclid n1 n2
やprint $ euclid n1 n2
が式になります。
このような関数や式の組み合わせであっても手続き型言語と同じプログラムが書ける、すなわちチューリング完全であることが保証されており、プログラム記述能力は変わりません。
参照透過性と副作用
Haskellのように式のみで記述するということは、代入文のように変数を書き換えたりしないということになります。すなわち全ての変数の値や関数は宣言時のみに定義され、後から書き換えを行うことのできないイミュータブルな宣言になるということです。 この仕様は以下のような参照透過性を満たし、かつ副作用が存在しないものになります。 ここで、参照透過性とは同じ関数に同じ引数を渡した場合、必ず同じ結果を返すことを表し、副作用とは関数内で他の外部変数や状態を書き換えたりすることを表します。
つまり簡単にまとめると
- 参照透過性:外部の影響を受けない
- 副作用がない:外部に影響を与えない
ということです。 これらの両方を満たす関数を純粋関数と呼び、Haskellのように(ほとんど)すべての関数が純粋関数であるような関数型言語のことを純粋関数型言語と呼びます。
外部入出力を含むプログラムは原理上、参照透過にはならず副作用を含むことになるため、純粋関数型言語は一見するとこれらの処理を行えないように思えます。しかし、これら入出力が発生する部分と純粋な部分を分離することが可能ならば純粋関数型言語と呼べることになっています。実際にこれはモナド等を活用することにより実現されています。(ほとんど全ての関数が純粋であるようなというのはそういう意味です。)
参照透過性に注目する
関数型言語とも呼ばれることもあるScalaはオブジェクト指向な面もあり純粋関数型言語ではないものの、ミュータブルであるかイミュータブルであるか、つまり後から変更できないという参照透過性が大きく意識されている言語の1つです。実際に変数定義はミュータブルのvar
とイミュータブルのval
の2つあり、ライブラリもmutable
とimmutable
の2つに分離されています。
参照透過性を満たす言語によるプログラムには以下のような特徴があります。
参照透過性を満たす言語の特徴
Haskellのような参照透過性を満たすプログラムの特徴としては以下のようなものが挙げられます。
テストやバグ修正が容易
参照透過性を満たすプログラムの場合、変数の値や関数の実行が文脈で変化しないため単体のテストのみで完結し、エラーが起きた際にもその定義だけを確認すればよくなり、バグ修正が比較的容易になります。 参照透過性を満たさないプログラムの場合は文脈にその出力が依存することがあるため、バグが発生した場合、その定義だけではなく前後の文脈も確認しないといけなくなる場合があり、その分の手間がかかります。
並列処理を行いやすい
参照透過性を満たすプログラムにおいては各関数は実行の順序によらず同じ入力に対し、必ず同じ値を返します。そのため入力値が同じであれば並列処理をしたとしても結果が変わらないことが保証されます。参照透過でない言語の場合、プログラムの実行順によって返り値が変化する場合があり、並列処理が向かないプログラムも存在します。
外部入出力との兼ね合いを考慮する必要がある
外部データベース等の入出力がある場合、読み込みを行う関数は参照透過性を満たさなくなり、書き換えを行う関数は外部に影響を及ぼす副作用があるため、原理上、純粋性を満たさなくなります。そのため、このような関数をどのように扱うかが問題になってきます。 純粋関数型言語Haskellの場合、これをモナド(Monad)と呼ばれる機構を用いて外部入出力等の参照透過でない関数や副作用のある関数と内部処理の純粋関数を分離し、内部処理に関しては純粋性を保ったままプログラミングすることが可能になっています。 このモナドと呼ばれる機構に関しては別の記事で詳しく説明していきます。
関数型言語の書き方を参考にする
先ほどの例で見たように純粋関数型言語Haskellの1つの特徴は参照透過性が保証されていることでした。つまり、入力が同じならいつでも返り値が同じになるような関数のみで構成されているということです。これには上記のようなメリットがあり、手続き型言語であってもある程度、参照透過なプログラムを作ることが可能です。
参照透過性を意識する
代入文を書くと一般にはそれ自体が副作用を生じる原因になります。よって、先ほどのjavaの例なるべく代入文を使わずに記述してみます。
public class GCD {
public static void main(String[] args) {
final int n1 = 15;
final int n2 = 10;
System.out.println(euclid(n1,n2));
}
public static int euclid(int n, int m) {
int k;
while (m != 0){
k = m;
m = n % m;
n = k;
}
return n;
}
public static int euclid2(int n, int m) {
if (m == 0) {
return n;
} else {
return euclid2(m,(n % m));
}
}
public static int euclid3(int n, int m) {
return (m == 0) ? n : euclid3(m,(n % m));
}
}
元のプログラムからeuclid
関数を分離してみます。1つ目は単に分離しただけで、引数n,m
と内部変数k
の値が変化します。基本的にループ文を書こうとすると書き換えが行われ、内部的には副作用が生じます。というよりもループ文は副作用があるからこそ同じ命令を繰り返すことで変化が生じ、意味をもたらしているものだと考えられます。しかしながらeuclid
自体は外部変数に依存せず、また外部変数を書き換えないので、euclid
は参照透過な純粋関数になります。
関数型言語ではループの代わりに再帰呼び出しを行うので、それを意識したものがeuclid2
です。この関数では変数への代入がないため副作用はなく、見た目はHaskellでのプログラムに近いです。しかし、細かいところをいうとif
文は式ではなくあくまで文なので、これを三項演算子を用いて式で書いたeuclid3
が最も関数型風な記述になります。
ただし、ここまでこだわって関数型言語風にする必要はなく、実用の上では最初のeuclid
関数を作ることで参照透過になるので、これで十分です。
関数型言語の他の特徴
他にも高階関数、つまり関数引数が取れたり返り値に関数を返したり型引数が取れたりするなどの特徴もあります。しかしながらこれは関数型言語としての特徴というよりも言語の種類に依存しない各言語における仕様の特徴なので、この記事で特に解説はしません。(確かに、ほとんどの関数型言語で高階関数が記述でき、かつよく使われるものではあります。)
まとめ
Scalaなどの言語で採用されている関数型言語の考え方について、そもそも関数型言語とは何か、その特徴について解説しました。