Fungsi

Fungsi #

Fungsi adalah unit terkecil dari abstraksi yang bisa diberi nama, dipanggil ulang, dan dikomposisikan. Di Dart, fungsi bukan sekadar “prosedur yang dibungkus” — fungsi adalah first-class citizen: bisa disimpan di variabel, dijadikan parameter, dikembalikan sebagai nilai, dan disusun menjadi fungsi yang lebih kompleks. Kemampuan ini membuka gaya pemrograman fungsional yang sangat ekspresif, di samping gaya imperatif dan berorientasi objek yang sudah familiar. Artikel ini membahas semua aspek fungsi Dart dari yang paling dasar hingga generator asinkron, dengan fokus pada kapan dan mengapa memilih satu pendekatan atas yang lain.

Anatomi Fungsi #

Setiap fungsi di Dart terdiri dari beberapa bagian: tipe kembalian, nama, daftar parameter, dan body. Semuanya punya aturan dan implikasi tersendiri.

// Bentuk paling lengkap
ReturnType namaFungsi(TipeParam param1, TipeParam param2) {
  // body
  return nilaiYangDikembalikan;
}

// void — tidak mengembalikan nilai
void cetakSalam(String nama) {
  print('Halo, $nama!');
}

// Mengembalikan nilai dengan tipe eksplisit
int tambah(int a, int b) {
  return a + b;
}

// Arrow function — untuk body satu ekspresi
int kali(int a, int b) => a * b;

// Top-level function — didefinisikan langsung di level file
// bisa dipanggil dari mana saja dalam file yang sama atau setelah di-import
double hitungPpn(double harga) => harga * 0.11;

Dart menyimpulkan tipe kembalian fungsi dalam beberapa kasus, tapi sebaiknya selalu tuliskan secara eksplisit untuk API publik — ini dokumentasi yang tidak bisa usang:

// ANTI-PATTERN: tipe kembalian tidak eksplisit untuk fungsi publik
hitungDiskon(double harga) {   // ✗ tipe kembalian disimpulkan — tidak jelas
  return harga * 0.1;
}

// BENAR: tipe kembalian eksplisit
double hitungDiskon(double harga) {  // ✓ jelas kontraknya
  return harga * 0.1;
}

Parameter: Empat Jenis dan Kapan Menggunakannya #

Dart mendukung empat jenis parameter yang bisa dikombinasikan dalam satu fungsi. Pemilihan jenis parameter yang tepat sangat mempengaruhi keterbacaan pemanggilan fungsi.

Positional Parameter (Wajib, Berurutan) #

Parameter yang harus diisi, dan urutannya menentukan nilainya. Cocok untuk fungsi dengan satu atau dua parameter yang maknanya sudah jelas dari posisinya.

// Dua parameter — maknanya jelas dari urutan
double bagi(double pembilang, double penyebut) {
  if (penyebut == 0) throw ArgumentError('Penyebut tidak boleh nol');
  return pembilang / penyebut;
}

print(bagi(10, 2));  // 5.0 — jelas: 10 dibagi 2
// ANTI-PATTERN: terlalu banyak positional parameter
void buatPengguna(String nama, String email, int umur, String kota,
    String negara, bool aktif, String peran) {
  // Pemanggilan: buatPengguna('Budi', '[email protected]', 25, 'Jakarta', 'ID', true, 'admin')
  // ✗ urutan sulit diingat, argumen mudah tertukar
}

// BENAR: gunakan named parameter untuk fungsi dengan banyak parameter
void buatPengguna({
  required String nama,
  required String email,
  required int umur,
  String kota = '',
  String negara = 'ID',
  bool aktif = true,
  String peran = 'user',
}) { ... }

// Pemanggilan jadi self-documenting
buatPengguna(nama: 'Budi', email: '[email protected]', umur: 25, peran: 'admin');

Optional Positional Parameter ([]) #

Parameter yang boleh tidak diisi, diakses berdasarkan posisi, dan nilainya null atau default jika tidak diisi. Jarang digunakan karena kurang ekspresif dibanding named parameter.

