evilfactorylabs

Cover image for Bersukaria dengan Gleam edisi ujung depan
Rizaldy
Rizaldy

Posted on

Bersukaria dengan Gleam edisi ujung depan

Sebelumnya saya sempat menerbitkan tulisan serupa yang bisa dikunjungi disini jika belum sempat membaca. Estimasi bacanya sekitar 14 menit, tapi bisa dilewati ke bagian yang menarik saja. Di tulisan itu saya memberi sedikit pengenalan untuk menulis Gleam di ujung belakang, dan sebagai seorang mantan pengembang frontend juga, rasanya tidak adil jika tidak membahas untuk bagian depannya juga.

Saat membuat program menggunakan bahasa pemrograman Gleam, target runtime nya per tulisan ini diterbitkan ada dua: Erlang dan JavaScript. Untuk Erlang, pengembang akan menggunakan perintah gleam export erlang-shipment untuk membuat precompiled/artefak yang nantinya akan dijalankan di BEAM dan jika targetnya JavaScript, maka akan menghasilkan kode JavaScript dengan format ES6 Module dan terserah ingin diapakan.

Tapi pertanyaan mendasarnya adalah satu: mengapa menggunakan Gleam jika ujungnya menjadi kode JavaScript juga? Meskipun kasus ini tidak terbatas di Gleam—bisa di ReScript, TypeScript, Reason, Clojure, dan masih banyak lagi—di tulisan ini saya akan mencoba untuk menjawab pertanyaan mendasar itu.

Yang mungkin bisa menjawab juga untuk bahasa pemrograman lain yang tadi saya sebut seperti ReScript, Reason dan Clojure.

Sebelum masuk ke topik inti, mari kita mulai dari topik yang paling sulit dalam pengembangan front-end: berurusan dengan state.

State this, state that

Aplikasi adalah tentang menampilkan informasi dari kumpulan data, sehingga data tersebut bisa berguna. Dan aplikasi seharusnya interaktif, karena jika tidak, tidak ada bedanya dengan dokumen biasa yang sama-sama menampilkan informasi.

Kumpulan data ini diterima dari sebuah "sistem" yang populer disebut backend. Backend bertugas untuk mengatur permintaan pengguna aplikasi, memberikan data yang diinginkan, sekaligus menolaknya jika harus. Aplikasi seperti ini menggunakan model client-server, meskipun di beberapa kasus, peran server tidak diperlukan: seperti aplikasi kalkulator ataupun aplikasi lain yang menggunakan model peer-to-peer misalnya seperti kalkulator. Bercanda, seperti aplikasi perpesanan instan misalnya yang hanya membutuhkan peran server sebagai "jembatan" untuk menghubungkan dua atau lebih peer (klien).

Lalu di mana letak tantangannya?

Cukup beragam. Misal, di aplikasi kalkulator. Oke oke misal di aplikasi dasbor dan terdapat kolom pencarian. Saat pengguna mengetik di kolom tersebut, pengguna mengharapkan apa yang dia ketik muncul, dan jika tidak, mengharapkan sebuah informasi alasan tidak munculnya.

Dan, uh, pengguna diyakini tidak sabaran dan sering kali membutuhkan distraksi saat proses menunggu.

Berdasarkan kasus diatas, setidaknya ada 5 kondisi yang harus kita pikirkan:

  • Saat data diminta
  • Saat data berhasil diterima
  • Saat data gagal diterima
  • Saat data diterima sesuai harapan
  • Saat data diterima tidak sesuai harapan

Yang pertama, ini paling umum: loading. Biasanya kita menambahkan spinner ataupun skeleton element untuk mendistraksi si pengguna.

Yang kedua & ketiga, ini umum juga: fulfilled/failed. Rentang status code ada di 200-530. Jika diatas 500, biasanya bisa melakukan retry, dan http client library yang digunakan biasanya sudah mengaturnya. Tentu jika membuat http client sendiri karena anti importir engineer club seharusnya sudah memikirkan ini juga :))

Yang keempat, ini masih umum. Data yang diterima sesuai harapan sehingga masalah undefined tidak ditemukan.

Yang kelima, tidak terlalu umum. Di kasus yang mantap ada yang menaruh status code di response body atau juga karena response yang diberikan tidak sesuai dengan kontrak yang disetujui sebelumnya.

Pengembang front-end harus mengatur itu semua, tapi, tidak perlu adu nasib. Backend pun melakukan hal serupa namun mungkin tidak serumit di depan.

Baiklah, dari berbagai pola yang digunakan dalam membuat UI, rumus paling populer adalah seperti ini:

UI = fn(state)
Enter fullscreen mode Exit fullscreen mode

Rumus ini mungkin dipopulerkan oleh Guillermo Rauch di Pure UI nya atau oleh Karolis Narkevicius di UI as a function of data nya, entahlah, kita sedang tidak membahas sejarah.

Ide nya sederhana: UI hanyalah sebuah function yang menerima data dan mengembalikan tampilan sebagai keluarannya, misal seperti ini:

function FeedView(state) {
  if (state.isFetching) <Spinner />
  if (state.isFulfilled) <Feed state={state} />
  if (state.isFailed) <FeedError state={state} />
}
Enter fullscreen mode Exit fullscreen mode

Yang menjadi masalah adalah bagaimana state diatur dan mengalir, dan ada belasan (jika bukan puluhan) cara. Yang cukup populer adalah menggunakan pola Flux, yang intinya kurang lebih menggunakan paradigma reactive programming. Jika menggunakan Redux, Mobx, Vuex, Pinia, dsb — yes, mereka pun menggunakan paradigma itu.

Berbekal rumus UI = fn(state), khususnya di pengembangan frontend modern, kemungkinan waktu kita akan dihabiskan banyak dalam menyusun UI dan mengatur state.

Dan semoga produktif serta terasa menyenangkan! *sambil flashback saat masih menjadi frontend engineer*

The Elm Architecture (TEA)

Berbicara jika UI = fn(state) tentu tidak adil jika tidak membahas efek samping alias side-effect. Ini menjadi salah satu alasan inti mengapa state harus berubah.

Di beberapa aplikasi, kita menggunakan event emitter dengan sebuah konsep bernama component lifecycle. Ingat componentDidMount dan componentWillMount untuk flashback? Atau ada mounted dan beforeDestroy juga bila di framework lain.

Di UI library modern, kita menggunakan pendekatan effect yang populer digunakan untuk berinteraksi dengan "sistem external". Dan ini contoh yang saya ambil dari dokumentasi resmi React dengan sedikit adjustment di spacing dan penggunaan semicolon:

