前書き
みなさん、こんにちは!私の名前はニキータです。TypeableではFRPアプローチを使用して、一部のプロジェクトのフロントエンドを開発しています。具体的には、WebフレームワークであるHaskellでの実装reflex
です。ロシア語のリソースに関するこのフレームワークのマニュアルはありません(そして英語のインターネットにはそれほど多くはありません)ので、少し修正することにしました。
この一連の記事では、を使用してHaskellWebアプリケーションを構築する方法について説明しreflex-platform
ます。reflex-platform
パッケージreflex
とを提供しますreflex-dom
。このパッケージreflex
は、関数型リアクティブプログラミング(FRP)のHaskell実装です。ライブラリにreflex-dom
は、を操作するための多数の関数、クラス、およびタイプが含まれていDOM
ます。これらのパッケージは別のものです。FRPアプローチは、Web開発だけでなく使用できます。Todo List
タスク一覧でさまざまな操作ができるアプリケーションを開発します。
この一連の記事を理解するには、Haskellプログラミング言語のゼロ以外のレベルの知識が必要であり、関数型リアクティブプログラミングの予備知識が役立ちます。
FRP. , — , :
Behavior a
— , . , .Event a
— . , .
reflex
:
Dynamic a
—Behavior a
Event a
, .. , , , , ,Behavior a
.
reflex
— . , . , , , , .., .
, 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
. . .