Elm – Hello world on the map – Elm volá Javascript

Začíná třetí díl seriálu, který jsem původně zamýšlel jako jediný článek. Není mým cílem psát tutoriál, dostupná dokumentace, na kterou hojně odkazuji, je poměrně kvalitní a snadno pochopitelná i lidem, kteří vládnou tak špatnou angličtinou jako já.

Seriál: Elm – Hello world on the map !!! (4 díly)

  1. Elm – Hello world on the map – úvod 29.8.2016
  2. Elm – Hello world on the map – začínáme 5.9.2016
  3. Elm – Hello world on the map – Elm volá Javascript 12.9.2016
  4. Elm – Hello world on the map – Elm naslouchá Javascriptu 19.9.2016

Cílem této série článků je podívat se více na zoubek vzájemné interakce mezi Elmem a Javascriptem. V tomto díle si nejprve necháme trochu vynadat od kompilátoru jazyka Elm. Díky tomu pochopíme, jak a proč jsou aplikace v Elmu bezpečné, byť se kompilují zase jen do obyčejného Javascriptu. Poté si ukážeme, jak volat z aplikace v Elmu vlastní javascriptový kód. V něm si pak můžeme dělat, co chceme, a tudiž můžeme vytvářet všemožné chyby, které nám beztypový Javascript umožní. A proto je takový kód od vlastní aplikace v Elmu zvláštním způsobem oddělený.

Elm a Javascript

Elm využívá Javascript dvěma způsoby. Jednak existuje něco, co se nazývá nativní modul. To je ten Javascript, který pak kompilátor Elmu vezme a přidá ho do aplikace. Na tento kód jsou přirozeně kladeny vysoké nároky, musí se dodržovat závazná pravidla a kupodivu chybí řádná dokumentace. Nativní moduly najdeme nejen v jádře jazyka Elm, ale používají je i některé (nízkoúrovňové) balíčky. K tomuto tématu bych se rád vrátil v některém z příštích dílů.

Druhým způsobem jak může aplikace v Elmu komunikovat s Javascriptem je pomocí portů. Toto rozhraní je určeno pro „běžné vývojáře“. Tímto způsobem můžeme z Elmu volat jakýkoliv javascriptový kód, chyby v něm obsažené se do aplikace v Elmu nepřenesou.

Mapa v Javascriptu

Vyjdeme z předchozího příkladu, ale maličko si jej upravíme.

<!DOCTYPE html>
<html lang="cs">
  <head>
    <meta charset="UTF-8">
    <title>Elm smapy</title>
    <!--
      přidáme si mapové api,
      rozhodl jsem se vyzkoušet api.mapi.cz
    -->
    <script type="text/javascript" src="//api.mapy.cz/loader.js"></script>
    <!--
      Zde načteme zkompilovaný zdrojový kód Elmu,
      ten jsme si ručně zkompilovali příkazem
      $ elm make Main.elm --output=elm-main.js
    -->
    <script type="text/javascript" src="./elm-main.js"></script>
  </head>
  <body>
    <!--
      Není pěkné míchat Javascript do HTMl a tak jej umístíme do samostatného souboru.
    -->
    <script type="text/javascript" src="./js-main-01.js"></script>
  </body>
</html>

Předně, není pěkné míchat Javascript do HTMl a tak jej umístíme do samostatného souboru a naimportujeme odkazem. A zrovna se pokusíme vytvořit mapu podobně jako v tomto příkladě.


/*
načteme api pro mapy.cz
*/
Loader.load()

/*
spustíme Elm
*/
Elm.Main.fullscreen()

/*
vytvoříme mapu
*/

window.onload = function () {
    const center = SMap.Coords.fromWGS84(17.519, 48.816)
    const mapa = new SMap(JAK.gel("mapa"), center, 12)
    mapa.addDefaultLayer(SMap.DEF_BASE).enable()
    mapa.addDefaultControls()
}

Modul v Elmu zatím necháme nezměněný, nemusíme tedy kompilovat, pouze si v browseru zobrazíme upravený index.html s novým javascriptovým kódem.

screen-01

No vida. Konečně se nám zobrazila mapa. Avšak svět Javascriptu a svět Elmu jsou stále oddělené. Vzpomínáte, v Elmu jsme si přece vytvořili model, kde jsme si inicializovali výchozí souřadnice středu mapy, zoom a id mapy (mělo by se použít jako attribut id v kontejneru, elementu <div> ). V Javascriptu používáme úplně jiné hodnoty, k těm definovaným v Elmu nemáme žádný přístup. Právě vytvořená stránka lže, uváděné souřadnice a zoom jsou nepravdivé. Pojďme otevřít porty a propojit tyto dva světy.

Elm a porty

Budeme muset lehce překopat původní zdrojový kód v Elmu. Ve funkci main jsme si v minulém díle vytvořili program dle základního patternu elm architecture design – model, update a view. Nyní musíme zavolat na pomoc commands and subscriptions. Funkce main se změní takto.