// Optional positional — hanya cocok untuk 1 parameter opsional
String formatNama(String depan, [String? belakang]) {
  return belakang != null ? '$depan $belakang' : depan;
}

print(formatNama('Budi'));           // 'Budi'
print(formatNama('Budi', 'Santoso')); // 'Budi Santoso'
// ANTI-PATTERN: beberapa optional positional yang membingungkan
void konfigurasikan(String host, [int? port, bool? ssl, int? timeout]) {
  // Pemanggilan: konfigurasikan('localhost', null, true, 30)
  // ✗ harus isi null untuk port agar bisa mengisi ssl — membingungkan
}

// BENAR: gunakan named parameter
void konfigurasikan(String host, {int port = 80, bool ssl = false, int timeout = 30}) { }
// Pemanggilan: konfigurasikan('localhost', ssl: true, timeout: 30)

Named Parameter ({}) #

Parameter yang diidentifikasi dengan nama saat pemanggilan — urutan tidak penting. Ini adalah jenis parameter yang paling direkomendasikan untuk fungsi dengan lebih dari dua parameter.

// Named parameter — semua dengan default value
void animasiWidget({
  required Widget child,
  Duration durasi = const Duration(milliseconds: 300),
  Curve kurva = Curves.easeInOut,
  bool putar = false,
  VoidCallback? selesai,
}) {
  // implementasi
}

// Pemanggilan — setiap argumen jelas tujuannya
animasiWidget(
  child: const Text('Halo'),
  durasi: Duration(milliseconds: 500),
  selesai: () => print('Animasi selesai'),
);

required — Named Parameter Wajib #

Tanpa required, named parameter selalu opsional. Tambahkan required untuk memaksa pemanggil menyediakan nilai:

// Tanpa required — semua opsional, mungkin lupa diisi
void buatOrder({String? idPengguna, List<Item>? items}) {
  // Bisa dipanggil tanpa argumen — tidak ada jaminan data lengkap
}

// Dengan required — compiler memaksa pengisian
void buatOrder({
  required String idPengguna,    // wajib diisi
  required List<Item> items,     // wajib diisi
  String? catatanKhusus,         // opsional
  bool ekspres = false,          // opsional dengan default
}) {
  // Di sini idPengguna dan items pasti tidak null
}

Kombinasi Parameter #

Positional dan named parameter bisa dikombinasikan, tapi positional harus selalu di depan:

// Positional wajib diikuti named opsional
String kirimPesan(
  String penerima,          // positional wajib
  String isi,               // positional wajib
  {
    bool prioritas = false, // named opsional
    DateTime? jadwal,       // named opsional nullable
  }
) {
  final prefix = prioritas ? '[PRIORITAS] ' : '';
  return '$prefix$penerima: $isi';
}

kirimPesan('Budi', 'Halo!');
kirimPesan('Siti', 'Penting!', prioritas: true);

Arrow Function #

Arrow function (=>) adalah singkatan dari fungsi yang body-nya hanya satu ekspresi. => ekspresi setara dengan { return ekspresi; }.

// Fungsi biasa
int kuadrat(int n) {
  return n * n;
}

// Arrow function — ekuivalen, lebih ringkas
int kuadrat(int n) => n * n;

// Sangat umum sebagai getter dan operator
class Lingkaran {
  final double r;
  const Lingkaran(this.r);

  double get luas => 3.14159 * r * r;           // getter arrow
  double get keliling => 2 * 3.14159 * r;        // getter arrow

  bool operator >(Lingkaran lain) => r > lain.r; // operator arrow
}
// ANTI-PATTERN: arrow function untuk logika yang lebih dari satu ekspresi
String kategori(int umur) => umur < 18
    ? 'minor'
    : umur < 60
    ? 'dewasa'    // ✗ nested ternary dalam arrow — sulit dibaca
    : 'lansia';

// BENAR: gunakan body biasa untuk logika yang lebih kompleks
String kategori(int umur) {
  if (umur < 18) return 'minor';
  if (umur < 60) return 'dewasa';
  return 'lansia';
}

