Berkenalan dengan Scala

source: https://dwglogo.com/scala-logo/

Beberapa tahun belakangan ini, muncul banyak alternatif bahasa pemrograman di atas JVM. Salah satunya adalah Scala. Apa itu Scala?

Scala adalah bahasa pemrograman yang menggabungkan paradigma pemograman berorientasi objek dengan fungsional. Scala memiliki type system yang kuat dan statis (strong, static type-system). Scala berjalan di atas JVM, dan memiliki interoperability yang kuat dengan Java. Developer bisa dengan mudah meng-import library Java di program berbasis Scala. Scala dirancang sebagai bahasa yang memungkinkan developer membuat kode yang ringkas, fleksibel, namun tetap aman.

Scala sudah banyak digunakan untuk membangun aplikasi yang erat dengan data engineering. Beberapa di antaranya adalah Apache Spark, Apache Kafka, Apache Flink, dan Apache Samza, dan lain-lain Akka.io. Selain itu, Scala juga digunakan di beberapa institusi komersial terkenal yaitu Twitter, Verizon, LinkedIn, dan Wix.

Jadi, apa fitur-fitur dari Scala? Beberapa fitur seperti immutability by default, trait, maupun pattern matching tersedia di Scala. Artikel ini hanya membahas sebagian kecil dari fitur-fitur yang ada di Scala. Berikut adalah sekilas pembahasan fitur-fitur dasar yang ada:

  • Scala menerapkan immutability

Salah satu fitur dasar di Scala adalah pilihan untuk membuat sebuah variabel immutable atau mutable. Berikut adalah contohnya:

val x = 1 //val is used for immutable identifier
var y = 2 //var is used for mutable identifier
x = 2 //compile error
y = 3 //compile success

Salah satu ajaran dasar ketika pertama kali memasuki Scala adalah gunakan selalu variabel immutable dibanding mutable. Konsekuensinya, developer akan dipaksa untuk membangun metode dan fungsi yang murni (tidak menjalankan mutasi pada variabel di luar lingkup) karena tidak mungkin mengubah variabel yang diinisiasi dengan val. Pada akhirnya developer akan lebih mudah menalar bagaimana aliran data dari satu tempat ke tempat lain.

Scala juga menyediakan struktur data immutable dan mutable. Ini tersedia di package scala.collection.immutable dan scala.collection.mutable . Panduan dasar di Scala adalah selalu gunakan struktur data List , Map , atau Set dari immutable kecuali ada kebutuhan untuk optimasi kinerja kode.

  • Scala memungkinkan type inference.

Walaupun type system Scala kaku, namun developer tidak harus selalu menganotasi setiap deklarasi variabel atau fungsi dengan tipenya. Dengan informasi yang cukup compiler akan otomatis menganotasi dengan benar tipe dari variabel yang dideklarasi. Ini memberikan keunggulan ergonomis untuk developer karena developer tidak harus selalu mengetikkan anotasi tipe yang diinginkan. Namun ada kalanya compiler tidak cukup pintar untuk memberikan anotasi yang benar. Ini cukup sering terjadi ketika developer sudah menggunakan fitur lebih canggih di Scala seperti implicits atau higher-kind type. Contohnya adalah sebagai berikut:

val x = 1 // x is Int
val y = "1" // y is String
val z: Boolean = true // z is Boolean ////of course
val a: String = 1 // compile error
val listOfInt = List(1) // listOfInt is List[Int]
val listOfString = List("a","b") // listOfString is List[String]

Namun, contoh seperti ini perlu jadi perhatian:

val listOfWhat = List(1, true, "Boolean") // listOfWhat is List[Any]

Ini karena Scala akan otomatis mencari tipe super dari Int, String, serta Boolean, dan berakhir di tipe Any , tipe paling super di hierarki tipe Scala. Jika menginginkan konstruksi List atau jenis koleksi yang lain yang menunjang tipe yang heterogen atau generic programming secara umum, developer bisa menggunakan library shapeless.

Scala juga memiliki tipe yang merupakan turunan dari semua tipe, yaitu Nothing . Lebih lanjut terkait type system di Scala bisa dilihat di artikel ini.

  • Blok kode adalah ekspresi

Atau dengan kata lain baris kode akan menghasilkan nilai. Ini cukup menyenangkan karena developer bisa membuat kode seperti berikut:

val x = 100
val isThisOk = if(x > 100) "Go" else "Don't go"
/*
* isThisOk = x > 100 ? "Go" : "Don't go"
*/

Atau seperti ini

val dbUsername = {
val conf = ConfigFactory.loadConfig()
val username = conf.getString("db.username")
username
}

atau bahkan seperti ini

val doNothing = {}

Pada contoh di atas, isThisOk dan dbUsernameakan memiliki tipe String . Khusus untuk doNothing yang tidak memiliki operasi apapun, tipe yang dimiliki adalah Unit . Ini bisa dibilang sepadan dengan void di C atau Java. Yang perlu diperhatikan:

val x = val y = 1 //compile error
val x = { val y = 1 } //compile success

ini disebabkan karena inisiasi variabel, kelas, atau trait merupakan pernyataan, bukan ekspresi. Ketika pernyataan dibungkus oleh kurung kurawal, nilai kembaliannya adalah Unit.

Sekedar catatan, walaupun type inference di Scala menghemat waktu dalam aktivitas koding, namun ada baiknya definisi fungsi serta metode tetap diberikan anotasi tipe untuk tipe kembalian. Ini untuk memudahkan kolega yang turut berbagi basis kode dengan kita memahami alur kode yang kita bangun.

  • Scala memungkinkan komposisi

Scala memungkinkan developer mengomposisi beberapa perilaku kode ke dalam sebuah kelas atau objek atau perilaku lain, yang oleh Scala disimbolkan dengan trait. Terlepas dari trait, Scala masih mendukung penggunaan abstract class layaknya Java. Berikut adalah contoh penggunaan trait:

trait Hero {
val name: String
val alignment: String
val elementType: String
}
trait Lawful {
val alignment: String = "chaotic"
}
class AlterEgo(override val name: String) extends Hero with Lawful {
val elementType = "wind"
}

Kelas AlterEgo di atas dideklarasi mengomposisi trait Hero dan Lawful. Yang harus diperhatikan adalah untuk mengomposisi trait pertama digunakan kata kunci extends namun untuk mengomposisi trait-trait berikutnya cukup menggunakan with.

Di beberapa pustaka seperti Akka HTTP, trait biasa digunakan seperti ini:

/** Something that can later be marshalled into a response */
trait ToResponseMarshallable {
type T
def value: T
}

object ToResponseMarshallable {
implicit def apply[A](_value: A): ToResponseMarshallable =
new ToResponseMarshallable {
type T = A
def value: T = _value
}
}

Trait dapat langsung diinisasi tanpa melalui komposisi terlebih dahulu, seperti layaknya trait ToResponseMarshallable di atas. Sekedar diketahui, potongan kode di atas digunakan oleh Akka HTTP sebagai implementasi dari Magnet Pattern yang dapat secara ajaib mengubah domain objek aplikasi menjadi keluaran router.

  • Fungsi adalah warga kelas satu

Seperti layaknya bahasa pemrograman berparadigma fungsional pada umumnya, fungsi di Scala bisa diinisiasi, dijadikan parameter fungsi lain, dan menjadi nilai kembalian dari ekspresi. Contoh penggunaan fungsi di Scala adalah sebagai berikut:

val sumTwoInt = (v1: Int, v2: Int) => v1 + v2 
val printAndSum = (v1: Int, v2: Int) => {
println(s"v1 is ${v1}")
println(s"v2 is ${v2}")
v1 + v2
}

Contoh di atas merupakan sintaks pemanis untuk inisiasi fungsi. Kode di atas merupakan bentuk lain dari kode berikut:

val sumTwoInt: (Int, Int) => Int = new Function2[Int, Int, Int] {
def apply(v1: Int, v2: Int): Int = v1 + v2
}
val printAndSum: (Int, Int) => Int = new Function2[Int, Int, Int] {
def apply(v1: Int, v2: Int): Int = {
println(s"v1 is ${v1}")
println(s"v2 is ${v2}")
v1 + v2
}
}

Beberapa poin dari koda di atas:

  • Fungsi pada dasarnya adalah trait . Jika kita membuat fungsi dengan 3 parameter, maka trait yang akan dipilih oleh compiler adalah Function3[T1, T2, T3, R] dan seterusnya. Metode apply merupakan tempat di mana operasi dari fungsi tersebut didefinisi. Scala menyediakan trait Function0 sampai Function22 . Jika membutuhkan fungsi dengan arity lebih dari 22, developer bisa membuat sendiri.
  • Tipe dari sebuah fungsi adalah kombinasi dari parameter serta nilai kembaliannya. Untuk 2 fungsi di atas, tipe dari fungsi-fungsi tersebut adalah (Int, Int) => Int

