Elm - podstawy języka i architektury

Mateusz Pokora

Czym jest Elm?

  • Język programowania kompilowany do JS
  • Funkcyjny i statycznie typowany
  • Tworzony od 2012 (pierwszy stable release 2016)

Kolejny dzień kolejny framework?

Czym jest Elm?

  • Nastawiony na łatwy start i użycie - brak "magicznych" funkcji
  • Przeznaczony do tworzenia GUI
  • Minimalistyczne API
  • Framework wbudowany w język

Programowanie funkcyjne? Słyszałem że to trudne!

  • Teoria kategorii
  • Funktor
  • Applicative
  • Lambda calculus
  • Semigroup
  • Monoid

Co daje nam elm?

  • Pełne środowisko do tworzenia aplikacji webowej
  • Bezpieczeństwo w postaci silnego systemu typów i pomocnego kompilatora
  • Developer experience (łatwy refactor, zmniejsza potrzebę unit testów)

Środowisko

  • React
  • Redux
  • Prettier
  • Flow/Typescript
  • Webpack
  • Babel
  • Immutable.js

vs.

  • Elm
  • elm-format
  • Html.program

System typów

System typów

  • NoRedInk - 20 000 linii kodu i 0 runtime exception
  • Bezpieczny refactor
  • Mniej unit testów
  • "Jak się skompiluje to działa"

System typów

Elm-format

  • Jeden rodzaj średników
  • Jeden format nowych linii i wcięć
  • Kontrowersyjny format przecinków :D
  • Niekonfigurowalny <3
todos =
    [ "Learn elm"
    , "Eat pizza"
    , "Have fun"
    ]

Elm - podstawy

Html

import Html as H
import Html.Attributes as A

H.div [A.class "title"] [
    H.span [] [H.text "Hello"]
]

Funkcje

 

  • Podstawowy element języka
  • Funkcje są "pure"
  • Zawsze zwracają tą samą wartość przy takich samych argumentach.
  • Brak efektów ubocznych (logowanie, komunikacja z serwerem)

Funkcje

// Deklaracja funkcji

sayHello : String -> String
sayHello name = 
    "Hello " ++ name ++ "!"

// Wywołanie

sayHello "Mateusz"

Listy

todos =
    [ "Learn elm"
    , "Eat pizza"
    , "Have fun"
    ]
  • Zbiór wartości o tym samym typie
  • Wartości mogą się powtarzać

Listy

List.map

List.filter

List.concat
  • Zestaw użytecznych funkcji dla danego typu (List.map, List.concat, List.filter)

Lambda

List.map
        (\todo ->
            H.div [] [ H.text todo.title ]
        )
  • Funkcja anonimowa

Record

/* point2D : { x : Int , y : Int } */

point2D =
  { x = 0
  , y = 0
  }

Type alias

type alias Point2D =
    { x : Int
    , y : Int
    }

point : Point2D
point =
  { x = 0
  , y = 0
  }

Immutability

var user = {
    name: "Mateusz",
    age: 21
};

user.age = 100;

Nie modyfikujemy istniejących struktur. Zamiast tego tworzymy nowe aktualizując pola.

user : User 
user = {
    name = "Mateusz"
    , age = 21
}

updatedUser = { 
    user | age = 100 
}

Elm architecture

(redux :D )

Text

https://elmbridge.github.io/curriculum/images/elm-architecture-1.jpeg

Model

type alias Model = {
    nameInput : String
    , emailInput : String
    , termsAndConditionsInput : Bool
    , formValid : Bool
    , modalOpen : Bool
}
  • Struktura danych i aktualny stan aplikacji
  • Cały stan aplikacji jest reprezentowany przez jeden obiekt

View

view : Model -> Html Msg
view model =
    div [] [
        input [
            value model.emailInput
            , onClick UpdateEmail
        ] 
        []
    ]
  • Reprezentacja wizualna naszego modelu
  • Inicjuje zmiany stanu