Fungsi sebagai First-Class Citizen #

Di Dart, fungsi adalah objek bertipe Function. Ini berarti fungsi bisa disimpan di variabel, dioperasikan seperti nilai, dan dijadikan parameter atau nilai kembalian fungsi lain.

Menyimpan Fungsi di Variabel #

// Tipe inferensi — var menyimpan referensi ke fungsi
var tambah = (int a, int b) => a + b;
print(tambah(3, 4)); // 7

// Tipe eksplisit menggunakan Function type
int Function(int, int) operasi = (a, b) => a * b;
print(operasi(3, 4)); // 12

// Ganti implementasi dengan tipe yang sama
operasi = (a, b) => a + b;
print(operasi(3, 4)); // 7

Fungsi sebagai Parameter (Higher-Order Function) #

Fungsi yang menerima fungsi lain sebagai parameter disebut higher-order function. Ini adalah fondasi dari map, where, sort, dan hampir semua method koleksi Dart:

// Higher-order function sederhana
List<T> filter<T>(List<T> list, bool Function(T) kondisi) {
  return list.where(kondisi).toList();
}

List<int> angka = [1, 2, 3, 4, 5, 6];
List<int> genap = filter(angka, (n) => n.isEven);     // [2, 4, 6]
List<int> besar = filter(angka, (n) => n > 3);        // [4, 5, 6]

// Fungsi yang menerima callback dengan signature spesifik
void jalankanDenganLog(String nama, void Function() aksi) {
  print('Memulai: $nama');
  final mulai = DateTime.now();
  aksi();
  final durasi = DateTime.now().difference(mulai);
  print('Selesai: $nama (${durasi.inMilliseconds}ms)');
}

jalankanDenganLog('Import data', () {
  importData();  // aksi yang akan di-log
});

Fungsi sebagai Nilai Kembalian #

// Fungsi yang mengembalikan fungsi — factory untuk membuat operasi
int Function(int) pembuat Penambah(int tambahan) {
  return (int n) => n + tambahan;
}

final tambah10 = pembuatPenambah(10);
final tambah100 = pembuatPenambah(100);

print(tambah10(5));   // 15
print(tambah100(5));  // 105
print(tambah10(tambah100(1))); // 111 — komposisi

// Aplikasi nyata: middleware/pipeline
typedef Middleware = String Function(String);

Middleware buatLogger(String prefix) {
  return (String pesan) {
    print('[$prefix] $pesan');
    return pesan; // teruskan pesan ke middleware berikutnya
  };
}

Middleware buatSanitizer() {
  return (String pesan) => pesan.trim().replaceAll('<', '&lt;');
}

typedef — Nama untuk Tipe Fungsi #

typedef mendefinisikan alias untuk tipe fungsi, membuat kode lebih terbaca dan tipe fungsi bisa digunakan berulang tanpa menulis ulang signature lengkapnya:

// Tanpa typedef — verbose dan sulit dibaca
void prosesData(List<String> data, bool Function(String) validator,
    String Function(String) transformer, void Function(String) output) { }

// Dengan typedef — bersih dan ekspresif
typedef Validator<T> = bool Function(T nilai);
typedef Transformer<T, R> = R Function(T nilai);
typedef Consumer<T> = void Function(T nilai);

void prosesData(
  List<String> data,
  Validator<String> validator,
  Transformer<String, String> transformer,
  Consumer<String> output,
) {
  for (final item in data) {
    if (validator(item)) {
      output(transformer(item));
    }
  }
}

// Penggunaan
prosesData(
  ['  Halo  ', '', '  Dart  '],
  (s) => s.trim().isNotEmpty,         // validator
  (s) => s.trim().toUpperCase(),      // transformer
  print,                              // output — referensi fungsi langsung
);
// Output: HALO
//         DART

typedef juga berguna untuk mendefinisikan callback yang digunakan di banyak tempat:

// Pola umum di Flutter
typedef VoidCallback = void Function();
typedef ValueChanged<T> = void Function(T value);
typedef AsyncCallback = Future<void> Function();

