Lua-was Tapi Mbois

Teman saya yang harusnya orang Batak tapi mengaku orang Surabaya sering menggunakan istilah "mbois" untuk mengungkapkan kekagumannya pada suatu hal. Artinya ya kurang lebih bagus atau keren. Saya sedang tidak ingin membahas teman saya ini. Yah, walaupun keberadaannya di muka bumi menggelitik saya. Dia mengalami krisis identitas yang lumayan parah. Namanya Pramudya. Sangat bagus, mirip pengarang terkenal. Tapi, dia minta dipanggil Boy. Tidak nyambung sama sekali.

Baru-baru ini saya sedang menyelidiki tentang penggunaan dynamically typed scripting language untuk diintegrasikan dengan Unreal Engine. Sebagai catatan, saya membenci game engine ini. Setiap kali saya berurusan dengannya, saya harus menahan sumpah serapah. Unreal Engine ini sangat canggih tapi berantakan. Seakan-akan dia dibuat keroyokan tanpa komando yang kuat. Yang penting jalan. Setiap kali muncul versi baru, tiba-tiba saja game yang dikembangkan dengan versi sebelumnya menjadi rusak. Harus ada penyesuaian lagi dengan versi Unreal Engine yang baru.

Hal yang menurut saya paling nirfaedah di engine ini adalah Blueprint, sebuah visual scripting language. Artinya, kita bisa ngoding tidak pake teks lagi, tapi pake diagram. Terus terang, saya benci setengah mati sama Blueprint. Saya ini orangnya cerdas kalau berurusan dengan bahasa tulis. Namun, tidak begitu kalau dengan bahasa gambar. Apalagi gambarnya gambar tiga dimensi. Saya masuk Hartono Mall saja kesasar. Padahal, saya sudah ke sana lebih dari dua kali. Sebagai seorang touch typist yang otaknya sudah pindah ke jari, melihat Blueprint saya langsung jadi idiot. Siapa yang tidak ngamuk kalau sudah seperti itu.

Nah, di sinilah peran dynamically typed scripting language itu jadi penting. Pokoknya, bagaimana caranya saya tidak menyentuh itu diagram-diagram ruwet selama-lamanya. Yang namanya game, ada bagian-bagian di mana scripting itu diperlukan. Misal, ketika merancang dialog antara dua karakter. Seandainya dialog itu ditulis sepenuhnya dalam C, maka setiap kali melakukan perubahan sedikit saja, maka seluruh _game_ harus di-_compile_ ulang. Padahal, C itu compiler-nya adalah yang paling lambat sedunia. Apalagi jika game tersebut ukurannya berpuluh-puluh GB. Untuk mengubah sebaris dialog, kita jadi harus jalan-jalan ke Bali dulu sembari menunggu proses compiling selesai. Kalau memakai bahasa scripting, tidak perlu harus di-compile lagi. Cukup dengan mengedit satu file maka sudah selesai.

Baiklah, cukup sudah saya mencaci-maki Unreal Engine. Saya akan sedikit filosofis di sini. Menurut anda, apakah arti elegan itu? Arti umumnya tentu adalah elok, anggun, atau lemah gemulai. Lawan katanya adalah norak. Tapi, sebagai seorang insinyur, saya tidak suka makna ini. Ada makna lain dari elegan yang sejak dulu kala menjadi ukuran saya untuk mengapresiasi suatu karya perangkat lunak. Elegan adalah menyelesaikan masalah yang rumit dengan cara yang sederhana. Semakin rumit masalah yang diselesaikan, semakin elegan. Semakin sederhana cara penyelesaiannya, semakin elegan. Elegan bukanlah menerapkan pola-pola arsitektur yang muluk-muluk. Bukan pula mengikuti aturan-aturan yang banyak dan mengekang. Elegan bukanlah untuk mendapatkan sistem yang berjalan dengan indah, tertata, rapi, sesuai dengan keinginan kita. Elegan tidak harus memakai gaun sutera, sepatu kaca, dan rambut disanggul. Dalam dunia per-software-an, elegan justru kebalikannya. Meminjam ucapan Linus Torvalds, "Intelligence is the ability to avoid doing work, yet getting the work done". Kalau beliau menyebutnya sebagai intelligent, saya menamakan itu dengan elegant.

