the notorious
snacky

<- Quay về trang chủ

Đôi điều về Elm

Dịp cuối tuần vừa rồi thì mình cũng bắt đầu học Elm và làm thử 1 project nho nhỏ, mục đích là clone lại Trello chơi.

Trong quá trình làm thì cũng nhận thấy có nhiều thứ cần phải note lại.

Elm là một ngôn ngữ lập trình hàm (functional programming language) khá thú vị, chịu ảnh hưởng khá nhiều từ Haskell, và được biên dịch trực tiếp ra JavaScript.

Ngoài việc giống Haskell ra thì điểm nổi bật nhất của Elm chính là kiến trúc của nó (Elm Architecture) , giúp cho việc phát triển web apps với các ngôn ngữ functional programming trở nên dễ dàng hơn. Redux của hội React cũng chịu ảnh hưởng lớn từ kiến trúc này.

Records

Records trong Elm là một khái niệm tương đồng với Objects trong JavaScript, tuy nhiên có vài điểm khác:

Để khai báo một record thì khá đơn giản, giống với JS:

hero : { name : String, level : Int }
hero = {
  name  = "Shadow Fiend",
  level = 1
}

Điểm khác biệt là khi chúng ta update một record, thì Elm sẽ trả về một record mới, vì một record là immutable:

hero = { hero | level = 25 }

Để tìm hiểu thêm, bạn có thể đọc tài liệu về Records của Elm.

Data Driven

Với Elm Architecture, chúng ta không cần phải (và không thể) tương tác trực tiếp với một DOM element nào cả, thay vào đó, chúng ta tương tác với dữ liệu (data) biểu diễn element đó. Mọi thao tác mà người dùng thực hiện trên web app của bạn, đều là việc tương tác (cập nhật) với các dữ liệu này.

Nếu bạn đã biết khái niệm data binding trong các framework khác như React hay Angular, thì đây chính là nó đó.

Ví dụ trong project của mình, các task hiển thị trên màn hình được biểu diễn bằng một List trong Model, như sau:

src/Models.elm

type alias Task = {
  name: String,
  status: String
}

type alias Model = {
  tasks: List Task,
  ...
}

Để hiển thị các task ra màn hình, việc chúng ta cần làm chỉ là hiển thị từng item có trong List đó, thông qua sự hỗ trợ của Html helper:

src/Views.elm

taskItemView : Int -> Task -> Html Msg
taskItemView index task =
  li [ class "task-item" ] [ text task.name ]

Khi user muốn thêm một task mới, việc chúng ta làm là tương tác với Model để add thêm một task:

src/Models.elm

addNewTask : Model -> ( Model, Cmd Msg )
addNewTask model =
  let
     newModel = { model | tasks = model.tasks ++ [ newTask ] }
  in
    ( newModel, Cmd.none )

Elm sẽ tự thực hiện phần việc bên dưới, bao gồm: kiểm tra sự thay đổi của Model, update lại DOM (như là thêm, xóa, update các element,...) tương ứng.

"Tương tác" với DOM element

Như đã nói ở phần vừa rồi, chúng ta không thể tương tác với bất kì một DOM element nào, vậy khi cần... tương tác thì phải làm sao?

Ví dụ chúng ta có một textbox và muốn lấy nội dung của nó (thuộc tính value).

Việc đầu tiên chúng ta cần làm chính là định nghĩa phần dữ liệu để biểu diễn nó, mà ở đây chúng ta tạo một trường mới trong Model:

src/Models.elm

type alias Model = {
  ...
  taskInput: String,
  ...
}

Tiếp theo, chúng ta sẽ thực hiện "binding" phần data này với textbox:

src/Models.elm

type Msg = ... | TextInput String

src/Main.elm

view : Model -> Html Msg
view model =
  ...
  input [ type_ "text", 
          placeholder "What's on your mind right now?",
          onInput TextInput,
          value model.taskInput
        ] []
  ...

Khi người dùng gõ vào textbox, sự kiện onInput được trigger, chúng ta sẽ phát đi một Message tạm gọi là TextInput kèm theo một tham số kiểu String để update giá trị của Model.textInput.

