Fungsi #
Di Ruby, istilah yang tepat bukan “fungsi” tapi “metode” — karena setiap potongan kode yang bisa dipanggil selalu melekat pada sebuah objek, baik itu instance kelas, kelas itu sendiri, atau objek top-level main. Tapi lebih dari sekadar terminologi, metode di Ruby punya sejumlah karakteristik yang membedakannya dari fungsi di bahasa lain: nilai kembalian selalu ada (ekspresi terakhir dikembalikan secara implisit), tanda kurung opsional saat memanggil, dan sistem parameter yang sangat fleksibel — dari default value, keyword argument, splat, hingga double splat untuk hash. Memahami semua ini adalah fondasi untuk menulis Ruby yang ekspresif dan mudah digunakan oleh developer lain.
Mendefinisikan dan Memanggil Metode #
Metode didefinisikan dengan keyword def dan diakhiri dengan end. Nama metode mengikuti konvensi snake_case:
def sapa(nama)
"Halo, #{nama}!"
end
puts sapa("Dewi") # => Halo, Dewi!
Tanda kurung saat memanggil metode bersifat opsional di Ruby — tapi ada konvensi yang perlu diikuti:
# Tanda kurung BOLEH dihilangkan saat memanggil
puts sapa "Dewi" # valid
puts sapa("Dewi") # juga valid
# KONVENSI: pakai tanda kurung jika ada argumen, hilangkan jika tidak ada
puts sapa("Dewi") # ← lebih jelas ada argumen
daftar_produk # ← tanpa argumen, tanda kurung tidak diperlukan
# ANTI-PATTERN: hilangkan tanda kurung saat ada argumen — ambigu
hasil = hitung lebar, tinggi # sulit dibedakan dari variabel
hasil = hitung(lebar, tinggi) # jelas ini pemanggilan metode
Nilai Kembalian Implisit #
Di Ruby, metode selalu mengembalikan nilai — yaitu nilai dari ekspresi terakhir yang dieksekusi. Keyword return tidak diperlukan kecuali untuk keluar lebih awal:
# ANTI-PATTERN: return eksplisit di akhir metode — tidak perlu
def kuadrat(n)
return n ** 2 # ← return di sini tidak perlu
end
# BENAR: return implisit — ekspresi terakhir dikembalikan otomatis
def kuadrat(n)
n ** 2
end
# return BERGUNA untuk early exit
def bagi(a, b)
return "Error: tidak bisa bagi nol" if b == 0
a.fdiv(b)
end
puts bagi(10, 2) # => 5.0
puts bagi(10, 0) # => "Error: tidak bisa bagi nol"
Karena metode adalah ekspresi yang mengembalikan nilai, mereka bisa langsung digunakan dalam konteks apapun:
def max(a, b)
a > b ? a : b
end
puts max(3, 7) # => 7
puts "Nilai terbesar: #{max(10, 5)}" # => Nilai terbesar: 10
hasil = [max(1,2), max(3,4), max(5,6)] # => [2, 4, 6]
Parameter dan Argumen #
Ruby menyediakan sistem parameter yang sangat kaya — jauh lebih fleksibel dari kebanyakan bahasa.
Parameter Posisional #
Parameter posisional adalah yang paling dasar — nilai diteruskan berdasarkan urutan:
def buat_profil(nama, umur, kota)
"#{nama}, #{umur} tahun, dari #{kota}"
end
puts buat_profil("Rina", 28, "Bandung")
# => Rina, 28 tahun, dari Bandung
Parameter Default #
Parameter bisa punya nilai default yang digunakan jika argumen tidak diberikan:
def kirim_pesan(tujuan, isi, format: :teks, prioritas: :normal)
"[#{prioritas.upcase}] Ke: #{tujuan} | Format: #{format} | #{isi}"
end
puts kirim_pesan("[email protected]", "Server down!")
# => [NORMAL] Ke: [email protected] | Format: teks | Server down!
puts kirim_pesan("[email protected]", "Critical!", prioritas: :tinggi, format: :html)
# => [TINGGI] Ke: [email protected] | Format: html | Critical!
Parameter default bisa berupa ekspresi apapun — termasuk nilai yang dihitung saat pemanggilan:
def log(pesan, waktu = Time.now, level = :info)
"[#{level.upcase}] #{waktu.strftime('%H:%M:%S')} — #{pesan}"
end
puts log("Server dimulai")
puts log("Error kritis", Time.now, :error)
Parameter dengan default value harus diletakkan setelah parameter tanpa default. Ruby memang mengizinkan default di tengah, tapi ini membingungkan — letakkan semua parameter wajib di awal, parameter opsional di akhir.
Keyword Arguments #
Keyword argument membuat pemanggilan metode lebih ekspresif karena nama parameter disebut eksplisit. Urutan tidak penting, dan tidak ada ambiguitas:
def buat_koneksi(host:, port:, database:, timeout: 30, ssl: false)
puts "Connecting to #{host}:#{port}/#{database}"
puts "Timeout: #{timeout}s | SSL: #{ssl}"
end
# Urutan bisa bebas — nama yang menentukan
buat_koneksi(
database: "app_production",
host: "db.example.com",
port: 5432,
ssl: true
)
# ANTI-PATTERN: parameter posisional untuk metode dengan banyak argumen
def buat_koneksi_lama(host, port, database, timeout, ssl)
# pemanggil harus ingat urutan yang tepat — rawan kesalahan
end
buat_koneksi_lama("db.example.com", 5432, "app", 30, true)
# angka 5432, 30, true — apa artinya tanpa nama?
Kamu bisa menerima keyword argument yang tidak diketahui sebelumnya dengan **:
def tampilkan_opsi(**opsi)
opsi.each { |k, v| puts " #{k}: #{v}" }
end
tampilkan_opsi(warna: :biru, ukuran: :besar, tebal: true)
# => warna: biru
# => ukuran: besar
# => tebal: true
Splat Operator — Argumen Variabel #
Splat (*) mengumpulkan semua argumen posisional yang tersisa ke dalam Array:
def jumlahkan(*angka)
angka.sum
end
puts jumlahkan(1, 2, 3) # => 6
puts jumlahkan(10, 20, 30, 40) # => 100
puts jumlahkan # => 0 (array kosong)
# Splat di tengah — tangkap "sisa" argumen
def pertama_tengah_terakhir(pertama, *tengah, terakhir)
puts "Pertama: #{pertama}"
puts "Tengah: #{tengah.inspect}"
puts "Terakhir: #{terakhir}"
end
pertama_tengah_terakhir(1, 2, 3, 4, 5)
# => Pertama: 1
# => Tengah: [2, 3, 4]
# => Terakhir: 5
Splat juga berguna untuk meneruskan Array sebagai argumen terpisah:
def tambah(a, b, c)
a + b + c
end
angka = [1, 2, 3]
puts tambah(*angka) # => 6 (splat "meledakkan" array jadi argumen terpisah)
Kombinasi Semua Jenis Parameter #
Ruby mengizinkan kombinasi semua jenis parameter — dengan urutan yang harus diikuti:
# Urutan wajib: posisional → *splat → keyword → **double_splat → &blok
def metode_kompleks(wajib, opsional = "default", *sisa, keyword:, kw_opsional: nil, **ekstra, &blok)
puts "wajib: #{wajib}"
puts "opsional: #{opsional}"
puts "sisa: #{sisa.inspect}"
puts "keyword: #{keyword}"
puts "kw_opsional: #{kw_opsional.inspect}"
puts "ekstra: #{ekstra.inspect}"
blok.call if blok
end
metode_kompleks("a", "b", "c", "d", keyword: "wajib", x: 1, y: 2) { puts "blok!" }
Metode dengan Blok #
Salah satu fitur paling khas Ruby adalah kemampuan meneruskan blok kode ke metode. Ada dua cara menerima dan menjalankan blok: dengan yield dan dengan parameter eksplisit &blok.
yield #
yield memanggil blok yang diteruskan ke metode. Kamu bisa meneruskan nilai ke blok sebagai argumen yield:
def ukur_waktu(label)
mulai = Time.now
hasil = yield # panggil blok, tangkap nilai kembaliannya
durasi = Time.now - mulai
puts "#{label} selesai dalam #{(durasi * 1000).round(2)}ms"
hasil
end
total = ukur_waktu("Kalkulasi") do
(1..1_000_000).sum
end
puts "Hasil: #{total}"
# => Kalkulasi selesai dalam ~XXms
# => Hasil: 500000500000
# Meneruskan nilai ke blok
def transformasi(koleksi)
koleksi.map { |item| yield item }
end
hasil = transformasi([1, 2, 3, 4, 5]) { |n| n ** 2 }
puts hasil.inspect # => [1, 4, 9, 16, 25]
block_given? mengecek apakah blok diteruskan — berguna untuk membuat blok opsional:
def proses(data)
hasil = data.map { |d| d.to_s.upcase }
if block_given?
yield hasil # beri blok kesempatan untuk melakukan sesuatu dengan hasilnya
else
hasil # kembalikan langsung jika tidak ada blok
end
end
proses(["a", "b", "c"]) # => ["A", "B", "C"]
proses(["a", "b", "c"]) { |h| h.join(", ") } # => "A, B, C"
Parameter Blok Eksplisit dengan & #
Jika kamu perlu menyimpan blok ke variabel, meneruskannya ke metode lain, atau memanggilnya lebih dari sekali, gunakan parameter &:
def jalankan_dua_kali(&blok)
blok.call
blok.call
end
jalankan_dua_kali { puts "Halo!" }
# => Halo!
# => Halo!
# Meneruskan blok ke metode lain
def filter_dan_proses(data, &kondisi)
data.select(&kondisi).map { |d| d * 10 }
end
hasil = filter_dan_proses([1, 2, 3, 4, 5]) { |n| n.odd? }
puts hasil.inspect # => [10, 30, 50]
&:method_name adalah shorthand idiomatik untuk mengonversi Symbol menjadi blok:
# ANTI-PATTERN: blok verbose untuk method sederhana
["rina", "budi", "citra"].map { |n| n.upcase }
["rina", "budi", "citra"].select { |n| n.start_with?("r") }
# BENAR: Symbol to Proc — ringkas dan ekspresif
["rina", "budi", "citra"].map(&:upcase)
["rina", "budi", "citra"].select(&:frozen?)
[1, -2, 3, -4].select(&:positive?)
[nil, 1, nil, 2].reject(&:nil?)
Method Visibility — public, private, protected #
Ruby mengontrol akses metode melalui tiga tingkat visibility:
class AkunBank
def initialize(saldo)
@saldo = saldo
end
# public — bisa dipanggil dari mana saja (default)
def info
"Saldo: #{format_rupiah(@saldo)}"
end
def transfer(tujuan, jumlah)
return "Saldo tidak cukup" unless cukup_saldo?(jumlah)
kurangi_saldo(jumlah)
tujuan.tambah_saldo(jumlah)
"Transfer berhasil"
end
protected
# protected — bisa dipanggil oleh instance dari kelas yang sama
def tambah_saldo(jumlah)
@saldo += jumlah
end
private
# private — hanya bisa dipanggil dari dalam kelas ini sendiri
def format_rupiah(angka)
"Rp #{angka.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1.').reverse}"
end
def cukup_saldo?(jumlah)
@saldo >= jumlah
end
def kurangi_saldo(jumlah)
@saldo -= jumlah
end
end
akun = AkunBank.new(1_000_000)
puts akun.info # => Saldo: Rp 1.000.000
akun.kurangi_saldo(100) # => NoMethodError: private method called
akun.format_rupiah(5000) # => NoMethodError: private method called
flowchart TD
A[Method Visibility] --> B[public]
A --> C[protected]
A --> D[private]
B --> B1["Bisa dipanggil dari mana saja\nObjek lain, luar kelas"]
C --> C1["Hanya dari dalam kelas\ndan subkelas yang sama\nBerguna untuk operator seperti <=>"]
D --> D1["Hanya dari dalam\nobjek itu sendiri\ntanpa receiver eksplisit"]Di Ruby modern (sejak 2.7),privatebisa digunakan langsung sebagai prefix:private def hitung_internal; end. Ini lebih ringkas dari menempatkan metode setelah keywordprivatedi bawahnya, dan lebih mudah dibaca karena niat visibilitasnya langsung terlihat di samping definisi metode.
Method Chaining #
Method chaining memungkinkan pemanggilan beberapa metode secara beruntun dalam satu ekspresi. Kuncinya adalah setiap metode harus mengembalikan objek yang relevan — biasanya self:
class QueryBuilder
def initialize
@kondisi = []
@urutan = nil
@batas = nil
end
def where(kondisi)
@kondisi << kondisi
self # kembalikan self untuk chaining
end
def order(kolom)
@urutan = kolom
self
end
def limit(n)
@batas = n
self
end
def build
sql = "SELECT * FROM users"
sql += " WHERE #{@kondisi.join(' AND ')}" unless @kondisi.empty?
sql += " ORDER BY #{@urutan}" if @urutan
sql += " LIMIT #{@batas}" if @batas
sql
end
end
query = QueryBuilder.new
.where("aktif = true")
.where("umur > 18")
.order("nama ASC")
.limit(10)
.build
puts query
# => SELECT * FROM users WHERE aktif = true AND umur > 18 ORDER BY nama ASC LIMIT 10
Method chaining sangat alami di Ruby karena hampir semua Enumerable method mengembalikan koleksi baru yang bisa langsung dichain:
hasil = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
.uniq
.select { |n| n > 2 }
.sort
.map { |n| n ** 2 }
.first(3)
puts hasil.inspect # => [9, 16, 25]
Konvensi Penamaan Metode #
Ruby punya konvensi penamaan yang kuat dan bermakna — akhiran nama metode memberikan informasi tentang perilakunya:
# Metode yang mengembalikan boolean — akhiran ?
user.aktif? # => true / false
string.empty? # => true / false
array.include?(5) # => true / false
angka.zero? # => true / false
# Metode yang berbahaya / memodifikasi objek asli — akhiran !
nama.upcase # mengembalikan String baru
nama.upcase! # memodifikasi nama itu sendiri (destruktif)
array.sort # mengembalikan Array baru yang terurut
array.sort! # mengurutkan array itu sendiri
string.strip # mengembalikan String baru tanpa whitespace
string.strip! # memodifikasi string itu sendiri
# Setter method — akhiran =
class Produk
def nama=(nilai) # setter: produk.nama = "Laptop"
@nama = nilai
end
def nama # getter: produk.nama
@nama
end
end
Panduan konvensi nama metode:
akhiran ? → metode predikat, selalu kembalikan boolean
akhiran ! → versi destruktif atau "berbahaya" dari metode biasa
akhiran = → setter method, dipanggil dengan sintaks assignment
awalan is_ → ANTI-PATTERN di Ruby — gunakan ? bukan is_aktif?
awalan get_/set_ → ANTI-PATTERN di Ruby — pakai nama langsung
Metode di Level Top-level #
Metode yang didefinisikan di luar kelas atau modul manapun sebenarnya menjadi private method dari objek main (instance dari Object). Ini membuatnya terlihat seperti fungsi global:
def halo
puts "Halo dari top-level!"
end
def tambah(a, b)
a + b
end
halo # => Halo dari top-level!
puts tambah(3, 4) # => 7
# Di balik layar, ini ekuivalen dengan:
# Object.send(:define_method, :halo) { puts "Halo!" }
# Itulah kenapa metode top-level bisa dipanggil dari mana saja
Proc dan Lambda sebagai First-class Function #
Ruby memperlakukan blok, Proc, dan Lambda sebagai objek kelas satu — mereka bisa disimpan ke variabel, diteruskan sebagai argumen, dan dikembalikan dari metode. Ini membuka pola pemrograman fungsional yang powerful.
# Lambda — metode anonim yang bisa disimpan ke variabel
kuadrat = ->(n) { n ** 2 }
tambah = ->(a, b) { a + b }
sapa = ->(nama, salam: "Halo") { "#{salam}, #{nama}!" }
puts kuadrat.call(5) # => 25
puts kuadrat.(5) # sintaks alternatif
puts kuadrat[5] # sintaks alternatif ketiga
puts tambah.call(3, 4) # => 7
puts sapa.call("Rina") # => "Halo, Rina!"
puts sapa.call("Budi", salam: "Selamat datang")
# Lambda sebagai argumen
def terapkan(nilai, transformasi)
transformasi.call(nilai)
end
puts terapkan(10, kuadrat) # => 100
puts terapkan(10, ->(n) { n + 5 }) # => 15
# Higher-order method — kembalikan lambda dari metode
def pembuat_pengganda(faktor)
->(n) { n * faktor }
end
kali_dua = pembuat_pengganda(2)
kali_sepuluh = pembuat_pengganda(10)
puts kali_dua.call(7) # => 14
puts kali_sepuluh.call(7) # => 70
# Komposisi fungsi
def komposisi(f, g)
->(x) { f.call(g.call(x)) }
end
tambah_satu = ->(n) { n + 1 }
kali_tiga = ->(n) { n * 3 }
tambah_lalu_kali = komposisi(kali_tiga, tambah_satu)
puts tambah_lalu_kali.call(4) # => (4+1)*3 = 15
Method Objects #
Kamu juga bisa mengambil referensi ke metode yang sudah ada sebagai objek menggunakan method():
def pangkat_dua(n)
n ** 2
end
fn = method(:pangkat_dua)
puts fn.call(6) # => 36
puts [1, 2, 3, 4].map(&fn).inspect # => [1, 4, 9, 16]
# Sangat berguna untuk meneruskan method ke iterator
[" Rina ", " Budi ", " Citra "].map(&method(:puts))
# mencetak setiap nama
validator = method(:valid_email?)
emails.select(&validator) # filter menggunakan method yang sudah ada
Memoization dalam Metode #
Pola memoization menggunakan ||= untuk menghitung nilai yang mahal hanya sekali dan menyimpannya dalam variabel instance:
class Laporan
def initialize(tahun)
@tahun = tahun
end
def total_pendapatan
@total_pendapatan ||= hitung_pendapatan
end
def total_pengeluaran
@total_pengeluaran ||= hitung_pengeluaran
end
def laba_bersih
total_pendapatan - total_pengeluaran
end
private
def hitung_pendapatan
puts "Menghitung pendapatan dari DB..."
# query yang mahal — hanya dijalankan sekali
rand(1_000_000_000)
end
def hitung_pengeluaran
puts "Menghitung pengeluaran dari DB..."
rand(500_000_000)
end
end
laporan = Laporan.new(2024)
puts laporan.total_pendapatan # "Menghitung..." muncul
puts laporan.total_pendapatan # cache — "Menghitung..." tidak muncul lagi
puts laporan.laba_bersih # menggunakan nilai yang sudah di-cache
Ringkasan #
- Return value selalu ada — ekspresi terakhir dalam metode dikembalikan secara implisit. Gunakan
returnhanya untuk early exit, bukan di akhir metode.- Keyword argument lebih ekspresif dari posisional — untuk metode dengan lebih dari dua-tiga argumen, keyword argument mencegah kebingungan urutan dan memperjelas maksud pemanggil.
- Splat
*untuk argumen variabel — kumpulkan argumen posisional tak terbatas ke Array. Double splat**untuk keyword argument tak terbatas ke Hash.yielduntuk blok sederhana,&blokuntuk blok yang perlu disimpan — gunakanblock_given?untuk membuat blok opsional.&:method_nameadalah idiom paling ringkas untuk meneruskan method sederhana ke iterator.- Akhiran
?untuk boolean,!untuk destruktif,=untuk setter — ini bukan hanya konvensi kosmetik; ia menyampaikan kontrak perilaku metode.privateuntuk implementasi internal — sembunyikan detail implementasi agar API kelas tetap bersih dan kamu bebas mengubah internal tanpa merusak pengguna.- Method chaining dengan
return self— kembalikanselfdari metode yang memodifikasi state untuk memungkinkan chaining yang ekspresif.- Lambda bisa disimpan ke variabel dan diteruskan — gunakan untuk higher-order function, komposisi, dan callback yang perlu disimpan lebih dari satu siklus pemanggilan.
- Memoization dengan
||=— simpan hasil kalkulasi mahal dalam variabel instance agar tidak dihitung ulang setiap kali dipanggil.