Functional Programming #
Dart adalah bahasa multi-paradigma — ia mendukung OOP sekaligus functional programming (FP). Kamu tidak harus memilih salah satu. Justru Flutter sendiri dibangun dengan banyak konsep FP: widget adalah fungsi murni dari state, build() tidak boleh punya side effect, dan data mengalir satu arah. Memahami FP di Dart berarti memahami mengapa Flutter bekerja seperti yang ia lakukan.
Apa itu Functional Programming? #
Functional programming adalah paradigma yang memperlakukan komputasi sebagai evaluasi fungsi matematika. Dua prinsip utamanya:
OOP (fokus pada): FP (fokus pada):
Objek dan state Fungsi dan transformasi data
Behavior melalui method Komposisi fungsi
State yang berubah Immutability
Hierarki class Fungsi sebagai nilai
Di Dart, FP bukan pengganti OOP — keduanya digunakan bersama. Gunakan class untuk memodelkan domain, gunakan prinsip FP untuk menulis logika yang bersih dan mudah diuji.
First-Class Functions #
Di Dart, fungsi adalah first-class citizen — fungsi bisa diperlakukan sama seperti nilai lainnya: disimpan dalam variabel, dikirim sebagai argumen, dan dikembalikan dari fungsi lain.
// Fungsi sebagai variabel
int Function(int, int) tambah = (a, b) => a + b;
int Function(int, int) kali = (a, b) => a * b;
print(tambah(3, 4)); // 7
print(kali(3, 4)); // 12
// Simpan dalam List
final operasi = <String, int Function(int, int)>{
'tambah': (a, b) => a + b,
'kurang': (a, b) => a - b,
'kali': (a, b) => a * b,
};
print(operasi['kali']!(5, 6)); // 30
// Tipe fungsi yang lebih kompleks
typedef Predicate<T> = bool Function(T);
typedef Transformer<A, B> = B Function(A);
typedef Reducer<T> = T Function(T, T);
Pure Functions — Fungsi Tanpa Efek Samping #
Pure function adalah fungsi yang:
- Selalu mengembalikan output yang sama untuk input yang sama
- Tidak mengubah state di luar dirinya sendiri (tidak ada side effect)
// PURE FUNCTION -- output hanya bergantung pada input
int tambah(int a, int b) => a + b;
String format(double nilai) => 'Rp ${nilai.toStringAsFixed(0)}';
List<T> filter<T>(List<T> list, bool Function(T) predicate) =>
list.where(predicate).toList();
// IMPURE FUNCTION -- bergantung pada state luar
int _counter = 0;
int increment() => ++_counter; // bergantung dan mengubah state global!
DateTime sekarang() => DateTime.now(); // output berubah setiap dipanggil
// Kenapa pure function lebih baik?
// 1. Mudah diuji -- tidak perlu mock state eksternal
// 2. Mudah di-debug -- output predictable
// 3. Aman untuk parallelisme -- tidak ada shared state
// 4. Bisa di-memoize -- hasil bisa di-cache berdasarkan input
void main() {
// Test pure function sangat mudah
assert(tambah(2, 3) == 5);
assert(tambah(2, 3) == 5); // selalu sama
assert(tambah(2, 3) == 5); // tidak peduli berapa kali dipanggil
}
Memisahkan Pure Logic dari Side Effects #
Kunci FP praktis adalah memisahkan logika murni (pure) dari efek samping (I/O, UI, network):
// PURE -- logika bisnis, mudah diuji
double hitungTotal(List<CartItem> items) =>
items.fold(0, (sum, item) => sum + item.harga * item.jumlah);
double hitungDiskon(double total, String kodePromo) {
return switch (kodePromo) {
'HEMAT10' => total * 0.1,
'HEMAT20' => total * 0.2,
_ => 0,
};
}
double hitungOngkir(double total, String kota) =>
total >= 200000 ? 0 : kota == 'Jakarta' ? 15000 : 25000;
// IMPURE -- side effects (UI, network, storage)
Future<void> checkout(List<CartItem> items, String promo, String kota) async {
// Panggil pure functions untuk kalkulasi
final total = hitungTotal(items);
final diskon = hitungDiskon(total, promo);
final ongkir = hitungOngkir(total - diskon, kota);
final bayar = total - diskon + ongkir;
// Side effects di sini -- isolated dari logika bisnis
await apiClient.createOrder(items, bayar);
await analytics.logPurchase(bayar);
showSuccessDialog(bayar);
}
Immutability — Data yang Tidak Berubah #
Immutability berarti setelah sebuah nilai dibuat, ia tidak bisa diubah. Sebagai gantinya, kamu membuat nilai baru berdasarkan nilai lama.
// Immutable class dengan copyWith pattern
class User {
final String id;
final String nama;
final String email;
final bool aktif;
const User({
required this.id,
required this.nama,
required this.email,
this.aktif = true,
});
// copyWith -- buat User baru dengan beberapa field berubah
User copyWith({
String? id,
String? nama,
String? email,
bool? aktif,
}) {
return User(
id: id ?? this.id,
nama: nama ?? this.nama,
email: email ?? this.email,
aktif: aktif ?? this.aktif,
);
}
@override
bool operator ==(Object other) =>
other is User && id == other.id && nama == other.nama &&
email == other.email && aktif == other.aktif;
@override
int get hashCode => Object.hash(id, nama, email, aktif);
@override
String toString() => 'User($nama, $email, aktif=$aktif)';
}
void main() {
final user = User(id: '1', nama: 'Budi', email: '[email protected]');
// SALAH -- mengubah user yang sudah ada (mutable state)
// user.nama = 'Sari'; // ERROR karena final!
// BENAR -- buat user baru dengan field yang berubah
final userBaru = user.copyWith(nama: 'Sari', aktif: false);
print(user); // User(Budi, [email protected], aktif=true)
print(userBaru); // User(Sari, [email protected], aktif=false)
print(user == userBaru); // false -- user asli tidak berubah
}
Immutable Collections #
// List tidak bisa dimodifikasi
final immutableList = List.unmodifiable([1, 2, 3]);
// immutableList.add(4); // throw UnsupportedError!
// Cara membuat list baru berdasarkan yang lama
final list = [1, 2, 3];
final listBaru = [...list, 4]; // [1, 2, 3, 4]
final listTanpa2 = list.where((n) => n != 2).toList(); // [1, 3]
final listDiubah = list.map((n) => n * 2).toList(); // [2, 4, 6]
// Map tidak bisa dimodifikasi
final immutableMap = Map.unmodifiable({'a': 1, 'b': 2});
// Cara update Map secara immutable
final map = {'nama': 'Budi', 'kota': 'Jakarta'};
final mapBaru = {...map, 'kota': 'Bandung'}; // update kota
final mapTambah = {...map, 'umur': 25}; // tambah key baru
Higher-Order Functions #
Higher-order function adalah fungsi yang menerima fungsi lain sebagai argumen, atau mengembalikan fungsi sebagai hasilnya.
Fungsi yang Menerima Fungsi #
// Higher-order function sederhana
List<B> transform<A, B>(List<A> items, B Function(A) f) =>
items.map(f).toList();
bool semua<T>(List<T> items, bool Function(T) predicate) =>
items.every(predicate);
T? cari<T>(List<T> items, bool Function(T) predicate) =>
items.cast<T?>().firstWhere(
(item) => predicate(item!),
orElse: () => null,
);
void main() {
final angka = [1, 2, 3, 4, 5];
// Kirim lambda sebagai argumen
final kuadrat = transform(angka, (n) => n * n);
print(kuadrat); // [1, 4, 9, 16, 25]
// Kirim named function sebagai argumen
bool isGanjil(int n) => n % 2 != 0;
final ganjil = angka.where(isGanjil).toList();
print(ganjil); // [1, 3, 5]
}
Fungsi yang Mengembalikan Fungsi #
// Fungsi yang membuat fungsi lain (function factory)
int Function(int) penambah(int n) => (x) => x + n;
int Function(int) pengali(int n) => (x) => x * n;
void main() {
final tambah5 = penambah(5);
final tambah10 = penambah(10);
final kali3 = pengali(3);
print(tambah5(3)); // 8
print(tambah10(3)); // 13
print(kali3(7)); // 21
// Berguna untuk membuat predicate yang dapat dikonfigurasi
bool Function(int) lebihDari(int batas) => (n) => n > batas;
bool Function(int) kurangDari(int batas) => (n) => n < batas;
final angka = [1, 5, 10, 15, 20];
print(angka.where(lebihDari(8)).toList()); // [10, 15, 20]
print(angka.where(kurangDari(8)).toList()); // [1, 5]
}
Closures — Fungsi yang Menangkap Scope #
Closure adalah fungsi yang “menangkap” variabel dari scope di sekitarnya — bahkan setelah scope tersebut selesai dieksekusi.
// Closure menangkap variabel dari scope luar
int Function() buatCounter() {
int hitung = 0; // variabel lokal
return () {
hitung++; // closure menangkap dan mengubah hitung
return hitung;
};
}
void main() {
final counter1 = buatCounter();
final counter2 = buatCounter(); // state independen!
print(counter1()); // 1
print(counter1()); // 2
print(counter1()); // 3
print(counter2()); // 1 -- counter2 punya state sendiri
print(counter2()); // 2
}
Closure di Flutter #
// Closure sangat umum di Flutter untuk callback
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback onAddToCart; // callback -- closure dari parent
const ProductCard({
super.key,
required this.product,
required this.onAddToCart,
});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Text(product.nama),
ElevatedButton(
onPressed: onAddToCart, // panggil closure
child: const Text('Tambah ke Keranjang'),
),
],
),
);
}
}
// Penggunaan -- closure menangkap product dari scope parent
ListView.builder(
itemBuilder: (context, index) {
final product = products[index];
return ProductCard(
product: product,
onAddToCart: () { // closure -- menangkap product dan cart
cart.add(product);
showSnackBar('${product.nama} ditambahkan!');
},
);
},
)
Function Composition #
Function composition adalah menggabungkan beberapa fungsi kecil menjadi pipeline yang lebih besar. Output satu fungsi menjadi input fungsi berikutnya.
// Composisi manual
String proses(String input) {
final dipotong = input.trim();
final hurufKecil = dipotong.toLowerCase();
final diubah = hurufKecil.replaceAll(' ', '_');
return diubah;
}
// Composisi menggunakan extension method
extension FunctionCompose<A, B> on B Function(A) {
C Function(A) then<C>(C Function(B) next) => (a) => next(this(a));
}
void main() {
final proses = ((String s) => s.trim())
.then((s) => s.toLowerCase())
.then((s) => s.replaceAll(' ', '_'));
print(proses(' Hello World ')); // hello_world
}
// Pipeline menggunakan fold -- sangat fleksibel
T applyAll<T>(T nilai, List<T Function(T)> transformasi) =>
transformasi.fold(nilai, (v, f) => f(v));
void main2() {
final hasil = applyAll(
' Flutter Developer ',
[
(s) => s.trim(),
(s) => s.toLowerCase(),
(s) => s.replaceAll(' ', '-'),
],
);
print(hasil); // flutter-developer
}
Currying dan Partial Application #
Currying mengubah fungsi dengan banyak argumen menjadi rangkaian fungsi yang masing-masing menerima satu argumen. Partial application adalah menerapkan sebagian argumen dan mendapatkan fungsi baru.
// Fungsi biasa dengan 3 argumen
String formatPesan(String template, String nama, String aksi) =>
template.replaceAll('{nama}', nama).replaceAll('{aksi}', aksi);
// Curried version
String Function(String) Function(String) Function(String) formatCurried(
String template,
) =>
(nama) => (aksi) => template
.replaceAll('{nama}', nama)
.replaceAll('{aksi}', aksi);
void main() {
// Partial application -- terapkan template dulu
final templateSelamat = formatCurried('{nama} berhasil {aksi}!');
// Buat fungsi spesifik per nama
final pesanBudi = templateSelamat('Budi');
final pesanSari = templateSelamat('Sari');
print(pesanBudi('login')); // Budi berhasil login!
print(pesanBudi('checkout')); // Budi berhasil checkout!
print(pesanSari('register')); // Sari berhasil register!
}
Partial Application yang Praktis #
// Contoh yang lebih praktis: middleware/interceptor
typedef RequestHandler = Future<Response> Function(Request);
typedef Middleware = RequestHandler Function(RequestHandler);
// Middleware sebagai partial application
Middleware withAuth(String token) =>
(next) => (request) async {
final authed = request.copyWith(
headers: {...request.headers, 'Authorization': 'Bearer $token'},
);
return next(authed);
};
Middleware withLogging(Logger logger) =>
(next) => (request) async {
logger.info('${request.method} ${request.path}');
final response = await next(request);
logger.info('Response: ${response.statusCode}');
return response;
};
// Compose middleware
RequestHandler buildHandler(RequestHandler base) =>
withLogging(logger)(withAuth(token)(base));
Memoization — Cache Hasil Pure Function #
Karena pure function selalu menghasilkan output yang sama untuk input yang sama, hasilnya bisa di-cache (memoize):
// Generic memoize
Map<A, B> Function(A) memoize<A, B>(B Function(A) fn) {
final cache = <A, B>{};
return (A arg) => cache.putIfAbsent(arg, () => fn(arg));
}
// Contoh: memoize fungsi komputasi berat
int Function(int) fibonacci = memoize((n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2); // rekursi di-cache!
});
void main() {
final stopwatch = Stopwatch()..start();
print(fibonacci(40)); // 102334155
print('Pertama: ${stopwatch.elapsedMilliseconds}ms');
stopwatch.reset();
print(fibonacci(40)); // 102334155 -- langsung dari cache!
print('Kedua: ${stopwatch.elapsedMilliseconds}ms'); // ~0ms
}
FP di Flutter — Penerapan Nyata #
Flutter sendiri adalah contoh FP dalam praktik:
// 1. Widget build() adalah pure function dari state
// Output (widget tree) hanya bergantung pada input (props + state)
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override
Widget build(BuildContext context) {
// Pure -- tidak ada side effect, hanya mengembalikan widget
return Card(
child: Column(
children: [
Text(product.nama),
Text('Rp ${product.harga}'),
],
),
);
}
}
// 2. State transformation -- buat state baru, jangan mutasi
class CartViewModel extends ChangeNotifier {
List<CartItem> _items = [];
// SALAH -- mutasi state langsung
// void addItem(Product p) => _items.add(CartItem(p));
// BENAR -- buat list baru (FP style)
void addItem(Product p) {
_items = [..._items, CartItem(product: p, jumlah: 1)];
notifyListeners();
}
void removeItem(String productId) {
_items = _items.where((item) => item.product.id != productId).toList();
notifyListeners();
}
void updateJumlah(String productId, int jumlah) {
_items = _items.map((item) {
return item.product.id == productId
? item.copyWith(jumlah: jumlah)
: item;
}).toList();
notifyListeners();
}
}
// 3. Data pipeline -- transformasi data secara fungsional
List<ProductViewModel> buildViewModels(
List<Product> products,
List<String> favoriteIds,
String query,
SortBy sortBy,
) {
return products
.where((p) => p.nama.toLowerCase().contains(query.toLowerCase()))
.map((p) => ProductViewModel(
product: p,
isFavorite: favoriteIds.contains(p.id),
))
.toList()
..sort((a, b) => switch (sortBy) {
SortBy.harga => a.product.harga.compareTo(b.product.harga),
SortBy.nama => a.product.nama.compareTo(b.product.nama),
SortBy.rating => b.product.rating.compareTo(a.product.rating),
});
}
Ringkasan #
- Dart mendukung FP melalui first-class functions, higher-order functions, closures, dan immutability — tanpa harus meninggalkan OOP.
- Pure functions selalu menghasilkan output yang sama untuk input yang sama dan tidak memiliki side effect — mudah diuji, diprediksi, dan di-cache.
- Immutability berarti membuat nilai baru alih-alih mengubah nilai yang ada. Gunakan
final,const,copyWith, spread operator, danList.unmodifiable.- Higher-order functions menerima atau mengembalikan fungsi — memungkinkan abstraksi perilaku dan pembuatan fungsi baru secara dinamis.
- Closures menangkap variabel dari scope sekitarnya — sangat umum di Flutter untuk callback, listener, dan builder.
- Function composition menggabungkan fungsi kecil menjadi pipeline — buat logika kompleks dari fungsi-fungsi sederhana yang mudah diuji.
- Memoization meng-cache hasil pure function berdasarkan inputnya — efektif untuk komputasi berat yang sering dipanggil dengan argumen yang sama.
- Flutter sendiri adalah contoh FP:
build()adalah pure function, state transformation seharusnya immutable, dan data pipeline menggunakanmap/where/reduce.