useEffect(() => {
  async function startFetching() {
    setBio(null)

    const result = await fetchBio(person)

    if (!ignore) {
      setBio(result)
    }
  }

  let ignore = false

  startFetching()

  return () => {
    ignore = true
  }
}, [person])
Enter fullscreen mode Exit fullscreen mode

Dari kode diatas, kita tahu jika:

  • effect dijalankan bila state person berubah
  • state person berubah saat terjadi perubahan di input <select>

Variable ignore diatas hanya sebagai contoh untuk menghindari race condition (mengambil dari halaman dokumentasinya).

Siapa yang bisa mengubah state person? Secara teknis, siapapun. Mungkin input dari select mungkin perubahan di url bar — tergantung bagaimana membuatnya.

Di kasus nyata, umumnya kita akan menggunakan state manager karena yakin state yang akan kita atur nanti cukup kompleks. Beruntung jika menggunakan library state manager yang battle-tested dan wished you the best of luck jika membuat state manager mandiri.

Dari kode di atas, itu relevan jika state tersebut hanya digunakan di komponen tersebut (atau yang dibawahnya). Bagaimana jika state tersebut pun digunakan di komponen lain yang se-level atau mungkin diatasnya?

Tentu saja dengan memindahkankan effectnya! Tapi menyimpan state disini dan disana cukup ribet, dan dengan menghindari penggunaan state manager yang ada kita akan berakhir dengan membuat state manager sendiri. Konsep state manager ini sederhananya adalah: buat state menjadi global dan tersimpan di satu tempat. Untuk mengambilnya, nilainya bisa menggunakan API tertentu seperti .getState() dan untuk mengubahnya bisa menggunakan API tertentu juga seperti .dispatch({ type: 'setBio', data }) dan biarkan reducer yang mengaturnya.

Pola ini dipopulerkan oleh Elm yang menggunakan konsep "Model View Update" yang bisa merujuk ke The Elm Architecture yang sederhananya:

  1. view menampilkan tampilan berdasarkan data yang sudah dibentuk
  2. view memberitahu runtime untuk melakukan perubahan (update) data lama dengan data baru (model)
  3. runtime memberitahu view untuk menampilkan tampilan berdasarkan data yang sudah dibentuk (kembali ke step 1)

Alurnya terus begitu dan selalu begitu. Pertanyaannya, mengapa ini menarik?

  • Dalam segi simplicity, disini separation of concerns nya cukup jelas
  • Dalam segi predictability, update adalah pure function yang memproses state yang ada lalu membuat state baru
  • Dalam segi scalability, well, ini cukup subjektif. Tapi cukup fokus dengan tiga pilar utama: model, update, view.

Di kode Elm sederhana, misal seperti ini:

import Browser

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

-- MAIN

main =
  Browser.sandbox {
    init = init
    , update = update
    , view = view
  }

-- MODEL

type alias Model =
  { thing: String, anotherThing: String }

init : Model
init =
  { thing = "world", anotherThing = " " }

-- UPDATE

type Msg =
  UpdateWorld String

update : Msg -> Model -> Model
update msg model =
  case msg of
    UpdateWorld newThing ->
      ({ model | thing = newThing, anotherThing = "... " })

-- VIEW

view : Model -> Html Msg
view model =
  div []
    [div [] [text ("hello" ++ model.anotherThing ++ model.thing)]
    , (buttonOfLife model)
    ]

buttonOfLife: Model -> Html Msg
buttonOfLife model =
  if model.thing == "world" then
    button [onClick (UpdateWorld "my world?")] [text "click me"]
  else
    div [] []
Enter fullscreen mode Exit fullscreen mode

Mungkin sintaks nya terlihat sedikit memusingkan begitu pula dengan penamaan variable ataupun untuk update nya. Tapi gambarannya seperti itu! Kode diatas kita belum menyertakan "side-effect" karena nantinya akan dibahas di topik utama kita, jika Elm architecture diatas sudah terlihat masuk akal, mari kita mulai membahas pemeran utama kita!

Lustre

Ladies and gentlemen, I present to you: Lustre.

Lustre adalah jawaban untuk yang menginginkan Elm di Gleam. Mengapa tidak membawa React ke Gleam aja, pak ustadz? Well, tentu, jika memang memiliki alasan sendiri mengapa menginginkan React di Gleam. Mungkin seperti Reason dan/atau ReScript yang mencoba untuk terlihat ataupun bertindak seperti React.

Saya pribadi tidak mengharapkan ada "seperti React" di Gleam, atau di Swift, atau di Rust. Biarlah React berada di dunianya sendiri :D

Berikan saya waktu 5 menit untuk memperkenalkan Lustre, lalu silahkan nilai sendiri apakah merasa cocok dengan Lustre. Pertama, saya akan mengubah kode Elm sebelumnya menjadi Lustre di Gleam, dan kurang lebih seperti ini:

import lustre
import lustre/effect.{type Effect}
import lustre/element.{type Element}

import lustre/element/html.{button, div, text}
import lustre/event.{on_click}

// -- INIT

pub fn main() {
  let app = lustre.application(init, update, view)

  let assert Ok(_) = lustre.start(app, "#app", Nil)

  Nil
}

fn init(_) -> #(Model, Effect(Msg)) {
  #(Model(thing: "world", another_thing: " "), effect.none())
}

// -- MODEL

pub type Model {
  Model(thing: String, another_thing: String)
}

// -- UPDATE

pub type Msg {
  UpdateWorld(String)
}

fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    UpdateWorld(world) -> #(
      Model(..model, thing: world, another_thing: "... "),
      effect.none(),
    )
  }
}

// -- VIEW

fn view(model: Model) -> Element(Msg) {
  div([], [
    div([], [text("hello" <> model.another_thing <> model.thing)]),
    button_of_life(model),
  ])
}

fn button_of_life(model: Model) -> Element(Msg) {
  case model.thing {
    "world" -> button([on_click(UpdateWorld("my world?"))], [text("click me")])
    _ -> div([], [])
  }
}
Enter fullscreen mode Exit fullscreen mode

Bukankah sangat mirip? Menurut saya pribadi sintaks yang Gleam lebih mudah dipahami dari yang Elm, YMMV.

Mungkin sintaks diatas terlihat sedikit... familiar? Pernah menggunakan React.createElement('div', {}, []) saat Webpack masih keren?

The Elm Architecture (TEA) ini cukup banyak memberi pengaruh, dari charmbracelet/bubbletea, Redux, iced-rs/iced, dan tentu saja Lustre itu sendiri. Yang berarti, cukup aman untuk dikatakan jika pendekatan/pola TEA ini dapat diandalkan.

Jika merasa terjual, kita akan lanjut ke sedikit perkenalan terkait Lustre untuk kasus nyata yang umum digunakan.

