導入
roadmap.shのFlutterのAdvanced Dartの章の5本目はFunctional Programming
です。
今回は、Dartにおける関数型プログラミングの基本概念を整理し、それがどのようにしてFlutterアプリ開発に役立つかを紹介していきます。
関数型プログラミングとは?
関数型プログラミング(Functional Programming)は、近年注目されているプログラミングの手法です。
この方法では、データの不変性や副作用の排除、関数の再利用性を重視します。
Dartはオブジェクト指向プログラミングの言語ですが、関数型プログラミングの要素も取り入れています。
これを活用することで、効率的でバグの少ないコードを書けるようになります。
関数型プログラミングの基本概念
関数型プログラミングでは、次のような概念が重要です。
- 高階関数: 関数を引数として受け取ったり、関数を返り値として返すことができます。
- データの不変性: データは変更せず、新しいデータを作成して使用します。
- ラムダやクロージャ: Dartでは、匿名関数(ラムダ)や、他の関数内で定義された関数(クロージャ)をサポートしています。
- 純粋関数: 同じ入力に対して常に同じ出力を返す関数です。外部の状態に依存せず、副作用がありません。
ラムダ計算
関数型プログラミングの基盤はラムダ計算にあります。
ラムダ計算では、変数、関数の定義、関数の適用といった要素を使って計算を行います。
ラムダについては、こちらの記事を参考にしてください。
https://qiita.com/phibi-soon/items/4eaffc3f3d69e5aba6a5
以下がラムダ計算の例です。
// 型の定義
typedef bool FunctionName(String value);
// メソッドの実装
bool functionName(String value) => true;
void mainApp() {
// メソッドを変数に格納
var x = functionName;
var result = getValue(x, 'Test');
}
bool getValue(FunctionName function, String value) {
// 第一引数にFunctionName型に合致するメソッドを受け取ることができる
return function(value);
}
さて、上記例は、基本的な構造としては以下の処理と同等といえます。
では、上記例はどのようなメリットがあるのでしょうか?
bool functionName(String value) => true;
void mainApp() {
var result = functionName('Test');
}
ラムダ計算を利用した際のメリット
ラムダ計算を利用したソースでは、関数型プログラミングの要素を取り入れており、関数を第一級オブジェクトとして扱っています。
繰り返しになりますが、関数を変数に格納し、他の関数に渡している点が特徴です。
この設計は以下のようなメリットがあります。
- 柔軟性
- getValue関数に異なるロジックを持つ関数を渡すことができます。
- つまり、型が
FunctionName
であれば、functionName関数ではない異なる処理を行う関数を渡せます。 - これにより、コードの再利用性が高まります
- つまり、型が
- getValue関数に異なるロジックを持つ関数を渡すことができます。
- 高階関数の利用
- Dartでの高階関数の利用により、関数を引数として渡したり、他の関数から関数を返したりすることができます。これにより、コードのモジュール化や抽象化が容易になります。
- ラムダ式や匿名関数の利用
- 特定の処理のために、その場限りの匿名関数やラムダ式をgetValueに渡すことも可能です。
- これにより、処理のカスタマイズが容易になります。
高階関数
高階関数とは、前項でも出てきたように、他の関数を引数に取ったり、関数を返す関数のことです。
これには、「継承よりもコンポジションを重視する」というオブジェクト指向プログラミングの考え方が関連しています。
関数型プログラミングでは継承がなく、複雑な計算は複数の関数と変数の組み合わせによって成り立っています。
不変性
関数型プログラミングでは、データは変更せず、新しいデータを作り直します。
これはFlutterのウィジェットツリーの再構築にも似ています。
副作用の排除
副作用とは、関数が外部の変数を変更したり、外部の変数に依存することです。
以下の例には、副作用が存在します。
String value = 'Test';
bool isTest() {
return value == 'Test';
}
この場合、一見value
の中身は’Test’
であり、isTest()
はtrue
を返しそうです。
しかしそんなことはありません。誰かがどこかでvalue
を書き換える実装をしているかもしれません。
その場合、isTest()
はtrue
を返さない可能性があります。
つまり、副作用が存在する実装では、他のプロセスが変数の値を変更し、関数が実行されるたびに異なる結果を返す可能性があります。
関数型プログラミングでは、副作用のない純粋関数を推奨されています。
以下の例では、isTest
は、入力値に応じて適切な値を返却するようになっています。
bool isTest(String value) {
return value == 'Test';
}
Dartでの関数型プログラミング
状態管理
Dartで状態管理を行う場合、関数を静的にして、クラスインスタンスに状態を保持させないようにします。
また、@immutable
アノテーションを使って不変の状態を維持し、フィールドをfinal
としましょう。
@immutable
abstract class AppState {
const AppState(this.value);
final String value;
}
高階関数と部分適用
部分適用とは、関数の一部の引数を固定し、残りの引数を後から渡せる関数を作る手法です。
int multiply(int a, int b) {
return a * b;
}
// aを固定して部分適用する場合
int mutiplyBy2(int b) {
return multiply(2, b);
}
さらに以下のコードは、関数型プログラミングと部分適用を組み合わせた例です。
multiplyBy
関数を使うと、a
を設定することが可能になります。
Function(int) multiplyBy(int a) {
return (int b) => multiply(a, b);
}
void main() {
var multiplyBy3 = multiplyBy(3);
print(multiplyBy3(4)); // 12
print(multiplyBy3(5)); // 15
}
最後に
関数型プログラミングは、特に大規模で複雑なアプリケーション開発において、その価値を発揮します。
Dartを利用した開発では、関数型の考え方を取り入れることで、コードの再利用性が高まり、バグの少ない堅牢なシステムを構築することができます。
関数型プログラミングを理解し、活用することで、よりモジュール化された柔軟なコードを実現し、プロジェクト全体の品質を向上させることができるでしょう。
関数型プログラミングの知識を深め、実践に活かしていきましょう!
参考: https://buildflutter.com/functional-programming-with-flutter/