main : Program Never
main =
    HtmlApp.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

Namísto Html.App.beginnerProgram voláme Html.App.program. Tato funkce si žádá poněkud odlišný inicializační záznam.

{ init : (model, Cmd msg), update : msg -> model -> (model, Cmd msg), subscriptions : model -> Sub msg, view : model -> Html msg }

Číst anotaci funkcí už nám nedělá problém, takže hned vidíme, že budeme navíc přidávat funkci pro prvek subscriptions a místo modelu máme prvek init. Do něj bude třeba předat funkci vracejicí tuple, n-tici, přesněji dvojici (model, Cmd msg). Pojmenujme si ji také init, ať je to jasnější, a pojďme ji vytvořit.

init : ( Model, Cmd Msg )
init =
    let
        model =
            initialModel
    in
        ( model, createSMap model )

Zápis pomocí let ... in je prozatím trochu nadbytečný, ale berte to jako ukázku vytváření lokálních proměnných ve funkcionálním elmu.

Definice modelu a funkce initialModel zůstanou stejné jako v minulém díle. Změna je v tom, že původně jsme funkci initialModel, která inicializuje model s výchozími hodnotami, použili přímo ve funkci main. Nyní tutéž (nezměněnou) funkci použijeme ve funkci init. Ta už nevrátí jen samotný model, ale zároveň přidá příkaz createSMap. Úžasné a snadné skládání funkcí. Příkazu (command) createSMap předáme jako hodnotu model.

Co to ale je, to createSMap? No, jsme ve funkcionálním jazyce, takže to bude zase nejspíš nějaká funkce. A skutečně. Její definice je ovšem poněkud nezvyklá. Začíná klíčovým slovem port a zcela chybí tělo funkce.

port createSMap : Model -> Cmd msg

Klíčovým slovem port říkáme, že se ve skutečnosti zavolá a spustí kód v externím Javascriptu. Proto tato funkce nemá žádné tělo. Očekávám, že nyní se při inicializaci aplikace, tedy při zavolání funkce init, spustí jako vedlejší efekt příkaz createSMap. A ten spustí nějaký (dosud nenapsaný) kód v čistém Javascriptu. Tak dobře, zkusme naši aplikaci zkompilovat.

screen-compiler-001

Dostali jsme pěkně vynadáno. Ano, to je jeden ze způsobů, jak se nás Elm snaží odrazovat od používání portů a přímého přístupu k Javascriptu. Ale zase umí pěkně poradit. Úplně na začátek souboru dopíšeme před slovo module slovo port.

port module Main exposing (..)

Tím z našeho modulu uděláme nečistý fujtabl modul, který používá podivné porty, za nimiž číhá potenciálně nebezpečný Javascript.

Tak co včíl? Půjde to zkompilovat? Zkusme.

screen-compiler-002

Jasně, ve funkci main se snažíme předat do záznamu funkci subscriptions, která dosud neexistuje. Na tu jsme v tom zmatku úplně zapomněli. Docela pěkně nás ten kompilátor vede a poučuje. Tož tady máš, kompilátore.

-- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none

V tuto chvíli subscriptions nepotřebujeme. K čemu je to dobré, se dozvíme později. Všimněte si, že Elm nám nedovolí jen tak na něco zapomenout. Proto musíme funkci subscriptions vytvořit a vrátit alespoň ono `Sub.none`.

A znovu spustíme kompilátor. Koho to nebaví, tak už má nainstalované elm-live, nebo má nějaké podobné řešení, které spouští kompilátor automaticky při změně zdrojového souboru. Výsledek kompilátoru je možné vidět v konzoli, pokud spouštíme elm make. Pokud použijeme elm reactor, zobrazí se chyba při kompilaci přímo v browseru. Elm reactor neumí (zatím) automaticky obnovit stránku v prohlížeči (live reloading).

Tak pojďme dál. Co nám kompilátor řekne teď?

screen-compiler-003

Jaj!!! My máme od minule funkci update, která bere dva parametry a vrací model update : Msg -> Model -> Model jenže Html.App.program požaduje, aby funkce pro update vracela n-tici, dvojici update : Msg ->Model ->(Model, Cmd Msg). Častý zdroj chyb v Javascriptu, neočekávaný typ proměnné, nám Elm páchat nedovolí. Takže takto?

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

Ve funkci init jsme podobným způsobem vraceli ( model, createSMap model ), čímž jsme spustili vedlejší efekt voláním příkazu createSMap. V update stejně tak říkáme, jaké příkazy, vedlejší efekty se mají spouštět. Prozatím zde žádný vedlejší efekt spouštět nebudeme, což musíme říci tím Cmd.none. Je to podobné jako jsme před chvíli použili Sub.none.

Tak, a opět promluví kompilátor.

$ elm make Main.elm --output=elm-main.js
Success! Compiled 1 module.                                         
Successfully generated elm-main.js

Výborně, konečně jsme vytvořili chyb prostou aplikaci v Elmu. Můžeme si ji hned spustit.