SPA di Host Springs Lustre

Untuk mempermudah, kita akan menggunakan model Single Page Application (SPA) alias rendering & routing dilakukan client-side — meskipun Lustre mendukung SSR dan "Server Components" yang masih WIP.

Untuk memulai, bisa merujuk ke halaman ini. Pastikan menggunakan lustre_dev_tools juga untuk mendapatkan fitur live reload dan dukungan TailwindCSS.

Kasus umum untuk aplikasi frontend adalah berbicara ke dunia luar dan bertransaksi untuk mendapatkan sesuatu bernama JSON yang nantinya akan ditampilkan dalam bentuk format yang lebih indah daripada sesuatu itu. Untuk bahan percobaan, salah satu yang menjadi favorit pengembang frontend adalah Starwars API dan mari kita gunakan itu.

Kita mulai dari endpoint /people, per tulisan ini diterbitkan, skema dari response nya adalah seperti ini:

[
  {
    "name": "Luke Skywalker",
    "height": "172",
    "mass": "77",
    "hair_color": "blond",
    "skin_color": "fair",
    "eye_color": "blue",
    "birth_year": "19BBY",
    "gender": "male",
    "homeworld": "https://swapi.info/api/planets/1",
    "films": [
      "https://swapi.info/api/films/1",
      "https://swapi.info/api/films/2",
      "https://swapi.info/api/films/3",
      "https://swapi.info/api/films/6"
    ],
    "species": [],
    "vehicles": [
      "https://swapi.info/api/vehicles/14",
      "https://swapi.info/api/vehicles/30"
    ],
    "starships": [
      "https://swapi.info/api/starships/12",
      "https://swapi.info/api/starships/22"
    ],
    "created": "2014-12-09T13:50:51.644000Z",
    "edited": "2014-12-20T21:17:56.891000Z",
    "url": "https://swapi.info/api/people/1"
  }
]
Enter fullscreen mode Exit fullscreen mode

Karena data di atas berasal dari luar Gleam, kita perlu melakukan decoding karena pada akhirnya kita akan menyimpan data tersebut di Gleam.

Melihat ke skema nya, data di atas dapat direpresentasikan menggunakan Records seperti ini:

pub type People {
  People(
    name: String,
    height: String,
    mass: String,
    hair_color: String,
    skin_color: String,
    eye_color: String,
    birth_year: String,
    gender: String,
    homeworld: String,
    films: List(String),
    species: List(String),
    vehicles: List(String),
    starships: List(String),
    created: String,
    edited: String,
    url: String,
  )
}
Enter fullscreen mode Exit fullscreen mode

Sebagai contoh, untuk si Luke, bisa dibuat seperti ini:

let luke =
    People(
      name: "Luke Skywalker",
      height: "172",
      mass: "77",
      hair_color: "blond",
      skin_color: "fair",
      eye_color: "blue",
      birth_year: "19BBY",
      gender: "male",
      homeworld: "https://swapi.info/api/planets/1",
      films: [
        "https://swapi.info/api/films/1", "https://swapi.info/api/films/2",
        "https://swapi.info/api/films/3", "https://swapi.info/api/films/6",
      ],
      species: [],
      vehicles: [
        "https://swapi.info/api/vehicles/14",
        "https://swapi.info/api/vehicles/30",
      ],
      starships: [
        "https://swapi.info/api/starships/12",
        "https://swapi.info/api/starships/22",
      ],
      created: "2014-12-09T13:50:51.644000Z",
      edited: "2014-12-20T21:17:56.891000Z",
      url: "https://swapi.info/api/people/1",
    )
Enter fullscreen mode Exit fullscreen mode

Lalu kita perlu membuat decoder nya, dan ini bagian menariknya: field diatas berjumlah 16 dan untuk melakukan decode, yang paling mendekati adalah dynamic.decode9. Yang berarti, sisa 7 nya perlu kita lakukan mandiri :))

Dan berikut decodernya:

fn decode_starwars_people(
  data: dynamic.Dynamic,
) -> Result(People, List(dynamic.DecodeError)) {
  let decode_people =
    dynamic.decode9(
      fn(
        name: String,
        height: String,
        mass: String,
        hair_color: String,
        skin_color: String,
        eye_color: String,
        birth_year: String,
        gender: String,
        home_world: String,
      ) {
        fn(
          films: List(String),
          species: List(String),
          vehicles: List(String),
          starships: List(String),
          created: String,
          edited: String,
          url: String,
        ) {
          People(
            name,
            height,
            mass,
            hair_color,
            skin_color,
            eye_color,
            birth_year,
            gender,
            home_world,
            films,
            species,
            vehicles,
            starships,
            created,
            edited,
            url,
          )
        }
      },
      dynamic.field("name", dynamic.string),
      dynamic.field("height", dynamic.string),
      dynamic.field("mass", dynamic.string),
      dynamic.field("hair_color", dynamic.string),
      dynamic.field("skin_color", dynamic.string),
      dynamic.field("eye_color", dynamic.string),
      dynamic.field("birth_year", dynamic.string),
      dynamic.field("gender", dynamic.string),
      dynamic.field("homeworld", dynamic.string),
    )(data)

  case decode_people {
    Ok(rest) -> decode_rest(data, rest)
    Error(e) -> Error(e)
  }
}

fn decode_rest(
  data: dynamic.Dynamic,
  handler: fn(
    List(String),
    List(String),
    List(String),
    List(String),
    String,
    String,
    String,
  ) ->
    People,
) -> Result(People, List(dynamic.DecodeError)) {
  case
    dynamic.field("films", dynamic.list(dynamic.string))(data),
    dynamic.field("species", dynamic.list(dynamic.string))(data),
    dynamic.field("vehicles", dynamic.list(dynamic.string))(data),
    dynamic.field("starships", dynamic.list(dynamic.string))(data),
    dynamic.field("created", dynamic.string)(data),
    dynamic.field("edited", dynamic.string)(data),
    dynamic.field("url", dynamic.string)(data)
  {
    Ok(films),
      Ok(species),
      Ok(vehicles),
      Ok(starships),
      Ok(created),
      Ok(edited),
      Ok(url)
    -> Ok(handler(films, species, vehicles, starships, created, edited, url))
    Error(e), _, _, _, _, _, _ -> Error(e)
    _, Error(e), _, _, _, _, _ -> Error(e)
    _, _, Error(e), _, _, _, _ -> Error(e)
    _, _, _, Error(e), _, _, _ -> Error(e)
    _, _, _, _, Error(e), _, _ -> Error(e)
    _, _, _, _, _, Error(e), _ -> Error(e)
    _, _, _, _, _, _, Error(e) -> Error(e)
  }
}