Ketika berkelana mencari bahasa scripting terhebat di muka bumi, saya bertemu dengan bahasa dynamically typed yang menurut saya elegan, yaitu Lua. Bagaimana tidak, di bawah ini adalah sintaks lengkap bahasa Lua.

chunk ::= block

block ::= {stat} [retstat]

stat ::=  ‘;’ |
        varlist ‘=’ explist |
        functioncall |
        label |
        break |
        goto Name |
        do block end |
        while exp do block end |
        repeat block until exp |
        if exp then block {elseif exp then block} [else block] end |
        for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end |
        for namelist in explist do block end |
        function funcname funcbody |
        local function Name funcbody |
        local namelist [‘=’ explist]

retstat ::= return [explist] [‘;’]

label ::= ‘::’ Name ‘::’

funcname ::= Name {‘.’ Name} [‘:’ Name]

varlist ::= var {‘,’ var}

var ::=  Name | prefixexp ‘[’ exp ‘]’ | prefixexp ‘.’ Name

namelist ::= Name {‘,’ Name}

explist ::= exp {‘,’ exp}

exp ::=  nil | false | true | Numeral | LiteralString | ‘...’ | functiondef |
        prefixexp | tableconstructor | exp binop exp | unop exp

prefixexp ::= var | functioncall | ‘(’ exp ‘)’

functioncall ::=  prefixexp args | prefixexp ‘:’ Name args

args ::=  ‘(’ [explist] ‘)’ | tableconstructor | LiteralString

functiondef ::= function funcbody

funcbody ::= ‘(’ [parlist] ‘)’ block end

parlist ::= namelist [‘,’ ‘...’] | ‘...’

tableconstructor ::= ‘{’ [fieldlist] ‘}’

fieldlist ::= field {fieldsep field} [fieldsep]

field ::= ‘[’ exp ‘]’ ‘=’ exp | Name ‘=’ exp | exp

fieldsep ::= ‘,’ | ‘;’

binop ::=  ‘+’ | ‘-’ | ‘*’ | ‘/’ | ‘//’ | ‘^’ | ‘%’ |
        ‘&’ | ‘~’ | ‘|’ | ‘>>’ | ‘<<’ | ‘..’ |
        ‘<’ | ‘<=’ | ‘>’ | ‘>=’ | ‘==’ | ‘~=’ |
        and | or

unop ::= ‘-’ | not | ‘#’ | ‘~’

Bandingkanlah dengan misal C#. Kalau tidak, Python 2 sajalah yang sama-sama bahasa dynamically typed. Saya jadi teringat Smalltalk, yang cuma butuh selembar amplop untuk mendeskripsikan keseluruhan bahasanya.

Lua ini juga runtime-nya sangat kecil, hanya ratusan KB saja. Sejak awal, Lua dirancang untuk diintegrasikan dengan bahasa C. Ini membuatnya menjadi favorit para pengembang game hingga saat ini. Ternyata, Lua ini sudah lama ada. Ya, kira-kira seangkatan sama Java, Python, atau Javascript lah. Saya curiga, dulu sebenarnya Brendan Eich ketika membikin ngaku-ngaku terinspirasi Scheme, padahal sebenarnya dia mencontek Lua.

Berkenalan dengan Lua

Tidak sebagaimana Go yang untuk mempelajarinya saya cukup butuh dua hari, saya justru butuh sekitar semingguan untuk bisa memahami seluk beluk Lua. Padahal, Go itu statically typed dan sintaksnya lebih banyak dari Lua. Tapi, saya justru lebih cepat paham. Bahkan, dua hari itu sudah menyinggung sampai ke channel. Entahlah, mungkin karena tutorial interaktif Go sangat bagus. Walaun ya, bagi saya Go itu cepat belajar, cepat lupa. Mungkin, karena Go itu katrok sehingga tidak berkesan. Tidak masalah. Kalau suatu saat harus bisa lagi, cuma butuh dua hari saja.

Selama seminggu belajar Lua, saya beberapa kali tertegun. Lua ini nampak serupa dengan banyak bahasa lain, tapi begitu ditilik lebih dalam, ternyata jauh beda.

Nil adalah ketiadaan