Yang menarik dari Scala terkait cara pemanggilan fungsi adalah ketika kita menjalankan suatu fungsi. Scala akan mengubah pemanggilan fungsi menjadi pemanggilan apply . Untuk contoh di atas, sumTwoInt(1, 2) serta sumTwoInt.apply(1, 2) adalah valid dan menghasilkan nilai yang sama.

Jika ingin menggunakan fungsi sebagai parameter, cara mendefinisikannya bisa seperti berikut:

val readFile = (fileName: String, callback: File => Unit) => {
FileIO.readAsync(fileName).onComplete {
case Success(file) => callback(file)
case Failure(reason) => //BOOM
}
}
val myCallback = (file: File) => {
println(s"File content is ${file.content}")
}
readFile("/path/to/myfile.txt", myCallback)

Bisa dilihat bahwa parameter callback adalah fungsi dengan 1 parameter bertipe File dengan nilai kembalian Unit. Pada pemanggilan readFile , parameter tersebut diisi oleh variabel myCallback yang merupakan fungsi dengan tipe serupa.

Di Scala, ada perbedaan antara metode dan fungsi. Fungsi bisa bebas didefinisi dan dimasukkan ke sebuah variabel, namun metode harus terikat ke kelas, objek, atau trait . Contohnya adalah sebagai berikut:

class Square(width: Double, height: Double) {
def area: Double = width * height
def scale(num: Double) = {
new Square(num * width, num * height)
}
}
object SquareFactory {
def createSquare(width: Double, height: Double) = {
new Square(width, height)
}
}

Metode dimulai dengan kata kunci def dan diikuti dengan parameter, tipe kembalian (jika diinginkan), serta badan dari metode tersebut. Yang membedakan adalah untuk memisahkan antara parameter dan badan fungsi, fungsi menggunakan => namun def menggunakan = .

  • Scala memiliki pattern matching yang kuat

Seperti kita tahu, kita bisa menggunakan konstruksi kode menggunakan switch(n) { case ...}untuk mengevaluasi sebuah nilai dan berdasarkan ekspresi boolean di dalam case yang ada. Namun seperti layaknya bahasa pemrograman bergaya ML, pattern matching pada Scala memungkinkan tidak hanya dilakukan pada variabel, tapi juga pada struktur data. Berikut contohnya:

val list = List(1, 2, 3)
val headOption = list match {
case value :: Nil => Some(value) //*1
case value :: tail => Some(value) //*2
case Nil => None //*3
}

Pada konstruksi kode di atas, kita tidak peduli apa nilai dari masing-masing elemen di dalam list , kita hanya peduli terhadap struktur dari list .

  • *1 akan dievaluasi jika list merupakan sebuah List dengan 1 elemen (Nil merupakan objek di Scala yang melambangkan List kosong)
  • *2 akan dievaluasi jika list merupakan sebuah List dengan lebih dari 1 elemen
  • *3 akan dievaluasi jika list tidak memiliki element

Karena list memiliki lebih dari 1 elemen, maka *2 akan dieksekusi dan headOption akan menghasilkan nilai Some(1). Sebagai tambahan, di sini digunakan kelas Some[T] dan None untuk mengsimbolkan bahwa ekspresi pattern matching di atas memungkinkan nilai yang kosong. Jika tidak sangat terpaksa sekali, jangan gunakan null untuk menandakan nilai kosong, walaupun masih dimungkinkan di Scala.

  • Scala menyediakan parametric polymorphism

Layaknya Java atau Rust, Scala memungkinkan developer untuk membuat class, trait, atau function yang memiliki type parameter. Secara simpel, developer bisa menyediakan abstraksi yang tidak peduli tipe dari value yang diproses tanpa harus kehilangan informasi lengkap dari tipe tersebut (a.k.a tidak harus menggunakan Any dan mekanisme defensif seperti asInstanceOf[Int]). Contoh parametric polymorphism pada Scala adalah seperti berikut:

case class Node[T](value: T)
val intNode = Node(1)
val strNode = Node("one")
val listNode = Node(List(1))
println(s"node value: ${intNode.value}") // 1
println(s"node value: ${strNode.value}") // one
def listSize[T](list: List[T]) = list.size
println(s"listSize(List(1))") // 1
println(s"listSize(List(Node(1), Node(2)))") // 2

Yang menarik adalah type parameter di Scala bisa diisi tipe ber- type parameter. Singkat cerita:

