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:

  1. Selalu mengembalikan output yang sama untuk input yang sama
  2. 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, dan List.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 menggunakan map/where/reduce.

← Sebelumnya: OOP di Dart   Berikutnya: Widget Overview →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact