Erstellen einer Haskell-Webanwendung mit Reflex. Teil 1

Einführung



Hallo, alle miteinander! Mein Name ist Nikita, und wir bei Typeable verwenden den FRP-Ansatz, um das Frontend für einige Projekte und insbesondere dessen Implementierung in Haskell - einem Webframework - zu entwickeln reflex



. Es gibt keine Handbücher zu diesem Framework für russischsprachige Ressourcen (und es gibt nicht so viele davon im englischsprachigen Internet), und wir haben beschlossen, es ein wenig zu korrigieren.







Diese Artikelserie führt Sie durch die Erstellung einer Haskell-Webanwendung mit dem reflex-platform



. reflex-platform



bietet Pakete reflex



und reflex-dom



. Das Paket reflex



ist eine Haskell- Implementierung von Functional Reactive Programming (FRP) . Die Bibliothek reflex-dom



enthält eine Vielzahl von Funktionen, Klassen und Typen zum Arbeiten DOM



. Diese Pakete sind getrennt, weil Der FRP-Ansatz kann nicht nur in der Webentwicklung verwendet werden. Wir werden eine Anwendung entwickeln Todo List



, mit der Sie verschiedene Manipulationen mit der Liste der Aufgaben durchführen können.













Zum Verständnis dieser Artikelserie sind Kenntnisse der Programmiersprache Haskell ungleich Null erforderlich, und Vorkenntnisse in der funktionalen reaktiven Programmierung sind hilfreich.

FRP. , — , :







  • Behavior a



    — , . , .
  • Event a



    — . , .


reflex



:







  • Dynamic a



    Behavior a



    Event a



    , .. , , , , , Behavior a



    .


reflex



— . , . , , , , .., .









nix



. .







, nix



. , NixOS, /etc/nix/nix.conf



:







binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
binary-caches-parallel-connections = 40
      
      





NixOS, /etc/nixos/configuration.nix



:







nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];
nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
      
      





:







  • todo-client



    — ;
  • todo-server



    — ;
  • todo-common



    — , ( API).


. :







  • : todo-app



    ;
  • todo-common



    (library), todo-server



    (executable), todo-client



    (executable) todo-app



    ;
  • nix



    ( default.nix



    todo-app



    );

    • useWarp = true;



      ;
  • cabal



    ( cabal.project



    cabal-ghcjs.project



    ).


default.nix



:







{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {
    owner = "reflex-frp";
    repo = "reflex-platform";
    rev = "efc6d923c633207d18bd4d8cae3e20110a377864";
    sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";
    })
}:
(import reflex-platform {}).project ({ pkgs, ... }:{
  useWarp = true;

  packages = {
    todo-common = ./todo-common;
    todo-server = ./todo-server;
    todo-client = ./todo-client;
  };

  shells = {
    ghc = ["todo-common" "todo-server" "todo-client"];
    ghcjs = ["todo-common" "todo-client"];
  };
})
      
      





: reflex-platform



. nix



.

ghcid



. .







, , todo-client/src/Main.hs



:







{-# LANGUAGE OverloadedStrings #-}
module Main where

import Reflex.Dom

main :: IO ()
main = mainWidget $ el "h1" $ text "Hello, reflex!"
      
      





nix-shell



, shell:







$ nix-shell . -A shells.ghc
      
      





ghcid



:







$ ghcid --command 'cabal new-repl todo-client' --test 'Main.main'
      
      





, localhost:3003



Hello, reflex!















3003?



JSADDLE_WARP_PORT



. , 3003.









, GHCJS



, GHC



. jsaddle



jsaddle-warp



. jsaddle



JS - GHC



GHCJS



. jsaddle-warp



, - DOM



JS-. useWarp = true;



, jsaddle-webkit2gtk



, . , jsaddle-wkwebview



( iOS ) jsaddle-clib



( Android ).







TODO



!







todo-client/src/Main.hs



.







{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where

import Reflex.Dom

main :: IO ()
main = mainWidgetWithHead headWidget rootWidget

headWidget :: MonadWidget t m => m ()
headWidget = blank

rootWidget :: MonadWidget t m => m ()
rootWidget = blank
      
      





, mainWidgetWithHead



<html>



. — head



body



. mainWidget



mainWidgetWithCss



. body



. — , style



, — body



.







HTML , . HTML . , , , DOM



, , .

blank



pure ()



, DOM



.







<head>



.







headWidget :: MonadWidget t m => m ()
headWidget = do
  elAttr "meta" ("charset" =: "utf-8") blank
  elAttr "meta"
    (  "name" =: "viewport"
    <> "content" =: "width=device-width, initial-scale=1, shrink-to-fit=no" )
    blank
  elAttr "link"
    (  "rel" =: "stylesheet"
    <> "href" =: "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
    <> "integrity" =: "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
    <> "crossorigin" =: "anonymous")
    blank
  el "title" $ text "TODO App"
      
      





head



:







<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
<link crossorigin="anonymous" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
  integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" rel="stylesheet">
<title>TODO App</title>
      
      





MonadWidget



DOM



, , .







elAttr



:







elAttr :: forall t m a. DomBuilder t m => Text -> Map Text Text -> m a -> m a
      
      





, . , , DOM



, , . , blank



. — . el



. , — elAttr



. , — text



. — . , , , , . html, elDynHtml



.







, MonadWidget



, .. DOM



. , , MonadWidget



DOM



, . , , DomBuilder



, , , . , , , , . MonadWidget



, . , MonadWidget



:







type MonadWidgetConstraints t m =
  ( DomBuilder t m
  , DomBuilderSpace m ~ GhcjsDomSpace
  , MonadFix m
  , MonadHold t m
  , MonadSample t (Performable m)
  , MonadReflexCreateTrigger t m
  , PostBuild t m
  , PerformEvent t m
  , MonadIO m
  , MonadIO (Performable m)
#ifndef ghcjs_HOST_OS
  , DOM.MonadJSM m
  , DOM.MonadJSM (Performable m)
#endif
  , TriggerEvent t m
  , HasJSContext m
  , HasJSContext (Performable m)
  , HasDocument m
  , MonadRef m
  , Ref m ~ Ref IO
  , MonadRef (Performable m)
  , Ref (Performable m) ~ Ref IO
  )

class MonadWidgetConstraints t m => MonadWidget t m
      
      





body



, , :







newtype Todo = Todo
  { todoText :: Text }

newTodo :: Text -> Todo
newTodo todoText = Todo {..}
      
      





:







rootWidget :: MonadWidget t m => m ()
rootWidget =
  divClass "container" $ do
    elClass "h2" "text-center mt-3" $ text "Todos"
    newTodoEv <- newTodoForm
    todosDyn <- foldDyn (:) [] newTodoEv
    delimiter
    todoListWidget todosDyn
      
      





elClass



, () . divClass



elClass "div"



.







, foldDyn



. reflex



:







foldDyn :: (Reflex t, MonadHold t m, MonadFix m) => (a -> b -> b) -> b -> Event t a -> m (Dynamic t b)
      
      





foldr :: (a -> b -> b) -> b -> [a] -> b



, , , . Dynamic



, .. . -, Dynamic



. , Dynamic



. .







foldDyn



( ), . , .. (:)



.







newTodoForm



DOM



, , , Todo



. .







newTodoForm :: MonadWidget t m => m (Event t Todo)
newTodoForm = rowWrapper $
  el "form" $
    divClass "input-group" $ do
      iEl <- inputElement $ def
        & initialAttributes .~
          (  "type" =: "text"
          <> "class" =: "form-control"
          <> "placeholder" =: "Todo" )
      let
        newTodoDyn = newTodo <$> value iEl
        btnAttr = "class" =: "btn btn-outline-secondary"
          <> "type" =: "button"
      (btnEl, _) <- divClass "input-group-append" $
        elAttr' "button" btnAttr $ text "Add new entry"
      pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl
      
      





, , inputElement



. , input



. InputElementConfig



. , , , initialAttributes



. value



HasValue



, input



. InputElement



Dynamic t Text



. , input



.







, , elAttr'



. DOM



, , . , . domEvent



. , Click



, . :







domEvent :: EventName eventName -> target -> Event t (DomEventType target eventName)
      
      





. ()



.







, — tagPromptlyDyn



. :







tagPromptlyDyn :: Reflex t => Dynamic t a -> Event t b -> Event t a
      
      





, , , Dynamic



. .. , tagPromptlyDyn valDyn btnEv



btnEv



, , valDyn



. .







, , promptly



, — . , . tagPromplyDyn valDyn btnEv



, , tag (current valDyn) btnEv



. current



Behavior



Dynamic



. . Dynamic



Event



tagPromplyDyn



, .. , , Dynamic



. , tag (current valDyn) btnEv



, , current valDyn



, .. Behavior



, .







Behavior



Dynamic



: Behavior



Dynamic



, Dynamic



, Behavior



. , t1



t2



, Dynamic



, t1



[t1, t2)



, Behavior



(t1, t2]



.







todoListWidget



Todo



.







todoListWidget :: MonadWidget t m => Dynamic t [Todo] -> m ()
todoListWidget todosDyn = rowWrapper $
  void $ simpleList todosDyn todoWidget
      
      





simpleList



. :







simpleList
  :: (Adjustable t m, MonadHold t m, PostBuild t m, MonadFix m)
  => Dynamic t [v]
  -> (Dynamic t v -> m a)
  -> m (Dynamic t [a])
      
      





reflex



, DOM



, div



. Dynamic



, , . :







todoWidget :: MonadWidget t m => Dynamic t Todo -> m ()
todoWidget todoDyn =
  divClass "d-flex border-bottom" $
    divClass "p-2 flex-grow-1 my-auto" $
      dynText $ todoText <$> todoDyn
      
      





dynText



text



, , Dynamic



. , , DOM



.







2 , : rowWrapper



delimiter



. . :







rowWrapper :: MonadWidget t m => m a -> m a
rowWrapper ma =
  divClass "row justify-content-md-center" $
    divClass "col-6" ma
      
      





delimiter



-.







delimiter :: MonadWidget t m => m ()
delimiter = rowWrapper $
  divClass "border-top mt-3" blank
      
      











.







, Todo



. . .








All Articles