Port v Javascriptu

Všimněte si jedné zásadní věci. V Elm aplikaci jsme si vytvořili port, funkci port createSMap : Model -> Cmd msg. Je to funkce nefunkce, postrádá tělo. Někomu to může připomínat deklaraci funkce z hlavičkového souboru v jazyce C/C++. A v principu to funguje podobně, ale ignoruje se selhání. Elm aplikace ví, že někde v externím Javascriptu nejspíš bude funkce createSMap. Tu můžeme zavolat pomocí příkazu (command). V našem případě tak činíme ve funkci init, často se vedlejší efekty spouští ve funkci update. A když žádná taková funkce v externím Javascriptu nebude, aplikace v Elmu to přejde mlčením. Opravdu. Elm prostě říká, ano, zavolám přes port funkci Javascriptu, předám jí správná data, ale neručím za to, zda to volání někdo na druhé straně přijme a co s tím udělá.

Trochu jiný přístup je v opačném směru. Pokud Elm aplikace přijímá nějaké volání z externího Javascriptu, kontroluje si datový typ, jaký dostává. O tom ale až příště.

Takže ještě jednou. V Elm aplikaci máme nadefinovanou funkci port createSMap : Model -> Cmd msg. K ní se potřebujeme dostat v Javascriptu a vzít si ta předávaná data (model).

Víme už, že Elm aplikaci spustíme zavoláním některým z těchto tří způsobů:

Elm.Main.fullscreen() // může být i Elm.Main.embed(element), nebo Elm.Main.worker()

Elm je objekt, který vytvořil Elm kompilátor. Elm.Main reprezentuje modul, který jsme kompilovali (a ve kterém je funkce main). Název Main je odvozen z názvu toho modulu. Pokud bychom hlavní modul pojmenovali třeba MyApp, bude tomu odpovídat objekt Elm.MyApp a volání například Elm.MyApp.fullscreen(). Object Elm.Main  vypadá takto:

  Object {
    embed: function embed(domNode, flags),
    fullscreen: function fullscreen(flags),
    worker: function worker(flags)
}

Tyto funkce už známe. Slouží ke spuštění Elm aplikace. Bohužel v Javascriptu není zřejmé, co funkce vrací. Tak vězte, že vrací právě to, co potřebujeme. Uložíme si to do proměnné.

const elm = Elm.Main.fullscreen() // může být i Elm.Main.embed(element), nebo Elm.Main.worker()

A někde v prohlížeči, v javascriptové konzoli, v rozšíření pro vývojáře si vypíšeme právě získanou hodnotu.

Object {
    ports: {
        createSMap: {
            subscribe: function subscribe(callback),
            unsubscribe: function unsubscribe(callback)
    }}}

Dál už je celkem intuitivní. Celé to tedy funguje tak, že v Javascriptu nastavím funkce zpětného volání, které budou naslouchat, až je Elm aplikace zavolá.

elm.ports.createSMap.subscribe( model -> console.log(model) );

Protože chceme vytvořit mapu pomocí mapové API od seznamu, bude celá implemetace vypadat takto:


/*
načteme api pro mapy.cz
*/
Loader.load()

/*
Když to nebylo obaleno funkcí window.onload,
tak
Uncaught ReferenceError: SMap is not defined
*/

window.onload = function () {

    /*
    spustíme Elm
    */
    const elm = Elm.Main.fullscreen()

    /*
    mapu vytvoříme ve funkci zpětného volání, bude se volat po zavolání createSMap z Elmu
    */
    const createSMap = function (karta)  {
        const center = SMap.Coords.fromWGS84(karta.center[1], karta.center[0])
        const mapa = new SMap(JAK.gel(karta.id), center, karta.zoom)
        mapa.addDefaultLayer(SMap.DEF_BASE).enable()
        mapa.addDefaultControls()
    }

    /*
    Nastavíme zpětné volání pro createSMap
    */
    elm.ports.createSMap.subscribe(createSMap);
}

Pokračování příště

Výborně, nyní se nám zobrazuje mapa dle parametrů definovaných v Elmu. Žádná duplicita. Ale ještě neumíme aktualizovat souřadnice středu při posunu mapy a nový zoom při jeho změně. Aplikace pořád lže, neboť komunikuje pouze jedním směrem. Tak to příště napravíme.

Jsem programátor na volné noze. Věnuji se především zpracování a prezentaci dat. Zajímám se o programovací jazyky. Začínal jsem s jazykem C/C++ a PHP pro web. Pak jsem se nadchnul pro Python. Když přišel Node.js, začal jsem věnovat více své pozornosti JavaScriptu. Nyní mne zajímají především jazyky Rust a Elm. Krom toho opravuji s přáteli zchátralý dětský tábor v Karpatech (nejen) z hlíny a slámy. A dělám spoustu dalších věcí, kvůli kterým mi na programování nezbývá čas.

Komentáře: 1

Přehled komentářů

Anonym
Zdroj: https://www.zdrojak.cz/?p=18738