fn decode_starwars_people_response(
  data: dynamic.Dynamic,
) -> Result(List(People), List(dynamic.DecodeError)) {
  dynamic.list(decode_starwars_people)(data)
}
Enter fullscreen mode Exit fullscreen mode

HAHAHA. Beautiful (menggunakan nada pegasus).

Saya belum menemukan cara "elegan" untuk menghadapi kasus diatas, dan jika memiliki, please let me know!

Sekarang, kita mulai ritual nya. Disini saya akan menggunakan lustre_http sebagai HTTP client nya dan ingin memanggil endpoint tersebut ketika init, jadi kodenya seperti ini:

fn get_starwars_people() -> Effect(Msg) {
  let endpoint = "https://swapi.info/api/people"

  lustre_http.get(
    endpoint,
    lustre_http.expect_json(
      decode_starwars_people_response,
      SWAPIPeopleReturned,
    ),
  )
}

fn init(_) -> #(Model, Effect(Msg)) {
  #(Model(starwars_people: []), get_starwars_people())
}

// -- MODEL

pub type Model {
  Model(starwars_people: List(People))
}

// -- UPDATE

pub type Msg {
  SWAPIPeopleReturned(Result(List(People), HttpError))
}

fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    SWAPIPeopleReturned(Ok(people)) -> #(
      Model(starwars_people: people),
      effect.none(),
    )
    SWAPIPeopleReturned(Error(_)) -> #(Model(..model), effect.none())
  }
}
Enter fullscreen mode Exit fullscreen mode

Dan mari membuat sedikit UI nya:

// -- VIEW

fn view(model: Model) -> Element(Msg) {
  div([class("min-h-screen bg-gray-800 lg:p-8")], [
    html.h1([class("text-center text-8xl text-yellow-500 font-bold mb-10")], [
      text("the people"),
    ]),
    div(
      [class("flex flex-wrap justify-center items-start")],
      model.starwars_people
        |> list.map(fn(people) { starwars_people_view(people) }),
    ),
  ])
}

fn starwars_people_view(people: People) -> Element(Msg) {
  div([class("max-w-sm rounded-lg overflow-hidden bg-gray-700 p-6 m-4")], [
    div([class("font-bold text-2xl mb-3 text-yellow-500")], [text(people.name)]),
    div([class("uppercase font-bold text-gray-300 text-lg")], [
      text(people.gender),
    ]),
  ])
}
Enter fullscreen mode Exit fullscreen mode

Dan pratinjau nya:

Image description

Yeah bukan sesuatu yang bisa dibanggakan haha.

Sekarang kita buat kondisi bagaimana jika response nya somehow berisi array kosong? Atau terjadi error? Atau mungkin skema nya berubah?

Karena kita sudah bermain kondisi, mari kita tambahkan "state" nya:

fn init(_) -> #(Model, Effect(Msg)) {
  #(
    Model(starwars_people: [], starwars_people_status: PeopleLoading),
    get_starwars_people(),
  )
}

// -- MODEL

pub type SWAPIStatus {
  PeopleLoading
  PeopleEmpty
  PeopleFulfilled
  PeopleError
}

pub type Model {
  Model(starwars_people: List(People), starwars_people_status: SWAPIStatus)
}
Enter fullscreen mode Exit fullscreen mode

Lalu di update nya:

// -- UPDATE

fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    SWAPIPeopleReturned(Ok(people)) ->
      case people {
        [] -> #(
          Model(..model, starwars_people_status: PeopleEmpty),
          effect.none(),
        )
        _ -> #(
          Model(
            starwars_people: people,
            starwars_people_status: PeopleFulfilled,
          ),
          effect.none(),
        )
      }

    SWAPIPeopleReturned(Error(_)) -> #(
      Model(..model, starwars_people_status: PeopleError),
      effect.none(),
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Dan view nya:

// -- VIEW

fn view(model: Model) -> Element(Msg) {
  div([class("min-h-screen bg-gray-800 lg:p-8")], [
    html.h1([class("text-center text-8xl text-yellow-500 font-bold mb-10")], [
      text("the people"),
    ]),
    div([class("flex flex-wrap justify-center items-start")], case
      model.starwars_people_status
    {
      PeopleLoading -> [starwars_people_view_loading()]
      PeopleFulfilled ->
        model.starwars_people
        |> list.map(fn(people) { starwars_people_view(people) })
      PeopleEmpty -> [starwars_people_view_empty()]
      PeopleError -> [starwars_people_view_error()]
    }),
  ])
}

fn starwars_people_view_empty() -> Element(Msg) {
  div([class("text-white text-2xl font-bold")], [text("where's everyone???")])
}

fn starwars_people_view_error() -> Element(Msg) {
  div([class("text-red-700 text-2xl font-bold")], [
    text("something went wrong with the people"),
  ])
}

fn starwars_people_view_loading() -> Element(Msg) {
  div([class("text-white")], [text("loading")])
}

fn starwars_people_view(person: People) -> Element(Msg) {
  div([class("max-w-sm rounded-lg overflow-hidden bg-gray-700 p-6 m-4")], [
    div([class("font-bold text-2xl mb-3 text-yellow-500")], [text(person.name)]),
    div([class("uppercase font-bold text-gray-300 text-lg")], [
      text(person.gender),
    ]),
  ])
}
Enter fullscreen mode Exit fullscreen mode

Untuk kode lengkapnya seperti ini:

import lustre

import gleam/dynamic
import gleam/list

import lustre/effect.{type Effect}
import lustre/element.{type Element}
import lustre_http.{type HttpError}

import lustre/attribute.{class}
import lustre/element/html.{div, text}

// -- MAIN

fn get_starwars_people() -> Effect(Msg) {
  let endpoint = "https://swapi.info/api/people"

  lustre_http.get(
    endpoint,
    lustre_http.expect_json(
      decode_starwars_people_response,
      SWAPIPeopleReturned,
    ),
  )
}

pub fn main() {
  let app = lustre.application(init, update, view)

  let assert Ok(_) = lustre.start(app, "#app", Nil)

  Nil
}

fn init(_) -> #(Model, Effect(Msg)) {
  #(
    Model(starwars_people: [], starwars_people_status: PeopleLoading),
    get_starwars_people(),
  )
}

// -- MODEL

pub type SWAPIStatus {
  PeopleLoading
  PeopleEmpty
  PeopleFulfilled
  PeopleError
}

pub type People {
  People(
    name: String,
    height: String,
    mass: String,
    hair_color: String,
    skin_color: String,
    eye_color: String,
    birth_year: String,
    gender: String,
    homeworld: String,
    films: List(String),
    species: List(String),
    vehicles: List(String),
    starships: List(String),
    created: String,
    edited: String,
    url: String,
  )
}