// Kelas yang menggunakan typedef sebagai properti
class Tombol {
  final String label;
  final VoidCallback? onTap;
  final ValueChanged<bool>? onHover;

  const Tombol({required this.label, this.onTap, this.onHover});
}

Closure — Menangkap State dari Scope Luar #

Closure adalah fungsi yang “mengingat” variabel dari scope tempat ia didefinisikan, bahkan setelah scope itu sudah selesai dieksekusi. Setiap closure memiliki salinan state-nya sendiri.

// Contoh dasar closure
Function buatPenghitung(int mulaiDari) {
  int hitung = mulaiDari;         // variabel yang di-capture
  return () {
    return hitung++;              // mengakses dan memodifikasi variabel luar
  };
}

final hitungA = buatPenghitung(0);
final hitungB = buatPenghitung(10);

print(hitungA()); // 0 — state A sendiri
print(hitungA()); // 1
print(hitungA()); // 2
print(hitungB()); // 10 — state B independen dari A
print(hitungA()); // 3 — A terus dari sebelumnya

Closure untuk Memoization #

Closure sangat berguna untuk memoization — caching hasil fungsi berdasarkan inputnya:

// Fungsi memoize generik menggunakan closure
Map<K, V> Function(K) memoize<K, V>(V Function(K) fungsi) {
  final cache = <K, V>{};
  return (K input) {
    return cache.putIfAbsent(input, () => fungsi(input));
  };
}

// Fibonacci tanpa memoization — O(2^n) sangat lambat
int fibNaif(int n) => n <= 1 ? n : fibNaif(n - 1) + fibNaif(n - 2);

// Fibonacci dengan memoization via closure — O(n)
final fibMemo = memoize<int, int>((n) {
  if (n <= 1) return n;
  // Rekursi menggunakan fungsi yang sama — tidak bisa langsung dengan memoize
  return fibNaif(n - 1) + fibNaif(n - 2); // ini hanya ilustrasi
});

Jebakan Closure dalam Loop #

// ANTI-PATTERN: closure dalam loop yang menangkap variabel loop
List<Function> fungsi = [];
for (int i = 0; i < 3; i++) {
  fungsi.add(() => print(i)); // ✗ semua closure menangkap REFERENSI ke i
}
// Saat dipanggil setelah loop, i sudah = 3
fungsi[0](); // 3 — bukan 0!
fungsi[1](); // 3 — bukan 1!
fungsi[2](); // 3 — bukan 2!

// BENAR: tangkap nilai saat iterasi dengan variabel lokal
List<Function> fungsi = [];
for (int i = 0; i < 3; i++) {
  final nilaiI = i;             // salin nilai ke variabel baru
  fungsi.add(() => print(nilaiI)); // ✓ menangkap nilai, bukan referensi
}
fungsi[0](); // 0 ✓
fungsi[1](); // 1 ✓
fungsi[2](); // 2 ✓

// Atau lebih idiomatis dengan List.generate
List<Function> fungsi = List.generate(3, (i) => () => print(i));

Rekursi #

Rekursi adalah teknik di mana fungsi memanggil dirinya sendiri untuk memecah masalah menjadi sub-masalah yang lebih kecil. Setiap fungsi rekursif membutuhkan base case (kondisi berhenti) untuk mencegah infinite recursion.

// Faktorial — contoh klasik
int faktorial(int n) {
  if (n <= 0) throw ArgumentError('n harus positif');
  if (n == 1) return 1;   // base case
  return n * faktorial(n - 1); // recursive case
}

