はじめに
オブジェクト指向でなぜつくるのかを読んでいたところ、ちょうど関数型プログラミングとの比較があったのでまとめてみました。
関数型プログラミング
関数型プログラミングは、計算を「関数」の適用することを基本としています。ここで言う関数とは副作用のない純粋な関数のことを指していて、CやPythonで書く関数とはまた別の意味です。副作用というのは、関数の外部状態を変更したり、外部からの入力に依存したりすることを指します。関数型プログラミングの特徴をざっくりと挙げると以下の通りです。
- 不変性
- 高階関数
- 再帰によるループ
それぞれの特徴について説明していきます。
不変性
関数型プログラミングでは、一度作成されたデータを変更することはできません。
関数型では、代入のことを「束縛」といいます。
let x = 5
-- let x = x + 5 -- この書き方はできない
let y = x + 5
2. 高階関数
高階関数は、関数を引数として受け取ったり、関数を返す関数のことを指します。
以下ではmap関数を実装しています。mapはリストの各要素に対して、関数を適用する高階関数で、例では各要素を2倍にしています。
myMap :: (a -> b) -> [a] -> [b]
myMap _ [] = []
myMap f (x:xs) = f x : myMap f xs
-- 使用例
main :: IO ()
main = print (myMap (* 2) [1, 2, 3, 4, 5])
-- 出力: [2, 4, 6, 8, 10]
3. 再帰によるループ
関数型にはループ文がありません。ループをさせる場合は、代わりに再帰処理を使います。以下は、リストを反転させるプログラムの例です。
-- リストを反転する再帰関数
reverseList :: [a] -> [a]
reverseList [] = []
reverseList (x:xs) = reverseList xs ++ [x]
main :: IO ()
main = print (reverseList [1, 2, 3, 4, 5]) -- 出力: [5, 4, 3, 2, 1]
ポリモーフィズムと関数型プログラミング
高階関数の説明からもわかるように、関数型言語では、関数を値として扱うことができます。関数そのものを変数に格納したり、別の関数の引数や戻り値に指定することも可能です。この仕組みを応用することで、全体の処理が共通で一部の処理だけを入れ替えたい場合に、簡潔に処理することができます。これはまさにオブジェクト指向のポリモーフィズムと一緒ですね!
ためしにオブジェクト指向言語のポリモーフィズムを関数型言語で再現してみます。
以下で、異なる車種(ガソリン車と電気自動車)の燃費計算を考えてみます。
オブジェクト指向言語
abstract class Car {
protected double distance;
protected double energyUsed;
public Car(double distance, double energyUsed) {
this.distance = distance;
this.energyUsed = energyUsed;
}
abstract double getEfficiency();
}
// ガソリン車
class GasCar extends Car {
public GasCar(double distance, double fuelUsed) {
super(distance, fuelUsed);
}
@Override
double getEfficiency() {
return distance / energyUsed; // 燃費 = 走行距離 / 使用燃料
}
}
// 電気自動車
class ElectricCar extends Car {
public ElectricCar(double distance, double energyUsed) {
super(distance, energyUsed);
}
@Override
double getEfficiency() {
return distance / (energyUsed / 33.7); // 燃費 = 走行距離 / (使用エネルギー / 33.7)
}
}
public class Main {
public static void main(String[] args) {
Car gasCar = new GasCar(100.0, 5.0);
Car electricCar = new ElectricCar(100.0, 20.0);
System.out.println("ガソリン車の燃費:" + gasCar.getEfficiency()); // ガソリン車の燃費:20.0
System.out.println("電気自動車の燃費:" + electricCar.getEfficiency()); // 電気自動車の燃費:168.5
}
}
Carクラスを定義し、getEfficiencyメソッドを抽象メソッドとして宣言します。そして、GasCarとElectricCarクラスがCarクラスを継承し、それぞれガソリン車と電気自動車の燃費計算ロジックを提供します。Car型の変数に異なる実装(GasCar、ElectricCar)を代入し、同じgetEfficiencyメソッドを呼び出すことで異なる動作を実現します。ここまでがポリモーフィズムの実装です。
関数型言語
続いて、関数型言語です。上記のオブジェクト指向言語と同じ実装をしてみます。
-- ガソリン車の燃費計算関数
gasEfficiency :: Double -> Double -> Double
gasEfficiency distance fuelConsumed = distance / fuelConsumed
-- 電気自動車の燃費計算関数
electricEfficiency :: Double -> Double -> Double
electricEfficiency distance energyConsumed = distance / (energyConsumed / 33.7)
-- 高階関数を使って燃費を計算
calculateEfficiency :: (Double -> Double -> Double) -> Double -> Double -> Double
calculateEfficiency efficiencyFunc distance energyConsumed = efficiencyFunc distance energyConsumed
main :: IO ()
main = do
let distance = 100.0 -- 走行距離
let fuelConsumed = 5.0 -- 使用燃料(ガソリン)
let energyConsumed = 20.0 -- 使用エネルギー(電気)
let gasCarEfficiency = calculateEfficiency gasEfficiency distance fuelConsumed
let electricCarEfficiency = calculateEfficiency electricEfficiency distance energyConsumed
putStrLn $ "ガソリン車の燃費:" ++ show gasCarEfficiency -- ガソリン車の燃費:20.0
putStrLn $ "電気自動車の燃費:" ++ show electricCarEfficiency -- 電気自動車の燃費:168.5
gasEfficiencyとelectricEfficiencyという関数を定義し、それぞれガソリン車と電気自動車の燃費計算処理を実装します。次に、calculateEfficiencyという高階関数を定義し、ここにgasEfficiencyやelectricEfficiencyを渡すことで、異なる車種の燃費計算を処理します。
オブジェクト指向言語では、一つのメソッドだけを入れ替えたい場合でも、いちいちスーパークラスとサブクラスを定義する必要があります。一方、関数型言語では、関数を引数として渡すだけで済みます。
まとめ
このように、関数型言語では関数を値として扱うことで、オブジェクト指向プログラミングのポリモーフィズムと同じ効果をよりシンプルに実現できます。関数型のアプローチは、コードが短く、明確で、柔軟性が高いという利点があります。