Update

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of 
        UpdateEmail email ->
            ({ model | email = email}, Cmd.none )
        SubmitForm ->
            // komunikacja z serwerem
  • Aktualizuje model na podstawie aktualnego stanu i Msg
  • Wywołuje side effecty (komunikacja z API)

Html.program

Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

Funkcja opisująca jak wygląda nasza aplikacja

Time travel debugging

  • Podgląd stanu aplikacji w dowolnym momencie jej działania
  • Cofnięcie aplikacji do stanu z przeszłości

Elm vs React/Redux

  • Elm skupia się na typach danych nie komponentach UI
  • Update może wywołać kolejny update
  • Wszystko budujemy naokoło TEA

Todo app

  • Wyświetlanie listy elementów
  • Dodawanie elementu do listy
  • Filtrowanie listy po atrybutach
  • Zaznaczanie elementu jako zrobiony
  • Komunikacja z serwerem (odczyt, zapis)

Jak zacząć?

1. npm install -g elm elm-format create-elm-app


2. create-elm-app todoApp


3. cd ./todoApp 


4. elm-app start

Jak zacząć?

1. git checkout 
    https://github.com/pokorson/node-workshop-server


2. install atom packages
    elm-jutsu 
    linter 
    linter-elm-make 
    elm-language 

1. Model

type alias Model =
    { todoList : List Todo
    }

type alias Todo =
    { title : String
    , id : Int
    }

2. Update

type Msg = NoOp

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    (model, Cmd.none)

3. View

view : Model -> H.Html Msg
view model =
    H.div [] [...]

Completed flag

type alias Todo =
    { title : String
    , id : Int
    , completed : Bool
    }

Conditional render

H.div [] [
    if todo.completed then
        H.text "Completed"
    else 
        H.text "In progress"
]

Statement vs expression

  • Statement - kod jest wykonywany jak instrukcje, linijka po linijce
  • Expression - kawałek kodu jest ewaluowany jako wartość

Statement

if (age == 21) {
    return 0;
} else {
    // do something
}

db.saveUser()

Expression

H.div [] [
    if todo.completed then
        H.text "Completed"
    else 
        H.text "In progress"
]

Zagadka?

H.div -> List Attribute -> ...



// dodaj opcjonalną klase
H.button [ A.type_ "submit" ]

Jak dodać opcjonalny atrybut do elementu?

Toggle completed

  1. Zdefiniuj nowy typ dla Msg
  2. Obsłuż typ w update
  3. Dodaj event do view

Toggle completed

type Msg
    = ToggleCompleted Int
    | DoSomethingElse

Union Types

Typy pozwalające lepiej odzwierciedlić co się dzieje w naszym kodzie

filter = "0"

if (filter == "0") {
    return "aktywny";
} else if (filter == "1") {
    return "nieaktywny";
}

Union Types

type Filter = Active | NonActive

Pattern matching

  • Bardziej rozwinięty switch/case
  • pozwala dopasowywać nie tylko wartości ale również typy oraz struktury

Pattern matching

case value of
    [] ->
        // pusta lista
    head :: [] ->
        // Lista jednoelementowa
    head :: tail ->
        // Lista wieloelementowa

Obsługa wszystkich możliwości

Kompilator sprawdza czy obsługujemy wszystkie możliwości danego typu

Obsługa wszystkich możliwości

type Visibility = 
    All 
    | Active 
    | Completed


tableView : Visibility -> Html Msg
tableView visibility = 
    div [] [
        case visibility of
            All ->
                text "Wszystkie"

            Active ->
                text "Aktywne"
            
    ]

This `case` does not have branches for all possibilities.

90|>          case visibility of
91|>            All ->
92|>                text "Wszystkie"
93|>
94|>            Active ->
95|>                text "Aktywne"

You need to account for the following values:

    Main.Completed

Add a branch to cover this pattern!

Wywołanie w widoku

todoItem todo =
    H.div [
        E.onClick (ToggleCompleted todo.id)
    ] [...]