src/Main.elm

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    ...
    TextInput content -> 
      ( { model | taskInput = content }, Cmd.none )

Như vậy, tại bất kì thời điểm nào, chúng ta cũng có thể lấy được nội dung của textbox thông qua giá trị Model.textInput, hoặc thay đổi nó thành một giá trị khác.

Side effects

Một đặc điểm của functional programming đó là việc hạn chế side effect, mỗi một hàm được viết ra đều chỉ thực hiện một việc duy nhất (pure function). Nghe qua thấy khà là mâu thuẫn đối với việc làm web, vì side effects là một phần tất yếu của các web apps (HTTP calls, Web socket,...).

Elm có một loại effect gọi là managed effects. Và cũng tương tự như khi làm việc với DOM, đối với managed effects, chúng ta khai báo data để biểu diễn thao tác mà chúng ta muốn thực hiện, Elm sẽ lo phần còn lại.

Có 2 kiểu managed effects chúng ta có thể dùng trong CommandsSubscriptions.

Trong project mà mình giới thiệu, khi muốn gọi một hàm setStorage để lưu dữ liệu vào localStorage sau khi add một task mới, ta có thể gọi nó dưới dạng một command, ở đây là setStorage:

src/Models.elm

addNewTask : Model -> ( Model, Cmd Msg )
addNewTask model =
  ( newModel, setStorage newModel )

Kết nối với JavaScript thông qua Ports

Để tương tác được với JavaScript (ví dụ sử dụng thư viện, sử dụng các hàm JS đã được viết trước đó,...), chúng ta có thể kết nối thông qua Ports.

Việc khai báo một port từ phía Elm khá đơn giản, ví dụ đây là đoạn code khai báo hàm setStorage, hàm này trả về một Command, và sẽ được gọi từ phía JavaScript.

src/Models.elm

port setStorage : Model -> Cmd msg

Từ khóa port đóng vai trò tương tự như extern trong C.

Ở phía JavaScript, ta viết:

main.js

app.ports.setStorage.subscribe(function(model) {
  // Do something
});

Như vậy, khi command setStorage được gọi từ phía Elm, phía JavaScript sẽ nhận được message này kèm với dữ liệu đi kèm (là Model) và thực hiện việc xử lý trong hàm callback của lệnh subscribe.

Pattern Matching

Pattern Matching là một phương pháp khá là hữu ích, xuất hiện trong nhiều ngôn ngữ lập trình, trong đó có cả Rust và Elm.

Việc sử dụng pattern matching trong Elm phụ thuộc nhiều vào cú pháp case .. of, ví dụ sau đây là một hàm kiểm tra một List có rỗng hay không:

isEmpty : List a -> Bool
isEmpty xs =
    case xs of
        [] ->
            True

        _ ->
            False

Nếu a là một List rỗng [] thì trả về True, ngược lại nếu nó match với một giá trị bất kì thì trả về False.

Hoặc ta có thể sử dụng pattern matching để viết hàm tính số Fibonacci:

fibo : Int -> Int
fibo n =
  case n of
    0 -> 1
    1 -> 1
    _ -> fibo (n-1) + fibo (n-2)

Đối với dân Haskell thì sẽ thấy cách viết này hơi khó chịu một tí, vì đã quen với cách viết của bên đó là:

fibo : Int -> Int
fibo 0 = 1
fibo 1 = 1
fibo n = fibo (n-1) + fibo (n-2)

Tuy như vầy có "đẹp" hơn, nhưng anh bạn tác giả của Elm lại không thích, nên anh ấy quyết định không đưa chức năng này vào .

Ngoài ra thì pattern matching còn làm được rất nhiều việc khác, rất là hữu dụng trong quá trình viết code Elm, các bạn có thể tham khảo ở gist Elm Destructuring cheatsheet.


Nhìn chung, Elm là một ngôn ngữ đáng để học, nhất là nếu bạn đã có sẵn background về lập trình (thường là imperative programming) và đang muốn dòm ngó sang functional programming thì Elm thực sự là điểm khởi đầu khá tốt.