Fungsi orde tinggi

map, filter, dan fold dari modul lists serta fungsi anonim sebagai argumen.

Daftar isi

Topik ini memperkenalkan fungsi orde tinggi (higher-order function), yaitu fungsi yang menerima fungsi lain sebagai argumen: nilai fun beserta dua bentuknya, trio lists:map, lists:filter, dan lists:foldl sebagai tulang punggung modul lists, serta closure yang menangkap variabel luar. Semua contoh dieksekusi nyata di shell OTP 29.

Nilai fun dan dua bentuknya#

Fungsi pada Erlang adalah nilai biasa yang dapat diikat ke variabel, dikirim sebagai argumen, dan dipanggil. Nilai itu dibuat dengan ekspresi fun:

erlang Fungsi anonim dan referensi fungsi modul
Erlang/OTP 29 [erts-17.1.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit:ns]

Eshell V17.1.1 (press Ctrl+G to abort, type help(). for help)
1> Kuadrat = fun(X) -> X * X end.
#Fun<erl_eval.42.130099583>
2> Kuadrat(5).
25
3> F = fun lists:sum/1.
fun lists:sum/1
4> F([1, 2, 3]).
6

Bentuk pertama adalah fungsi anonim (anonymous function) yang terikat ke Kuadrat, dan bentuk kedua adalah referensi fun Modul:Fungsi/Aritas yang merangkai fungsi yang sudah ada menjadi nilai. Perhatikan bahwa tubuh fun ditutup dengan end, bukan titik, dan variabel di dalamnya mengikuti aturan pengikatan sekali yang sama. Nilai fun inilah bahan baku semua fungsi orde tinggi.

map, filter, dan foldl#

Tiga fungsi modul lists mencakup sebagian besar kebutuhan pengolahan list. map/2 menerapkan satu fungsi pada setiap elemen, filter/2 menyisakan elemen yang lolos uji, dan foldl/3 meringkas seluruh list menjadi satu nilai dengan membawa akumulasi:

erlang Trio pengolahan list
5> lists:map(fun(X) -> X * 2 end, [1, 2, 3]).
[2,4,6]
6> lists:filter(fun(N) -> N rem 2 =:= 0 end, lists:seq(1, 10)).
[2,4,6,8,10]
7> lists:foldl(fun(X, Acc) -> X + Acc end, 0, [1, 2, 3, 4]).
10
8> lists:foldl(fun(X, Acc) -> X * Acc end, 1, [1, 2, 3, 4]).
24

Fungsi foldl/3 menerima fungsi dua argumen: elemen saat ini dan akumulasi. Ekspresi ke-7 di atas memulai dari 0 lalu menjumlahkan seluruh elemen, sedangkan ekspresi ke-8 memulai dari 1 untuk perkalian, dan inilah alasan nilai awalnya berbeda: 0 adalah unsur identitas penjumlahan, 1 adalah unsur identitas perkalian. Ketiganya membungkus pola rekursi dengan akumulator yang ditulis manual pada topik akumulator dan tail call, dan eksekusinya mengikuti pola yang sama pula.

Untuk pemrosesan dari belakang, foldr/3 tersedia dengan urutan yang berlawanan. Perbedaannya terlihat pada operasi yang tidak bersifat asosiatif. Pengurangan dengan foldl/3 menghitung dari kiri, sedangkan foldr/3 dari kanan, dan hasilnya berbeda pada data yang sama:

erlang Arah pemrosesan foldl dan foldr
1> lists:foldl(fun(X, Acc) -> X - Acc end, 0, lists:seq(1, 4)).
2
2> lists:foldr(fun(X, Acc) -> X - Acc end, 0, lists:seq(1, 4)).
-2

foldl/3 merangkai dari kiri sehingga bentuk bersarangnya adalah 4 - (3 - (2 - (1 - 0))), sedangkan foldr/3 merangkai dari kanan dengan bentuk 1 - (2 - (3 - (4 - 0))), dan kedua urutan itu menghasilkan 2 dan -2. Untuk penjumlahan dan perkalian hasilnya selalu sama karena kedua operasi itu asosiatif, sehingga pilihan arah biasanya ditentukan oleh bentuk pembangunan strukturnya, bukan oleh hasil angkanya.

Closure: fungsi yang membawa lingkungannya#

Fungsi anonim mengikat variabel yang terlihat pada saat ia dibuat, dan ikatan itu ikut bepergian bersama fungsinya. Sifat ini disebut closure:

erlang Fungsi anonim menangkap variabel luar
9> Pengali = 3.
3
10> Kalikan = fun(X) -> X * Pengali end.
#Fun<erl_eval.42.130099583>
11> Kalikan(4).
12