pub type Model {
  Model(starwars_people: List(People), starwars_people_status: SWAPIStatus)
}

// -- UPDATE

pub type Msg {
  SWAPIPeopleReturned(Result(List(People), HttpError))
}

fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    SWAPIPeopleReturned(Ok(people)) ->
      case people {
        [] -> #(
          Model(..model, starwars_people_status: PeopleEmpty),
          effect.none(),
        )
        _ -> #(
          Model(
            starwars_people: people,
            starwars_people_status: PeopleFulfilled,
          ),
          effect.none(),
        )
      }

    SWAPIPeopleReturned(Error(_)) -> #(
      Model(..model, starwars_people_status: PeopleError),
      effect.none(),
    )
  }
}

// -- VIEW

fn view(model: Model) -> Element(Msg) {
  div([class("min-h-screen bg-gray-800 lg:p-8")], [
    html.h1([class("text-center text-8xl text-yellow-500 font-bold mb-10")], [
      text("the people"),
    ]),
    div([class("flex flex-wrap justify-center items-start")], case
      model.starwars_people_status
    {
      PeopleLoading -> [starwars_people_view_loading()]
      PeopleFulfilled ->
        model.starwars_people
        |> list.map(fn(people) { starwars_people_view(people) })
      PeopleEmpty -> [starwars_people_view_empty()]
      PeopleError -> [starwars_people_view_error()]
    }),
  ])
}

fn starwars_people_view_empty() -> Element(Msg) {
  div([class("text-white text-2xl font-bold")], [text("where's everyone???")])
}

fn starwars_people_view_error() -> Element(Msg) {
  div([class("text-red-700 text-2xl font-bold")], [
    text("something went wrong with the people"),
  ])
}

fn starwars_people_view_loading() -> Element(Msg) {
  div([class("text-white")], [text("loading")])
}

fn starwars_people_view(person: People) -> Element(Msg) {
  div([class("max-w-sm rounded-lg overflow-hidden bg-gray-700 p-6 m-4")], [
    div([class("font-bold text-2xl mb-3 text-yellow-500")], [text(person.name)]),
    div([class("uppercase font-bold text-gray-300 text-lg")], [
      text(person.gender),
    ]),
  ])
}

fn decode_starwars_people(
  data: dynamic.Dynamic,
) -> Result(People, List(dynamic.DecodeError)) {
  let decode_people =
    dynamic.decode9(
      fn(
        name: String,
        height: String,
        mass: String,
        hair_color: String,
        skin_color: String,
        eye_color: String,
        birth_year: String,
        gender: String,
        home_world: String,
      ) {
        fn(
          films: List(String),
          species: List(String),
          vehicles: List(String),
          starships: List(String),
          created: String,
          edited: String,
          url: String,
        ) {
          People(
            name,
            height,
            mass,
            hair_color,
            skin_color,
            eye_color,
            birth_year,
            gender,
            home_world,
            films,
            species,
            vehicles,
            starships,
            created,
            edited,
            url,
          )
        }
      },
      dynamic.field("name", dynamic.string),
      dynamic.field("height", dynamic.string),
      dynamic.field("mass", dynamic.string),
      dynamic.field("hair_color", dynamic.string),
      dynamic.field("skin_color", dynamic.string),
      dynamic.field("eye_color", dynamic.string),
      dynamic.field("birth_year", dynamic.string),
      dynamic.field("gender", dynamic.string),
      dynamic.field("homeworld", dynamic.string),
    )(data)

  case decode_people {
    Ok(rest) -> decode_rest(data, rest)
    Error(e) -> Error(e)
  }
}

fn decode_rest(
  data: dynamic.Dynamic,
  handler: fn(
    List(String),
    List(String),
    List(String),
    List(String),
    String,
    String,
    String,
  ) ->
    People,
) -> Result(People, List(dynamic.DecodeError)) {
  case
    dynamic.field("films", dynamic.list(dynamic.string))(data),
    dynamic.field("species", dynamic.list(dynamic.string))(data),
    dynamic.field("vehicles", dynamic.list(dynamic.string))(data),
    dynamic.field("starships", dynamic.list(dynamic.string))(data),
    dynamic.field("created", dynamic.string)(data),
    dynamic.field("edited", dynamic.string)(data),
    dynamic.field("url", dynamic.string)(data)
  {
    Ok(films),
      Ok(species),
      Ok(vehicles),
      Ok(starships),
      Ok(created),
      Ok(edited),
      Ok(url)
    -> Ok(handler(films, species, vehicles, starships, created, edited, url))
    Error(e), _, _, _, _, _, _ -> Error(e)
    _, Error(e), _, _, _, _, _ -> Error(e)
    _, _, Error(e), _, _, _, _ -> Error(e)
    _, _, _, Error(e), _, _, _ -> Error(e)
    _, _, _, _, Error(e), _, _ -> Error(e)
    _, _, _, _, _, Error(e), _ -> Error(e)
    _, _, _, _, _, _, Error(e) -> Error(e)
  }
}

fn decode_starwars_people_response(
  data: dynamic.Dynamic,
) -> Result(List(People), List(dynamic.DecodeError)) {
  dynamic.list(decode_starwars_people)(data)
}
Enter fullscreen mode Exit fullscreen mode

Menyimpan semua kode di satu berkas tentu bukanlah pilihan yang bijak, nantinya kita akan coba melakukan sedikit refactor!

Fearless Refactor

Pertama, perihal selera (preferensi). Untuk struktur direktori saya senangnya mengaturnya seperti ini:

src
├── components
├── data
├── effects
├── pages
└── views
Enter fullscreen mode Exit fullscreen mode

Dengan catatan:

  • components berisi reusable components seperti Navbar, Card dsb yang return nya Element(t)
  • data berisi hal-hal yang bersifat... data. Model, msg, dsb yang return nya Model
  • effects berisi hal-hal yang return nya Effect(Msg) termasuk pemanggilan jaringan
  • pages untuk menyimpan top-level module yang berkaitan dengan halaman tertentu seperti Home, StaticPage dsb yang return nya Element(Msg)
  • views ini lebih seperti ke fragments. Misal layout, FeedView, dan terkadang component-component kecil yang bersifat "state" seperti Loading, Empty, Error dsb yang return nya Element(t) juga

Pendekatan diatas tujuannya (tujuan saya) biar tidak terlalu memusingkan taro ini di mana, cukup merujuk ke return nya, dan that's it.