case class Apply[F[_], T](f: T => F[T])
{
def value(ground: T): F[T] = f(ground)
}
val listConstructor = Apply[List, Int](initial => List(initial))
val optionConstructor = Apply[Option, String](initial => Option(initial))

Kelas Apply diinisiasi dengan menerima tipe List atau Option atau tipe lain yang didefinisi tipenya memiliki 1 type parameter . Fitur ini secara sekilas belum jelas kegunaannya untuk apa (haha), tapi ketika developer sudah mulai mengadopsi design pattern Monad, struktur seperti di atas banyak digunakan.

  • Abstraksi sederhana untuk operasi asinkron

Selain fitur-fitur inti di atas, salah satu yang cukup menyenangkan di Scala adalah abstraksi untuk operasi yang melibatkan aksi asinkron dan multithreading cukup mudah digunakan, yaitu menggunakan Future[T], dengan contoh sebagai berikut:

def getUserById(id: Int): Future[Option[User]] = Future {
val userOpt = DB.getUser(id)
userOpt
}
getUserById(1).onComplete {
case Success(userOpt) => // do something here
case Failure(ex) =>
// oops our access to db somehow exploded
// ex is an instance of Throwable, or from java.lang.Exception
}
//hey, how about we access several user sequentially?
//listOfOptUser will be Future[List[Option[User]]
val listOfOptUser = for {
firstUser <- getUserById(2)
secondUser <- getUserById(3)
} yield List(firstUser, secondUser)
//see that at the end of for-comprehension, we use yield to create the result
listOfOptUser.onComplete {
case Success(list) => list.size
case Failure(ex) => //BOOM. If either of getUserByIds invocation are error, they will end up here
}

Secara sederhana, Future adalah kelas yang menerima satu argumen. Argumen tersebut akan didelegasi ke threadpool. Nilai akhir yang akan diterima oleh Future adalah nilai kembalian dari ekspresi yang dijadikan argumen. Untuk contoh di atas, getUserById merupakan sebuah metode yang menjalankan operasi akses ke basis data secara asinkron karena ekspresi DB.getUserId(id) dan selanjutnya “dibungkus” oleh Future. Sekedar diketahui, walaupun Future melakukan pembungkusan ekspresi layaknya Option atau List , Future bukanlah Monad.

Selain fitur-fitur di atas, Scala menyediakan fitur-fitur tingkat lanjut yang memungkinkan developer mengakses kemampuan pemograman fungsional murni dan mengimplementasi teori kategori ke dalam basis kode. Banyak panduan serta e-book yang mengangkat topik ini seperti ini, ini, atau ini.

Jadi, apa keunggulan Scala? Berdasarkan pengalaman penulis, Scala memberikan manfaat berikut:

  • Basis kode menjadi ringkas dan ekspresif (bahkan terlalu ringkas)
  • Scala fleksibel tapi tetap aman (type-safe). Ini memudahkan developer membangun DSL yang spesifik untuk domain permasalahan yang dihadapi. Hal ini didukung oleh berbagai fitur Scala seperti trait , implicits , dan macro .
  • Untuk mempelajari Scala, tidak harus paham pemograman fungsional. Ini disebabkan oleh karena paradigma yang dianut oleh Scala adalah campuran OOP dan FP, developer tidak harus selalu taat pada asas FP. Seiring dengan berjalannya waktu, pengalaman, dan kebutuhan, developer bisa sedikit demi sedikit mengevolusi basis kodenya menjadi lebih taat ke FP, atau bahkan terjun ke bahasa lain yang memberikan fitur pemrograman fungsional yang tidak ada di Scala.

Pendapat personal, menekuni Scala memiliki banyak kelemahan, seperti tidak adanya arahan pasti bagaimana memilih dengan benar fitur-fitur dari Scala untuk menyelesaikan masalah (abstract class atau trait, misal), build tools yang kurang ramah, atau compiler yang galak tapi kadang ambigu (hai implicits), serta cekungan pembelajaran yang cukup curam untuk benar-benar memahami kegunaan dari fitur-fitur yang ada. Tapi setelah itu semua bisa dilewati, menekuni Scala memungkinkan developer untuk membuat aplikasi yang ringkas, berkinerja bagus, namun tetap stabil.


P.S. Jika teman-teman menyukai artikel semacam ini, silakan subscribe ke newsletter kita dan dapatkan notifikasi artikel terbaru langsung di inbox kamu!