Fungsi Kalikan menangkap nilai Pengali pada saat didefinisikan, dan pemanggilannya kemudian tetap memakai nilai tangkapan itu. Satu aturan penting berlaku: variabel luar hanya bisa dibaca dari dalam fun, bukan diikat ulang, karena aturan pengikatan sekali berlaku sama di dalam maupun di luar. Closure membuat perilaku fungsi orde tinggi dapat disesuaikan tanpa argumen tambahan pada setiap pemanggilan.

Untuk efek samping: foreach#

Trio di atas semuanya mengembalikan list atau nilai ringkasan. Bila tujuan utamanya efek samping, misalnya mencetak, lists:foreach/2 menjalankan fungsi pada setiap elemen lalu mengembalikan atom ok. Pemaduan fungsi juga sah: hasil satu fungsi orde tinggi langsung menjadi masukan fungsi yang lain:

erlang foreach dan pemaduan map dengan filter
1> lists:foreach(fun(N) -> io:format("nilai ~p~n", [N]) end, [1, 2]).
nilai 1
nilai 2
ok
2> lists:filter(fun(N) -> N rem 2 =:= 0 end, lists:map(fun(X) -> X + 1 end, [1, 2, 3, 4])).
[2,4]

Ekspresi kedua menambahkan satu pada setiap elemen lalu menyaring yang genap, dan hasilnya [2, 4] sesuai urutan evaluasinya dari dalam ke luar. Untuk alur yang panjang, pola penulisan berlapis semacam ini biasanya digantikan list comprehension agar urutan tahapannya terbaca alami.

Contoh menyeluruh: statistik nilai ujian#

Modul berikut menggabungkan pola-pola di atas untuk satu kebutuhan nyata, yaitu rata-rata nilai ujian:

erlang statistik.erl
-module(statistik).
-export([rerata/1]).

rerata([]) -> 0;
rerata(Nilai) -> lists:sum(Nilai) / length(Nilai).

Modul lists sebenarnya sudah menyediakan lists:sum/1, sehingga fungsi ini cukup menggabungkannya dengan length/1. Hasilnya di shell:

erlang Rata-rata nilai ujian
1> c(statistik).
{ok,statistik}
2> Nilai = [70, 85, 90, 60, 75].
"FUZ<K"
3> statistik:rerata(Nilai).
76.0

Cetakan "FUZ<K" pada baris kedua bukan keanehan data, melainkan kebiasaan shell: list yang seluruh elemennya adalah kode karakter yang dapat dicetak ditampilkan sebagai string, dan [70, 85, 90, 60, 75] kebetulan membaca F, U, Z, <, K. Nilainya tetap list bilangan, dan rerata/1 menghitungnya menjadi 76.0. Perilaku pencetakan ini dibahas ulang pada modul Record dan string.

Karena fungsi hanyalah nilai, fungsi orde tinggi dapat ditulis sendiri tanpa alat tambahan. Modul berikut mendefinisikan versi map sendiri dengan rekursi biasa:

erlang orde.erl
-module(orde).
-export([mapku/2]).

mapku(_F, []) -> [];
mapku(F, [K | S]) -> [F(K) | mapku(F, S)].

Satu-satunya perbedaan dari rekursi list pada topik rekursi sebagai iterasi adalah parameter F yang dipanggil seperti fungsi biasa. Klausa dasarnya mengembalikan list kosong tanpa memanggil F, sehingga list kosong aman:

erlang Memanggil map buatan sendiri
6> c(orde).
{ok,orde}
7> orde:mapku(fun(X) -> X * 10 end, [1, 2]).
[10,20]

Hasilnya identik dengan lists:map/2 untuk kasus ini, dan struktur penerapannya memperlihatkan bahwa map, filter, dan fold hanyalah rekursi list yang membawa fungsi sebagai argumen. Dalam proyek nyata, versi modul lists tetap pilihan utama karena sudah teruji dan punya optimasi tersendiri.

Ringkasan#

  • Fungsi adalah nilai: dibuat dengan fun anonim atau dirujuk dengan fun Modul:Fungsi/Aritas, dan keduanya dipanggil seperti fungsi biasa.
  • lists:map/2, lists:filter/2, dan lists:foldl/3 menutupi kebutuhan ubah, saring, dan ringkas, dengan foldl membawa akumulasi dan nilai awal.
  • Nilai awal fold dipilih dari unsur identitas operasinya, misalnya 0 untuk penjumlahan dan 1 untuk perkalian.
  • Fungsi anonim menangkap variabel luarnya sebagai closure, tetapi variabel tersebut hanya dapat dibaca, tidak diikat ulang.
  • lists:foreach/2 menjalankan efek samping per elemen dan mengembalikan ok, sedangkan pemaduan fungsi orde tinggi membentuk alur dari dalam ke luar.
  • Fungsi orde tinggi bisa ditulis sendiri dengan rekursi list biasa yang membawa fungsi sebagai argumen.

Referensi#