type alias Model =
    { ...
    , visibility : Filter
    }


type Filter
    = All
    | Completed
    | Uncompleted

Filtrowanie todo

Filtrowanie todo

type Msg =
    ...
    | ToggleFilter Filter



update msg model =
    case msg of 
        ToggleFilter filter ->
            ...

Filtrowanie todo

List.map
    (\todo -> H.text todo.title)
    (List.filter (\todo -> todo.completed) model.todoList)


// pipe operator
// przekazujemy wartość do funkcji
model.todoList
    |> List.filter (\todo -> todo.completed)
    |> List.map (\todo -> H.text todo.title)

Null values

  • Brak wartości null, undefined
  • Możliwość braku wartości reprezentowana przez typ Maybe

Maybe

description : Maybe String
description =
    Just "I'm description"

description =
    Nothing

            
case description of
    Just body ->
        String.reverse body
    
    Nothing ->
        ""

Komunikacja parent-children

  • Enkapsulacja części stanu w innym module. (React local state :D)
  • Aplikacja Elm posiada jedną funkcje update, jeden model i jeden view

Przeniesienie części modelu children pod jedną wartość parent

import TodoList

type alias Model =
    { todoList : TodoList.Model
    }

Opakowanie typu Msg

import TodoList

view : Msg -> Model -> H.Html Msg
view msg model = 
    H.div []
        [ H.button [ A.onClick DoSomething ] []
        , TodoList.view model.todoList
        ]


type Msg = DoSomething

Opakowanie typu Msg

import TodoList

view : Msg -> Model -> H.Html Msg
view msg model = 
    H.div []
        [ H.button [ A.onClick DoSomething ] []
        , H.map TodoListMsg (TodoList.view model.todoList)
        ]


type Msg
    = DoSomething
    | TodoListMsg TodoList.Msg

Opakowanie typu Msg

TodoListMsg listMsg ->
    let
        ( newListModel, newListMsg ) =
            TodoList.update listMsg model.todoList
    in
        ( { model | todoList = newListModel }, Cmd.map TodoListMsg newListMsg )

Komunikacja z serwerem

elm-package install NoRedInk/elm-decode-pipeline

Effects as data

  1. Zapytania http opisywane są jak dane, obiekt zawierający
  2. Następnie konfiguracja przesyłana jest do runtime Elm-a który wykonuje zapytanie za nas

Effects as data

todosRequest : Request a
todosRequest = 
    Http.get "http://localhost:3030/todos" decoder

Json.Decode

  • Separacja pomiędzy naszymi czystymi typami a "niebezpiecznym" światem zewnętrznym
todoDecoder : Decode.Decoder Todo
todoDecoder =
    DecodeP.decode Todo
        |> DecodeP.required "title" Decode.string
        |> DecodeP.required "id" Decode.int

Wysłanie requestu


init : ( Model, Cmd Msg )
init =
    ( ...
    , Http.send TodoListLoaded fetchTodos  
    )


update msg model = 
    case msg of 
        FetchTodos ->
            (model
            , Http.send TodoListLoaded fetchTodos)
        ...
type alias RemoteModel =
    { todoList : List Todo
    , requestPending : Bool
    , requestError : String
    }

Model danych z serwera

Eliminacja niemożliwego stanu

Utworzenie struktury modelu w taki sposób aby wykluczyć stany które wzajemnie się wykluczają

type ListState
    = Loading
    | Failure String
    | Success (List Todo)

Eliminacja niemożliwego stanu

type alias Model =
    { todoList : ListState
    , visibility : Filter
    }


type ListState
    = Loading
    | Failure String
    | Success (List Todo)

Eliminacja niemożliwego stanu

view model =
    case model.todoList of
        Loading ->
            H.text "todo list loading..."

        Failure err ->
            H.text "error"
        
        Success todoList ->
            renderList...
        

Eliminacja niemożliwego stanu

Dziękuję za uwagę :)

Elm nodeschool

By vrael560

Elm nodeschool

  • 786