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 kategoriiFunktorApplicativeLambda calculusSemigroupMonoid
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
- Zdefiniuj nowy typ dla Msg
- Obsłuż typ w update
- 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
- Zapytania http opisywane są jak dane, obiekt zawierający
- 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
- 831