Ada file-file yang biasanya saya taruh di top-level (src) seperti update.gleam untuk menampung segala hal yang return nya #(Model, Effect(Msg)), hal-hal yang bersifat FFI, dan tentu saja utils kebanggaan kita.

Pertama, kita memindahkan Model. Ini yang paling mudah:

// data/model.gleam

pub type SWAPIStatus {
  PeopleLoading
  PeopleEmpty
  PeopleFulfilled
  PeopleError
}

pub type People =
  List(Person)

pub type Person {
  Person(
    name: String,
    height: String,
    mass: String,
    hair_color: String,
    skin_color: String,
    eye_color: String,
    birth_year: String,
    gender: String,
    homeworld: String,
    films: List(String),
    species: List(String),
    vehicles: List(String),
    starships: List(String),
    created: String,
    edited: String,
    url: String,
  )
}

pub type Model {
  Model(starwars_people: People, starwars_people_status: SWAPIStatus)
}
Enter fullscreen mode Exit fullscreen mode

Disini sekaligus mengubah nama record People menjadi Person agar tidak membingungkan sekaligus sepertinya lebih relevan dan People menjadi List(Person) sebagai alias.

Umumnya model.gleam tidak memiliki dependensi (kecuali stdlib) untuk menghindari circular deps.

Lalu msg yang akan selalu bertambah:

// data/msg.gleam

import lustre_http.{type HttpError}

import data/model.{type People}

pub type Msg {
  SWAPIPeopleReturned(Result(People, HttpError))
}
Enter fullscreen mode Exit fullscreen mode

Dilanjutkan effect yang diawali dengan api.gleam:

// api.gleam

import gleam/dynamic
import lustre_http

import lustre/effect.{type Effect}

import data/model.{type People, type Person}
import data/msg.{type Msg, SWAPIPeopleReturned}

pub fn get_starwars_people() -> Effect(Msg) {
  let endpoint = "https://swapi.info/api/people"

  lustre_http.get(
    endpoint,
    lustre_http.expect_json(
      decode_starwars_people_response,
      SWAPIPeopleReturned,
    ),
  )
}

fn decode_starwars_people(
  data: dynamic.Dynamic,
) -> Result(Person, List(dynamic.DecodeError)) {
  let decode_people =
    dynamic.decode9(
      fn(
        name: String,
        height: String,
        mass: String,
        hair_color: String,
        skin_color: String,
        eye_color: String,
        birth_year: String,
        gender: String,
        home_world: String,
      ) {
        fn(
          films: List(String),
          species: List(String),
          vehicles: List(String),
          starships: List(String),
          created: String,
          edited: String,
          url: String,
        ) {
          model.new_person(
            name,
            height,
            mass,
            hair_color,
            skin_color,
            eye_color,
            birth_year,
            gender,
            home_world,
            films,
            species,
            vehicles,
            starships,
            created,
            edited,
            url,
          )
        }
      },
      dynamic.field("name", dynamic.string),
      dynamic.field("height", dynamic.string),
      dynamic.field("mass", dynamic.string),
      dynamic.field("hair_color", dynamic.string),
      dynamic.field("skin_color", dynamic.string),
      dynamic.field("eye_color", dynamic.string),
      dynamic.field("birth_year", dynamic.string),
      dynamic.field("gender", dynamic.string),
      dynamic.field("homeworld", dynamic.string),
    )(data)

  case decode_people {
    Ok(rest) -> decode_rest(data, rest)
    Error(e) -> Error(e)
  }
}

fn decode_rest(
  data: dynamic.Dynamic,
  handler: fn(
    List(String),
    List(String),
    List(String),
    List(String),
    String,
    String,
    String,
  ) ->
    Person,
) -> Result(Person, List(dynamic.DecodeError)) {
  case
    dynamic.field("films", dynamic.list(dynamic.string))(data),
    dynamic.field("species", dynamic.list(dynamic.string))(data),
    dynamic.field("vehicles", dynamic.list(dynamic.string))(data),
    dynamic.field("starships", dynamic.list(dynamic.string))(data),
    dynamic.field("created", dynamic.string)(data),
    dynamic.field("edited", dynamic.string)(data),
    dynamic.field("url", dynamic.string)(data)
  {
    Ok(films),
      Ok(species),
      Ok(vehicles),
      Ok(starships),
      Ok(created),
      Ok(edited),
      Ok(url)
    -> Ok(handler(films, species, vehicles, starships, created, edited, url))
    Error(e), _, _, _, _, _, _ -> Error(e)
    _, Error(e), _, _, _, _, _ -> Error(e)
    _, _, Error(e), _, _, _, _ -> Error(e)
    _, _, _, Error(e), _, _, _ -> Error(e)
    _, _, _, _, Error(e), _, _ -> Error(e)
    _, _, _, _, _, Error(e), _ -> Error(e)
    _, _, _, _, _, _, Error(e) -> Error(e)
  }
}

fn decode_starwars_people_response(
  data: dynamic.Dynamic,
) -> Result(People, List(dynamic.DecodeError)) {
  dynamic.list(decode_starwars_people)(data)
}
Enter fullscreen mode Exit fullscreen mode

Untuk penempatan si decoder cukup fleksibel, yang penting ga bingung harus nyari kemana haha.

Lalu kita ke update (sekaligus si data/model.gleam dan nambahin new_person juga) buat nambahin si init:

// update.gleam

import gleam/pair

import lustre/effect.{type Effect}

import data/model.{type Model}
import data/msg.{type Msg}

import effects/api

pub fn init() -> #(Model, Effect(Msg)) {
  pair.new(model.init(), api.get_starwars_people())
}

// data/model.gleam

pub fn new_person(
  name: String,
  height: String,
  mass: String,
  hair_color: String,
  skin_color: String,
  eye_color: String,
  birth_year: String,
  gender: String,
  home_world: String,
  films: List(String),
  species: List(String),
  vehicles: List(String),
  starships: List(String),
  created: String,
  edited: String,
  url: String,
) -> Person {
  Person(
    name,
    height,
    mass,
    hair_color,
    skin_color,
    eye_color,
    birth_year,
    gender,
    home_world,
    films,
    species,
    vehicles,
    starships,
    created,
    edited,
    url,
  )
}

pub fn init() -> Model {
  Model(starwars_people: [], starwars_people_status: PeopleLoading)
}
Enter fullscreen mode Exit fullscreen mode

Perlu diketahui bila Gleam melakukan type inference jadi menulis "type signature/annotation" di return seharusnya tidak diwajibkan kecuali di bagian parameter agar tidak ambigu.

Lalu bagian init di main kita sekarang menjadi seperti ini:

fn init(_) -> #(Model, Effect(Msg)) {
  update.init()
}
Enter fullscreen mode Exit fullscreen mode