Bagi Lua, sesuatu yang bernilai nil dan sesuatu yang tidak pernah ada itu adalah sama. Kita mengakses variabel bernilai nil atau kita mengakses variabel yang tidak pernah ada, maka Lua akan menganggap dua-duanya adalah nil.

local a = nil

print(a)
print(b)

Jika potongan kode di atas dijalankan, maka hasilnya adalah:

nil
nil

Ada udang di balik fungsi

Tampaknya, para pembuat Lua ngotot bahwa sintaks Lua harus sederhana. Karena itulah, beberapa hal yang kalau di bahasa lain membutuhkan sintaks khusus, di Lua dia disamarkan sebagai fungsi. Untuk memuat modul lain, Lua punya "fungsi" yang bernama require

local mod = require('mod')

Potongan kode di atas artinya berkas bernama mod.lua akan dimuat dan disimpan ke dalam variabel mod.

Demikian juga dengan pcall dan coroutine, yang penjelasannya menyusul di bawah.

pcall

pcall adalah singkatan dari "protected call". Ia merupakan cara Lua untuk error handling. Kegunaan pcall adalah untuk menangkap error. Jika ada kode yang memanggil fungsi error tapi tidak ditangani oleh pcall, maka program Lua akan langsung keluar.

function foo(a, b)
-- semacam kode
    return 'foo'
end

function bar()
-- semacam kode
    error('error message') -- kira-kira, sama artinya dengan throw dalam bahasa lain
end

-- tanpa pcall
foo(a, b)
bar()

-- dengan pcall
local success, result = pcall(foo, a, b)
local success, result = pcall(bar)

-- inline function body
local success, result = pcall(function() error('anonymous funcion') end)

Dalam hal ini, saya lebih suka cara Lua ketimbang cara Go. Error di Go hanyalah nilai kembalian fungsi biasa. Kalau error tidak ditangani, maka akan diam-diam menyelinap ke baris berikutnya. Tahu-tahu, panic di tempat yang berbeda. Mengesalkan.

Coroutine

Kalau dalam bahasa lain, untuk memudahkan memprogram secara konkuren kita mengenal fitur yang namanya async / await ataupun coroutine. Ada pula yang namanya generator atau iterator. Yang terakhir ini biasanya dipakai untuk implementasi lazy infinite collection. Coroutine di Lua adalah penggabungan keduanya. Ini lebih mudah dibandingkan dengan misalnya C# yang harus berumit ria dengan async enumerable.

Di bawah ini adalah contoh penggunaan coroutine dalam fungsi yang menghasilkan bilangan prima.