// Fibonacci — dua pemanggilan rekursif
int fibonacci(int n) {
  if (n < 0) throw ArgumentError('n harus >= 0');
  if (n <= 1) return n;   // base case
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Binary search — rekursi pada struktur data
int? binarySearch(List<int> list, int target, [int? lo, int? hi]) {
  lo ??= 0;
  hi ??= list.length - 1;

  if (lo > hi) return null;                    // base case: tidak ditemukan
  final mid = lo + (hi - lo) ~/ 2;
  if (list[mid] == target) return mid;         // base case: ditemukan
  if (list[mid] < target) return binarySearch(list, target, mid + 1, hi);
  return binarySearch(list, target, lo, mid - 1);
}

Rekursi vs Iterasi #

// Rekursi — elegan tapi punya overhead stack call
int jumlahRekursif(List<int> list) {
  if (list.isEmpty) return 0;
  return list.first + jumlahRekursif(list.sublist(1));
  // Untuk list 10.000 elemen → 10.000 stack frames → Stack Overflow!
}

// Iterasi — lebih aman untuk data besar
int jumlahIteratif(List<int> list) {
  return list.fold(0, (acc, n) => acc + n); // ✓ aman untuk data besar
}
// ANTI-PATTERN: rekursi tanpa base case yang jelas
int hitungMundur(int n) {
  print(n);
  return hitungMundur(n - 1); // ✗ tidak ada base case — Stack Overflow!
}

// BENAR: selalu ada base case yang pasti tercapai
int hitungMundur(int n) {
  if (n <= 0) return 0;     // base case
  print(n);
  return hitungMundur(n - 1);
}

Generator: sync* dan async* #

Generator adalah fungsi khusus yang menghasilkan urutan nilai secara lazy — nilai hanya dihitung saat dibutuhkan, bukan semuanya sekaligus. Dart mendukung dua jenis generator.

Sinkron (sync* + yield) #

Generator sinkron menghasilkan Iterable — urutan nilai yang bisa diiterasi satu per satu:

// Generator sinkron — menghasilkan Iterable<int>
Iterable<int> range(int dari, int ke, {int langkah = 1}) sync* {
  for (int i = dari; i < ke; i += langkah) {
    yield i; // "kirim" satu nilai, lalu pause
  }
}

// Penggunaan
for (final n in range(0, 10)) print(n);        // 0 1 2 3 4 5 6 7 8 9
for (final n in range(0, 20, langkah: 2)) print(n); // 0 2 4 6 8 10 12 14 16 18

// Generator rekursif untuk traversal tree
Iterable<int> preorder(TreeNode? node) sync* {
  if (node == null) return;
  yield node.nilai;                // root dulu
  yield* preorder(node.kiri);     // yield* meneruskan semua nilai dari Iterable lain
  yield* preorder(node.kanan);
}

Asinkron (async* + yield) #

Generator asinkron menghasilkan Stream — urutan event asinkron yang dikirimkan seiring waktu:

// Generator asinkron — menghasilkan Stream<String>
Stream<String> bacaBarisFile(String path) async* {
  final file = File(path);
  final baris = file.openRead()
      .transform(utf8.decoder)
      .transform(LineSplitter());

  await for (final baris in baris) {
    yield baris; // kirim satu baris, lalu tunggu consumer siap
  }
}

// Stream dengan delay — polling, websocket, sensor
Stream<int> sensorSuhu() async* {
  while (true) {
    await Future.delayed(Duration(seconds: 1));
    yield bacaSuhuDariSensor(); // kirim satu pembacaan per detik
  }
}

// Consumer
await for (final suhu in sensorSuhu().take(10)) {
  print('Suhu: ${suhu}°C');
}

yield* — Delegasi ke Generator Lain #

yield* (yield-star) mendelegasikan semua nilai dari Iterable atau Stream lain tanpa perlu loop manual:

// Tanpa yield* — verbose
Iterable<int> gabung(List<Iterable<int>> semua) sync* {
  for (final iterable in semua) {
    for (final item in iterable) {
      yield item;
    }
  }
}

// Dengan yield* — ringkas
Iterable<int> gabung(List<Iterable<int>> semua) sync* {
  for (final iterable in semua) {
    yield* iterable; // delegasikan semua nilai dari iterable ini
  }
}

Fungsi Asinkron: async / await #

Fungsi yang melakukan operasi asinkron (I/O, HTTP, database) dideklarasikan dengan async dan mengembalikan Future<T>:

// Fungsi async — mengembalikan Future<String>
Future<String> ambilNamaPengguna(String id) async {
  final response = await http.get(Uri.parse('/api/users/$id'));
  if (response.statusCode != 200) {
    throw HttpException('Gagal: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  return json['nama'] as String;
}

// Pemanggilan
Future<void> main() async {
  try {
    final nama = await ambilNamaPengguna('U001');
    print('Halo, $nama!');
  } on HttpException catch (e) {
    print('Error: $e');
  }
}
// ANTI-PATTERN: async tanpa await — mengembalikan Future tanpa menunggu
Future<void> simpanData(Data data) async {
  database.insert(data); // ✗ tidak di-await — operasi mungkin belum selesai
  print('Tersimpan'); // mencetak sebelum insert benar-benar selesai
}

// BENAR: await semua operasi async
Future<void> simpanData(Data data) async {
  await database.insert(data); // ✓ tunggu sampai selesai
  print('Tersimpan');
}

Function Composition #

Komposisi fungsi adalah teknik menggabungkan beberapa fungsi kecil menjadi pipeline transformasi data. Dart tidak punya operator komposisi bawaan seperti |> di beberapa bahasa, tapi pola ini mudah diimplementasikan:

// Tiga fungsi kecil dengan tanggung jawab tunggal
String trim(String s) => s.trim();
String uppercase(String s) => s.toUpperCase();
String tambahPrefix(String s) => 'DART: $s';

// Komposisi manual — nested call
String hasil = tambahPrefix(uppercase(trim('  halo dunia  ')));
// 'DART: HALO DUNIA'

// Komposisi dengan extension method yang lebih elegan
extension StringPipeline on String {
  String pipe(String Function(String) fungsi) => fungsi(this);
}

String hasil = '  halo dunia  '
    .pipe(trim)
    .pipe(uppercase)
    .pipe(tambahPrefix);
// 'DART: HALO DUNIA'

// Compose function — membuat fungsi gabungan
T Function(T) compose<T>(List<T Function(T)> fungsiList) {
  return (T input) => fungsiList.fold(input, (acc, f) => f(acc));
}

final prosesTeks = compose<String>([trim, uppercase, tambahPrefix]);
print(prosesTeks('  dart itu keren  ')); // 'DART: DART ITU KEREN'

Ringkasan #

  • Tipe kembalian selalu eksplisit untuk fungsi publik — ini dokumentasi kontrak yang tidak bisa usang seperti komentar.
  • Named parameter ({}) lebih disukai untuk fungsi dengan lebih dari dua parameter — pemanggilan jadi self-documenting dan urutannya tidak penting.
  • required memaksa pemanggil mengisi named parameter — gunakan untuk data yang benar-benar wajib, hindari menjadikan semua parameter nullable sebagai ganti.
  • Arrow function (=>) untuk body satu ekspresi, body biasa untuk logika lebih dari satu langkah. Jangan paksa logika kompleks ke dalam satu ekspresi ternary bersarang.
  • typedef memberikan nama bermakna untuk tipe fungsi yang digunakan berulang — membuat signature fungsi higher-order jauh lebih terbaca.
  • Closure menangkap referensi, bukan nilai — dalam loop, buat variabel lokal salinan nilai loop untuk menghindari semua closure menunjuk ke nilai akhir.
  • Rekursi elegan untuk masalah yang secara alami bersifat rekursif (tree traversal, divide and conquer), tapi tidak aman untuk data besar karena stack depth. Gunakan iterasi atau fold untuk agregasi sederhana.
  • sync* + yield menghasilkan Iterable yang dievaluasi secara lazy — ideal untuk sequence yang berpotensi besar atau infinite. async* + yield menghasilkan Stream untuk sequence event asinkron.
  • yield* mendelegasikan semua nilai dari Iterable/Stream lain — gunakan ini dalam generator rekursif daripada loop manual.
  • Fungsi asinkron harus await semua operasi async di dalam body-nya — lupa await menyebabkan kode berlanjut sebelum operasi selesai, sumber bug yang sulit dilacak.

← Sebelumnya: Perulangan   Berikutnya: Kelas →

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