Yeaa initialization pada dasarnya sebuah update, kan?

Dan terakhir, bagian si update di main:

fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    msg.SWAPIPeopleReturned(Ok(people)) -> update.set_starwars_people(people)
    msg.SWAPIPeopleReturned(Error(_)) -> update.set_starwars_people_error()
  }
}
Enter fullscreen mode Exit fullscreen mode

Daan perlu memperbarui data/model.gleam dan update.gleam kita:

// data/model.gleam

/// buyer alert: idealnya klo model udah kompleks
/// perlu bawa bawa existing state seperti ini: Model(..model, starwars_people:, starwars_people_status:)
/// agar tidak ter-override
pub fn set_starwars_people(
  starwars_people: People,
  starwars_people_status: SWAPIStatus,
) {
  Model(starwars_people:, starwars_people_status:)
}

/// buyer alert: idealnya klo model udah kompleks
/// perlu bawa bawa existing state seperti ini: Model(..model, starwars_people:, starwars_people_status:)
/// agar tidak ter-override
pub fn starwars_people_error() {
  Model(starwars_people: [], starwars_people_status: PeopleError)
}

// update.gleam

pub fn set_starwars_people(people: People) {
  case people {
    [] -> model.set_starwars_people(people, PeopleEmpty)
    people -> model.set_starwars_people(people, PeopleFulfilled)
  }
  |> pair.new(effect.none())
}

pub fn set_starwars_people_error() {
  pair.new(model.starwars_people_error(), effect.none())
}
Enter fullscreen mode Exit fullscreen mode

Lumayan tertata, kan?

Dan jika ingin menambah "fitur" baru:

  • update data/msg.gleam, compiler error, harus menambah pattern tersebut di update
  • update update.gleam
  • update model.gleam
  • compiler happy
  • repeat

Sekarang refactor terakhir, view:

// -- VIEW

fn view(model: Model) -> Element(Msg) {
  views.starwars_people_view(model)
}

// views.gleam

import gleam/list

import lustre/attribute.{class}
import lustre/element/html.{div, text}

import data/model.{type Model, type People}

import components/card

import views/empty
import views/error
import views/loading

pub fn starwars_people_view(model: Model) {
  div([class("min-h-screen bg-gray-800 lg:p-8")], [
    html.h1([class("text-center text-8xl text-yellow-500 font-bold mb-10")], [
      text("the people"),
    ]),
    div([class("flex flex-wrap justify-center items-start")], [
      case model.starwars_people_status {
        model.PeopleLoading -> loading.starwars_people()
        model.PeopleFulfilled -> people(model.starwars_people)
        model.PeopleEmpty -> empty.starwars_people()
        model.PeopleError -> error.starwars_people()
      },
    ]),
  ])
}

fn people(people: People) {
  div(
    [class("flex flex-wrap justify-center items-start")],
    people |> list.map(fn(person) { card.person(person) }),
  )
}

// components/card.gleam

import lustre/attribute.{class}

import lustre/element.{type Element}
import lustre/element/html.{div, text}

import data/model.{type Person}

pub fn person(person: Person) -> Element(t) {
  div([class("max-w-sm rounded-lg overflow-hidden bg-gray-700 p-6 m-4")], [
    div([class("font-bold text-2xl mb-3 text-yellow-500")], [text(person.name)]),
    div([class("uppercase font-bold text-gray-300 text-lg")], [
      text(person.gender),
    ]),
  ])
}

// views/empty.gleam

import lustre/attribute.{class}

import lustre/element.{type Element}
import lustre/element/html.{div, text}

pub fn starwars_people() -> Element(t) {
  div([class("text-white text-2xl font-bold")], [text("where's everyone???")])
}

// views/error.gleam

import lustre/attribute.{class}

import lustre/element.{type Element}
import lustre/element/html.{div, text}

pub fn starwars_people() -> Element(t) {
  div([class("text-red-700 text-2xl font-bold")], [
    text("something went wrong with the people"),
  ])
}

// views/loading.gleam

import lustre/attribute.{class}

import lustre/element.{type Element}
import lustre/element/html.{div, text}

pub fn starwars_people() -> Element(t) {
  div([class("text-white")], [text("loading")])
}

Enter fullscreen mode Exit fullscreen mode

Dan hasilnya:

Image description

Benar sekali, tetap sama.

Terakhir, mari kita buat bahan untuk simulasi state! Tambahkan ini di api.gleam yang literally harmless:

// effects/api.gleam

// dengan gaya (pipeline)
pub fn starwars_people_empty() -> Effect(Msg) {
  Ok([])
  |> SWAPIPeopleReturned
  |> fn(msg) { fn(dispatch) { dispatch(msg) } }
  |> effect.from
}

// tanpa gaya
pub fn starwars_people_error() -> Effect(Msg) {
  effect.from(fn(dispatch) {
    dispatch(SWAPIPeopleReturned(Error(lustre_http.NetworkError)))
  })
}
Enter fullscreen mode Exit fullscreen mode

Lalu di update.init tinggal ganti misal jadi ini:

// update.gleam

pub fn init() -> #(Model, Effect(Msg)) {
  pair.new(model.init(), api.starwars_people_empty())
}
Enter fullscreen mode Exit fullscreen mode

Daaan:

Image description

Atau untuk yang error nya (sekalian pamer editor yang lain):

Image description

Perfect.

Kode final main kita:

import lustre

import lustre/effect.{type Effect}
import lustre/element.{type Element}

import data/model.{type Model}
import data/msg.{type Msg}

import update
import views

// -- MAIN

pub fn main() {
  let app = lustre.application(init, update, view)

  let assert Ok(_) = lustre.start(app, "#app", Nil)

  Nil
}

fn init(_) -> #(Model, Effect(Msg)) {
  update.init()
}

// -- UPDATE

fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    msg.SWAPIPeopleReturned(Ok(people)) -> update.set_starwars_people(people)
    msg.SWAPIPeopleReturned(Error(_)) -> update.set_starwars_people_error()
  }
}

// -- VIEW

fn view(model: Model) -> Element(Msg) {
  views.starwars_people_view(model)
}
Enter fullscreen mode Exit fullscreen mode

Sangat SoC.

Mengapa Gleam di platform web?

Sebagian besar—jika memang tidak semua—bahasa pemrograman yang menggunakan type system static memiliki goal utama dan yang paling utama: safety. Yang maksudnya, setiap kemungkinan masalah yang akan terjadi di runtime dapat dihindari karena sudah di cek di compile time.