function generate_prime(max)
    coroutine.yield(2)
    coroutine.yield(3)
    local past_values = {2, 3}
    local current = 5
    while current <= max do
        local i = 1

        while i <= #past_values and current % past_values[i] ~= 0 do
            i = i + 1
        end

        if (i > #past_values) then
            table.insert(past_values, current)
            coroutine.yield(current)
        end

        current = current + 2
    end
end

function main()
    local co = coroutine.create(generate_prime)
    _, value = coroutine.resume(co, 1000)

    while coroutine.status(co) ~= 'dead' do
        _, value = coroutine.resume(co)
        print(value)
    end
end

main()

coroutine.create akan membungkus fungsi generate_prime menjadi sebuah coroutine co. Ketika dipanggil pertama kali, coroutine.resume akan meneruskan argumen 1000 menjadi parameter max dari fungsi generate_prime. Pemanggilan-pemanggilan berikutnya lebih mudah untuk dicerna. coroutine.yield akan memberikan current kepada main lalu berhenti. Sedangkan coroutine.resume akan membuat generate_prime melanjutkan eksekusi hingga bertemu coroutine.yield berikutinya.

Pada contoh di atas, saya memperlakukan coroutine sebagai generator atau iterator. Tapi, akan sama saja prinsipnya untuk pemrograman konkuren ala “async / await”. Ini juga mirip seperti di Go dengan channel-nya.

package main

import "fmt"

func generate_prime(max int, c chan int) {
    c <- 2
    c <- 3
    pastValues := []int{2,3}
	current := 5
	for current < max {
        i := 0

        for (i < len(pastValues)) {
            if current % pastValues[i] == 0 {
                break;
            }
			i += 1
        }

        if i >= len(pastValues) {
            pastValues = append(pastValues, current)
            c <- current
        }

		current += 2
	}
    close(c)
}

func main() {
	c := make(chan int)
	go generate_prime(1000, c)

	for i := range c {
        fmt.Println(i)
    }
}

Sebenarnya, tidak semirip itu karena Go didasari oleh teori Communicating Sequential Process. Teori ini menjelaskan prinsip kerja sama dan pemindahan data pada dua proses yang konkuren. Sementara itu, Lua cuma punya satu thread. Coroutine satu dan yang lainnya bekerja sama bergantian berjalan di thread yang sama.

Oh ya, coroutine ini pada dasarnya ada pcall di dalamnya. Jadi, setiap kali kita memanggil coroutine.resume di atas, maka kita akan menerima dua nilai, nilai pertama adalah apakah pemanggilan coroutine.resume berakhir sukses, yang kedua adalah nilai yang dikembalikan oleh coroutine.yield.

Pemrograman "fungsional"

Ada beberapa ciri yang membuat Lua sedikit mendekati bahasa fungsional. Salah satunya adalah fungsi di Lua bisa dijadikan sebagai variabel. Lua juga mempunyai fungsi tanpa nama yang bisa digunakan sebagai lambda di bahasa lain. Fungsi di Lua bisa didefinisikan secara bersarang dan variabel dari luar fungsi bisa dikenali dari dalam fungsi. Kalau sudah begini, boleh lah Lua dianggap sebagai bahasa fungsional.

local obj = {}

function obj.foo()
    print 'foo'
end

obj.bar = function()
    print 'bar'
end

obj["foo"]() -- foo adalah fungsi yang menjadi anggota table obj
obj.bar() -- bar serupa method atau member function di bahasa lain

local i = 1

-- fungsi printi "menangkap" i dan menyalin nilainya ke dalam
function printi()
    print(i)
end

while i <= 10 do
    printi() -- menampilkan nilai i yang terbaru sesuai loop
    i = i+1
end

printi()

Lua adalah saudara Javascript

Bukan rahasia lagi kalau saya membenci Javascript. Akar kebencian ini adalah karena Javascript tidak masuk akal. Namun, kalau dipikir-pikir, ada beberapa hal di mana Lua dan Javascript ini sebenarnya mirip.

  1. Array

    Javascript dan Lua sama-sama memiliki array. Lua menyebutnya dengan istilah table, sementara Javascript menyebutnya dengan…​ array. Akan tetapi, …​

    //Javascript
    a = []
    a[0] = 1
    a[1] = 2
    a[99] = 100
    
    console.log(a.length) //nilainya adalah 100
    a[99] = undefined
    delete a[99] // biar lebih yakin, ikut cara Python
    console.log(a.length) //nilainya adalah 100

    Coba, apa gunanya membedakan undefined dan null kalau begini caranya? Sama-sama hasilnya nggak beres. Kalau di Lua,

    -- Lua
    a = {}
    a[1] = 1 -- array di Lua dimulai dari 1
    a[2] = 2
    a[100] = 100
    
    -- #a artinya panjang dari a
    print(#a) -- #a nilainya adalah 2
    a[100] = nil
    print(#a) -- #a nilainya adalah 2
    a[2] = nil
    print(#a) -- #a nilainya adalah 1

    Lua menjamin bahwa panjang suatu "array" akan benar jika tidak ada lubang (elemen bernilai nil) di dalamnya. Ngelesnya masih lebih bagus dari Javascript lah.

  2. Associative array

    Javascript menyebut map atau associative array dengan object. Lua menyebut associative array dengan…​ table. Ya, Lua menggunakan tipe data yang sama untuk map maupun array.

    -- Lua
    a = {}
    a["foo"] = true
    a["bar"] = "string"
    
    print(a.foo) -- true
    print(a.bar) -- "string"
    //Javascript
    a = {}
    a["foo"] = true;
    a["bar"] = "string";
    
    console.log(a.foo); // true
    console.log(a.bar); // "string"

    Mirip kan?

  3. Object-oriented programming

    Sebelum ada ES6, ES7, ES2017, ES2018, dan sebagainya, Javascript mengimplementasikan OOP dengan menggunakan fungsi. OOP di Lua juga bisa diimplementasikan dengan menggunakan fungsi. Mirip kan? Sebagaimana di Javascript, Lua menganut OOP berbasis prototype. Objek bisa langsung dibuat tanpa harus ada kelasnya terlebih dahulu.

    "Kelas" Javascript di bawah ini,

    //Javascript OOP
    function Vehicle(make, model) {
        this.make = make;
        this.model = model;
        this.start = function() {
            console.log(this.make + ' ' + this.model + ' starts');
        };
        this.drive = function() {
            console.log(this.make + ' ' + this.model + ' drives');
        };
        this.stop = function() {
            console.log(this.make + ' ' + this.model + ' stops');
        };
    }
    var ford = new Vehicle('Ford', 'Focus');
    var toyota = new Vehicle('Toyota', 'Corolla');
    ford.start();
    toyota.start();

    jika diterjemahkan ke Lua, akan menjadi

    -- Lua OOP, dengan closure
    function Vehicle(make, model)
        local self = { make = make, model = model }
    
        function self.start()
            print(self.make .. ' ' .. self.model .. ' starts')
        end
        function self.drive()
            print(self.make .. ' ' .. self.model .. ' drives')
        end
        function self.stop()
            print(self.make .. ' ' .. self.model .. ' stops')
        end
    
        return self
    end
    
    local ford = Vehicle('Ford', 'Focus')
    local toyota = Vehicle('Toyota', 'Corolla')
    ford.start()
    toyota.start()

    Tidak jauh beda, bukan? Kalau masih terasa beda, cobalah untuk memicingkan mata sampai hampir terpejam, pasti lama-lama akan jadi sama.

    Sebenarnya, kelas di Lua juga bisa diimplementasikan dengan cara lain, yaitu dengan cara metatable. Kelas ala metatable ini lebih umum dipakai di kalangan programmer Lua. Ini mirip dengan Javascript yang juga punya mekanisme prototype, akan tetapi…​ ah sudahlah.

    -- Lua OOP, dengan metatable
    Vehicle = {}
    function Vehicle.new(make, model)
        local self = setmetatable({}, Vehicle)
        self.make = make
        self.model = model
        return self
    end
    
    function Vehicle:start()
        print(self.make .. ' ' .. self.model .. ' starts')
    end
    
    function Vehicle:drive()
        print(self.make .. ' ' .. self.model .. ' drives')
    end
    
    function Vehicle:stop()
        print(self.make .. ' ' .. self.model .. ' stops')
    end
    
    local ford = Vehicle.new('Ford', 'Focus')
    local toyota = Vehicle.new('Toyota', 'Corolla')
    ford:start()
    toyota:start()

    Akan tetapi, sintaksnya sedikit asing bagi yang belum terbiasa. Di Lua, Vehicle:drive() itu hanyalah cara lain untuk menyebut Vehicle.drive(self). Kalau di Python, self itu muncul di definisi fungsi, tapi menghilang ketika dipanggil. Di Lua, kalau mau menghilang baik ketika di definisi maupun di pemanggilan, harus memakai simbol :. Mengapa harus seperti itu? Apa bedanya dengan OOP cara closure? Panjang ceritanya. Singkatnya, dua-duanya bisa dipakai. Dua-duanya juga mendukung inheritance.

Sebuah Senjata Baru

Bagi saya yang pintar tapi tidak jenius ini, menemukan Lua adalah sebuah berkah. Saya semakin yakin, bahkan di kalangan para programmer yang intelek sekalipun, mengikuti banyak orang seringnya sesat. Contohnya adalah Javascript, PHP, C++ di tahun 1990-an. Semakin ke sini, semakin banyak saya menemukan teknologi yang paling banyak dipakai bukanlah teknologi yang paling bagus. Ini tidak semata-mata masalah selera. Ini soal elegan. Dan, elegan di dunia per-enjinir-an itu objektif.

Begitulah, saya akhirnya bertemu dengan mainan baru. Untungnya, mainan baru ini disetujui oleh bos-bos saya. Mungkin, karena reputasinya yang bagus di dunia game development. Apapun itu, yang penting saya bisa tambah hepi di pekerjaan saya.