Indonesian (Bahasa Indonesia) translation by Husain Ali Yahya (you can also view the original English article)
Elixir adalah sebuah bahasa pemograman yang masih sangat muda (muncul pada tahun 2011), tapi dia sekarang sedang populer. Pada awalnya saya teretarik dengan bahasa ini karena saat menggunakannya kamu bisa menyelesaikan beberapa tugas programmer dari sudut pandang yang berbeda. Contohnya, kamu bisa melakukan iterasi tanpa menggunakan siklus for
atau mengorganisasikan kode-mu tanpa classes.
Elixir memiliki beberapa fitur yang sangat bertenaga dan menarik yang bisa jadi sulit dipahami jika kamu berasal dari dunia OOP. Namun, setelah beberapa saat, semuanya mulai terasa logis dan kamu bisa melihat betapa expresif kode-nya. Comprehensions adalah salah satunya dan pada artikel ini saya akan menjelaskan cara menggunakannya.
Comprehensions dan Mapping
Pada dasarnya, sebuah list comprehension adalah konstruksi spesial yang mengizinkanmu untuk buat sebuah daftar baru di atas yang sudah ada, Konsep ini ditemukan pada bahasa-bahasa seperti Haskell dan Clojure. Erlang juga memilikinya, makanya, Elixir memiliki comprehensions juga.
Kamu mungkin bertanya-tanya perbedaan comprehensions dengan fungsi map/2 yang juga mengambil dan memproduksi koleksi yang baru? Itu bisa jadi pertanyaan yang cukup bagus! Baik, pada kasus paling sederhana, comprehension melakukan hal yang sama. Perhatikan contoh berikut:
defmodule MyModule do def do_something(list) do list |> Enum.map(fn(el) -> el * 2 end) end end MyModule.do_something([1,2,3]) |> IO.inspect # => [2,4,6]
Di sini saya mengambil sebuah daftar dengan tiga angka dan membuat sebuah daftar baru dengan seluruh angka-nya yang dikalikan oleh 2
. panggilan map
dapat disederhanakan lebih jauh menjadi Enum.map( &(&1 * 2) )
.
Fungsi do_something/1
bisa ditulis kembali menggunakan comprehension:
def do_something(list) do for el <- list, do: el * 2 end
Inilah penampilan dari comprehension, menurut saya, kode-nya sedikit lebih elegan dari yang di kode pertama. Di sini, sekali lagi, kita mengambil tiap elemen dari daftar dan mengalikannya dengan 2
. Bagian el <- list
disebut sebagai generator dan dia menjelaskan secara pasti pengambilan nilai dari koleksi sesuai yang kamu inginkan.
Ingat bahwa kita tidak dipaksa untuk melewatkan sebuah daftar ke fungsi do_something/1
- kode-nya akan bekerja dengan apapun yang bisa dienumerasi.
defmodule MyModule do def do_something(collection) do for el <- collection, do: el * 2 end end MyModule.do_something((1..3)) |> IO.inspect
Pada contoh ini, saya melewatkan sebuah rentang sebagai sebuah argumen.
Comprehension bekerja dengan binstring juga. Sintaks-nya sedikit berbeda karena kamu harus melingkupi generatormu dengan <<
dan >>
. Mari demosntrasikan ini dengan membuat sebuah fungsi sederhana untuk "mengambil" sebuah string yang dilindungi dengan Caesar cipher. Ide-nya sederhana: kita mengganti tiap huruf dari kata dengan sebuah huruf dari angka tetap posisi alfabet-nya. Saya akan menggesernya 1
posisi agar sederhana.
defmodule MyModule do def decipher(cipher) do for << char <- cipher >>, do: char - 1 end end MyModule.decipher("fmjyjs") |> IO.inspect # => 'elixir'
Ini terlihat sangat mirip dengan contoh sebelumnya kecuali di bagian <<
dan >>
. Kita mengambil sebuah kode dari tiap karakter dalam string, menguranginya dengan satu dan membuat sebuah string lagi. Sehingga pesan tersandi-nya adalah "elixir"!
Namun tetap, ada lebih banyak lagi selain itu. Salah satu fitur berguna lainnya dari comprehensions adalah kemampuannya untuk menyaring keluar beberapa elemen.
Comprehensions dan Filtering
Mari perluas contoh pertama kita. Saya akan melewatkan serentang integers dari 1
hingga 20
, hanya mengambil elemen yang genap dan mengalikannya dengan 2
:
defmodule MyModule do require Integer def do_something(collection) do collection |> Stream.filter( &Integer.is_even/1 ) |> Enum.map( &(&1 * 2) ) end end MyModule.do_something( (1..20) ) |> IO.inspect
Di sini saya harus menyaratkan modul Interger
-nya untuk mampu menggunakan macro is_even/1
. Dan juga, saya menggunakan Stream
untuk mengoptimasi kode-nya sedikit dan mencegah iterasinya dilakukan dua kali.
Sekarang mari tulis contoh ini kembali dengan comprehension:
def do_something(collection) do for el <- collection, Integer.is_even(el), do: el * 2 end
Jadi, seperti yang kamu lihat, for
bisa menerima sebuah filter opsional untuk melewati beberapa elemen dari koleksinya.
Kamu tidak dibatasi hanya dengan satu filter, sehingga kode berikut juga-lah valid:
def do_something(collection) do for el <- collection, Integer.is_even(el), el < 10, do: el * 2 end
Dia akan mengambil semua angka genap kurang dari 10
. Jangan lupa untuk membatasi filter-filter dengan koma.
Filter-nya akan dievaluasi untuk tiap elemen dari koleksinya dan jika evaluasi-nya mengembalikan true
, blok-nya akan dieksekusi. Jika tidak, sebuah elemen baru diambil. Apa yang menarik adalah generator juga bisa digunakan untuk menyaring elemen-elemen menggunakan when
:
def do_something(collection) do for el when el < 10 <- collection, Integer.is_even(el), do: el * 2 end
Ini sangat mirip dengan apa yang kita lakukan ketika menulis guard clauses:
def do_something(x) when is_number(x) do # ... end
Comprehensions dengan Banyak Koleksi
Sekarang anggap tidak memiliki satu tapi dua koleksi sekaligus, dan kita ingin membuat sebuah koleksi baru. Contohnya,mengambil semua angka genap-nya dari koleksi pertama dan yang ganjil dari yang kedua lalu mengalikan mereka:
defmodule MyModule do require Integer def do_something(collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: el1 * el2 end end MyModule.do_something( (1..20), (5..10) ) |> IO.inspect
Contoh ini mengilustrasi bahwa comprehension bisa bekerja dengan lebih satu koleksi sekaligus. Angka genap pertama dari collection1
akan diambil dan dikalikan dengan tiap angka ganjil dari collection2
. Kemudian, integer kedua dari collection1
akan diambil dan dikalikan dan seterusnya. Hasilmya akan menjadi:
[10, 14, 18, 20, 28, 36, 30, 42, 54, 40, 56, 72, 50, 70, 90, 60, 84, 108, 70, 98, 126, 80, 112, 144, 90, 126, 162, 100, 140, 180]
Apalagi, hasil nilainya tidak harus integers. Contohnya, kamu bisa mengembalikan tupel yang mengadung integers dari koleksi pertama dan kedua:
defmodule MyModule do require Integer def do_something(collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: {el1,el2} end end MyModule.do_something( (1..20), (5..10) ) |> IO.inspect # => [{2, 5}, {2, 7}, {2, 9}, {4, 5}...]
Comprehension dengan Opsi "Into"
Hingga titik ini, hasil akhir dari comprehension selalu sebuah daftar. Sebenarnya ini tidak wajib juga. Kamu bisa menspesifikasikan sebuah parameter into
yang menerima sekoleksi untuk mengandung hasilnya.
Parameter ini menerima struktur apapun yang mengimplementasikan protokol Collectable, contohnya kita mebuat sebuah map seperti ini:
defmodule MyModule do require Integer def do_something(collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), into: Map.new, do: {el1,el2} end end MyModule.do_something( (1..20), (5..10) ) |> IO.inspect # => %{2 => 9, 4 => 9, 6 => 9...}
Di sini saya mengatakan into: Map.new
yang juga bisa diganti dengan into: &{}
. Dengan mengembalikan tupel {el1, el2}
kita pada dasarnya mengatur elemen pertama sebagai sebuah kunci dan yang kedua sebagai nilainya.
Contoh ini tidaklah sangat berguna, namun, mari buat sebuah map dengan sebuah nomor sebagai key dan mengkuadratkannya sebagai sebuah nilai:
defmodule MyModule do def do_something(collection) do for el <- collection, into: Map.new, do: {el, :math.sqrt(el)} end end squares = MyModule.do_something( (1..20) ) |> IO.inspect # => %{1 => 1.0, 2 => 1.4142135623730951, 3 => 1.7320508075688772,...} squares[3] |> IO.puts # => 1.7320508075688772
Pada contoh ini saya menggunakan modul :math
Erlang secara langsung. Lagipula, semua nama modul adalah atom. Sekarang kamu bisa dengan mudah mencari kuadrat dari tiap angka antara 1
sampai 20
.
Comprehensions dan Pattern Matching
Hal terakhir untuk disebut adalah kamu bisa melakukan pattern matching di comprehensions juga. Pada beberapa kasus dia bisa jadi sangat mudah.
Anggap kita memiliki map mengandung nama-nama pekerja dan gaji kotor mereka:
%{"Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30}
Saya ingin membuat sebuah map dimana nama-nama-nya di huruf kecil dan dikonversi ke atom, dan gajinya dihitung menggunakan persentase pajak:
defmodule MyModule do @tax 0.13 def format_employee_data(collection) do for {name, salary} <- collection, into: Map.new, do: {format_name(name), salary - salary * @tax} end defp format_name(name) do name |> String.downcase |> String.to_atom end end MyModule.format_employee_data( %{"Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30} ) |> IO.inspect # => %{alice: 39.15, bill: 34.8, jim: 26.1, joe: 43.5}
Pada contoh ini kita mendefinisikan sebuah atribut modul @tax
dengan sebuah angka asal. Lalu, saya mendekonstruksi data-nya di comprehension menggunakan {name, salary} <- collection
. Terakhir, format nama-nya dan kalkulasi gajinya sebutuhnya dan menyimpan hasilnya di map yang baru. Cukup sederhana lagi ekspresif.
Kesimpulan
Pada artikel ini, kita telah melihat cara menggunakan Elixir comprehension. Kamu mungkin butuh beberapa waktu untuk terbiasa dengannya. Konstruksi ini sangat rapih dan pada beberapa kasus bisa cocok lebih baik daripada fungsi seperti map
dan filter
. Kamu bisa mencari beberapa contoh di Dokumentasi Resmi Elixir dan panduan memulai.
Semoga, kamu menganggap panduan ini berguna dan menyenangkan! Terima kasih telah ikut dengan saya dan sampai jumpa lagi.