Abaikan dulu candaan lama tentang this di JavaScript, salah satu "insiden" populer yang sampai tahun ini terjadi di dunia sopwer engineering adalah perihal null. Null menyebabkan yang tidak diketahui tidak diketahui, dan mengakses null pointer adalah tindakan yang tabu, dan memberikan nilai null dengan kesadaran penuh? Sudah pasti butuh seseorang untuk bicara.

Null mungkin berguna sebagai wadah untuk nilai di masa depan nanti, dan nilai di masa depan tersebut pada akhirnya memiliki tipe juga.

Baiklah, sedikit sangkalan: memanggil undefined sebagai function ataupun mengakses undefined sebagai objek tidak membuat dunia berhenti. Tidak membuat server kebakaran dan tidak membuat perangkat pengguna meledak. Bagian buruknya, paling membuat aplikasi crash dan user tidak bisa menggunakan. Itupun jika ada user nya. Hehe canda user.

Tidak jarang saya menemukan aplikasi web yang crash yang seringnya karena content blocker di peramban saya. Dan itu tidak normal, karena, apalagi selanjutnya? Karena saya pakai Safari? Karena navigator.language saya undefined?

Di bahasa pemrograman yang menggunakan static typing, kebanyakan (tidak semua) mereka tidak memiliki konsep null. Karena, yaa null melanggar kontrak dari typing itu sendiri, kan? Beberapa bahasa pemrograman yang masih memiliki konsep null adalah Swift, JAVa, Kotlin, dan TypeScript.

Kemungkinan penggunaan null adalah saat berinteraksi dengan dunia luar, yang kemungkinan besar, null dianggap sebagai "nilai yang tidak bernilai". Misal, 2 hal ini secara teknis melakukan hal yang sama:

let one = 1
one = getUser()
console.log(one.username) // foltebels

let two = null
two = getUser()
console.log(two.username) // foltebels
Enter fullscreen mode Exit fullscreen mode

Penulis kode yang static typing centric pasti berdalih "bang kaga safe itu, gimana klo properti username ga ada atau si tipe two adalah Promise"? Dan itu exactly yang dihindari oleh bahasa pemrograman yang static type checking:

interface User {
  username: string
  description: any
}

function getUser(): Promise<User> {
  return Promise.resolve({
    username: "foltebels", description: 3
  })
}

let one = null
getUser().then(a => a).then(b => b).then(c => c).then(d => d).then(j => j).then(k => k).then(lol => {
  one = lol
})
console.log(one.username) // bro berbalapan

// versi bener (ga ada compiler error, tapi you know)
let one = { username: "", description: 0 }

Enter fullscreen mode Exit fullscreen mode

Kode di atas dijamin safe karena bakal terjadi compiler error karena si one type nya adalah any dengan nilai null bukan User sebagaimana yang diharapkan oleh return dari getUser

Di bahasa yang lebih strict/strong seperti Gleam, kode of course akan lebih panjang dari yang di atas. Yang pertama, tidak ada konsep Promise di Gleam tapi ada Result yang harus return 2 kondisi: sukses dan error. Promise pun menjalankan 2 callback: resolve dan reject, di contoh di atas kita hanya pakai resolve tapi kita masih bisa menggunakan .catch di promise chain tersebut karena bukan sesuatu yang illegal anyway.

Contoh kode Gleam nya adalah seperti ini:

type User {
  User(username: String, description: Int)
}

type UserError {
  UnknownError
}

fn get_user() -> Result(User, UserError) {
  case Nil == Nil {
    True -> Ok(User(username: "foltebels", description: 3))
    False -> Error(UnknownError)
  }
}

let one = case get_user() {
  Ok(user) -> user
  Error(_) -> User(username: "", description: 0)
}

io.debug(one)
Enter fullscreen mode Exit fullscreen mode

Mungkin kode di atas curang, tapi struktur data Gleam immutable: tidak ada konsep "re-assign" nilai karena yang ada hanyalah "membuat referensi" baru.

Gleam pada dasarnya memiliki nilai Nil, dan itu adalah sebuah nilai. Pattern match ekspresi case di atas akan selalu match ke True karena Nil == Nil dan faktanya memang begitu.

Oke oke, sekarang kita ke yang praktikal: penggunaan Option. Di banyak kasus—kecuali mungkin berurusan dengan GC, misalnya—sebenarnya ini yang diinginkan oleh penggunaan null di luar sana.

Masalahnya, Option pun harus diberikan tipe nya. Jika properti description kondisinya adalah "bisa jadi ada, bisa jadi tidak" berarti tipe nya Option(Int), karena "klo ada, sudah pasti Int".

Dan sekali lagi, Option akan berguna jika berurusan dengan dunia di luar Gleam, dari akses database file atau pemanggilan melalui jaringan. Selainnya, pada dasarnya kita tidak membutuhkan Option karena besar kemungkinan kita tahu apa yang kita ketik, well, kecuali, mungkin, hmm kodenya tiba-tiba ada di clipboard.

Oke oke, kembali ke topik, harusnya pertanyaannya tidak sebatas "Gleam" di "platform web", subjek Gleam bisa di ganti begitupula dengan kata di platform nya. Dari semua bahasa pemrograman yang sempat saya pelajari dari Reason, ReScript, Clojure dan TypeScript (berkali-kali), yang paling "masuk" dengan saya hanya Gleam. Go dan Rust mungkin termasuk juga, tapi saya tidak yakin perannya sama seperti Gleam.

Dan of course juga karena di Gleam ada Lustre yang memungkinkan untuk membuat aplikasi menggunakan TEA.

Kekurangan

Kurang lebih sama seperti yang ada di tulisan sebelumnya. Dan saya rasa tidak cocok jika Frontend menggunakan Gleam sedangkan Backend nya menggunakan bahasa yang lain kecuali jika memang hanya ingin menggunakan Lustre nya.

Jika ada ingin membuat personal project berjenis web app, sudah pasti akan saya tulis menggunakan Gleam, dan sudah ada yang on-going juga.

Dan menurut opini pribadi saya, jika membandingkan dengan bahasa pemrograman lain, Gleam lebih mudah dipahami dan cukup sederhana. Misal ambil contoh Reason:

  • Kapan harus pakai myMutableNumber^ dan kapan tidak (perihal ref)
  • Mengapa ada {js|🌍|js} dan ada {j|hello, $world|j} juga?
  • Mengapa ~ di ~labeled_argument= penting?
  • Dan masih banyak lagi

Tentu Gleam pun tidak sempurna. Tapi entahlah, belum menemukan sesuatu yang sampai bikin stress selain membuat decoder haha.

Penutup

Tulisan ini sekaligus mengakhiri series Gleam dari saya di tahun ini. Kurang lebih mungkin sama dengan yang part sebelumnya ya.

Nah, waktunya kembali buidl™

Top comments (0)