Reflection을 사용하여 Haskell 응용 프로그램을 만듭니다.제3부분

저자: 니키타 안니시모프
안녕하세요!본고에서 우리는 EventWriter류와 ghcjs-dom라이브러리를 어떻게 사용하는지 토론할 것이다.

EventWriter 사용


현재 실행 중, 플러그인 단계에서 이벤트를 보내기 위해, 우리는 이벤트를 반환값으로 전달합니다.이것은 항상 편리하지 않습니다. 특히 이벤트 이외의 내용을 되돌려야 할 때 (예를 들어 폼을 입력하면 클릭한 이벤트와 폼의 데이터를 동시에 되돌릴 수 있습니다.)이벤트를 자동으로 맨 위에 보낼 수 있는 메커니즘을 사용하면 이벤트로 되돌아오는 번거로움을 줄일 수 있습니다.이런 메커니즘은 확실히 존재한다.이 클래스는 표준 EventWriter monad와 비슷한 이벤트를 쓸 수 있습니다.애플리케이션을 다시 작성하려면 Writer을 사용하십시오.
우선 EventWriter류를 고려해 봅시다.
class (Monad m, Semigroup w) => EventWriter t w m | m -> t w where
  tellEvent :: Event t w -> m ()
EventWriter형이 바로 우리가 활동하는 유형이다.이 유형은 w류의 실례로 이 유형의 값을 조합할 수 있다.만약 Semigroup을 사용하여 두 개의 다른 이벤트를 기록하고 어느 시점에서 동시에 실행한다면, 모든monad가 실행하면 하나의 이벤트를 초래할 수 있도록 어떤 방식으로 이 이벤트를 같은 종류의 이벤트로 조합해야 한다.
transformer가 이 종류를 대표하는 실례가 하나 있는데 그것이 바로 tellEvent이다.함수 EventWriterT을 사용하여 실행할 수 있습니다.
그 후에 우리는 함수를 바꾸기 시작했다.기능 runEventWriterT은 가장 큰 변화를 겪을 것입니다.
rootWidget :: MonadWidget t m => m ()
rootWidget =
  divClass "container" $ mdo
    elClass "h2" "text-center mt-3" $ text "Todos"
    (_, ev) <- runEventWriterT $ do
      todosDyn <- foldDyn appEndo mempty ev
      newTodoForm
      delimiter
      todoListWidget todosDyn
    blank
transformer runner 호출을 추가하고 모든 반환 이벤트를 삭제했습니다.
비록 rootWidget의 변화는 그다지 현저하지 않지만, 그것들은 여전히 언급할 가치가 있다.
newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()
newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo
  iEl <- inputElement $ def
    & initialAttributes .~
      (  "type" =: "text"
      <> "class" =: "form-control"
      <> "placeholder" =: "Todo" )
    & inputElementConfig_setValue .~ ("" <$ btnEv)
  let
    addNewTodo = \todo -> Endo $ \todos ->
      insert (nextKey todos) (newTodo todo) todos
    newTodoDyn = addNewTodo <$> value iEl
    btnAttr = "class" =: "btn btn-outline-secondary"
      <> "type" =: "button"
  (btnEl, _) <- divClass "input-group-append" $
    elAttr' "button" btnAttr $ text "Add new entry"
  let btnEv = domEvent Click btnEl
  tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl
보시다시피 함수 형식이 업데이트되어 아무것도 돌아오지 않습니다.필요한 구속 newTodoForm도 추가했습니다.이에 따라 우리는 함수체에서 반환값을 삭제하고 현재 EventWriter 함수를 사용하고 있다.
함수 tellEvent이 훨씬 간단해졌다.
todoListWidget
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Dynamic t Todos -> m ()
todoListWidget todosDyn = rowWrapper $
  void $ listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget
현재 우리는 되돌아오는 사건에 전혀 관심이 없기 때문에 todoListWidget에서 Event을 추출할 필요가 없다.Dynamic의 기능도 눈에 띄게 달라졌다.현재 우리는 되돌아오는 형식을 사용하고 todoWidget을 변환할 필요가 없다.함수 Event t (Event t TodoEvent)dyn_의 다른 점은 전자가 되돌아오는 값을 무시하는 데 있다.
todoWidget
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Int -> Dynamic t Todo -> m ()
todoWidget ix todoDyn' = do
  todoDyn <- holdUniqDyn todoDyn'
  dyn_ $ ffor todoDyn $ \td@Todo{..} -> case todoState of
    TodoDone         -> todoDone ix todoText
    TodoActive False -> todoActive ix todoText
    TodoActive True  -> todoEditable ix todoText
함수 dyn, todoDone, todoActive의 유일한 변화는 되돌아오는 것이 아니라 새로운 형식과 이벤트 쓰기입니다.
todoActive
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Int -> Text -> m ()
todoActive ix todoText = divClass "d-flex border-bottom" $ do
  divClass "p-2 flex-grow-1 my-auto" $
    text todoText
  divClass "p-2 btn-group" $ do
    (doneEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Done"
    (editEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Edit"
    (delEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Drop"
    tellEvent $ Endo <$> leftmost
      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
      , update (Just . startEdit) ix  <$ domEvent Click editEl
      , delete ix <$ domEvent Click delEl
      ]

todoDone
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Int -> Text -> m ()
todoDone ix todoText = divClass "d-flex border-bottom" $ do
  divClass "p-2 flex-grow-1 my-auto" $
    el "del" $ text todoText
  divClass "p-2 btn-group" $ do
    (doneEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Undo"
    (delEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Drop"
    tellEvent $ Endo <$> leftmost
      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
      , delete ix <$ domEvent Click delEl
      ]

todoEditable
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Int -> Text -> m ()
todoEditable ix todoText = divClass "d-flex border-bottom" $ do
  updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $
    editTodoForm todoText
  divClass "p-2 btn-group" $ do
    (doneEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Finish edit"
    let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix
    tellEvent $
      tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)
todoEditable류의 사용은 코드를 더욱 간단하고 읽기 쉽게 한다.

ghcjs dom

EventWriterreflex만 수정할 수 있지만 JS 응용 프로그램은 보통 더 많은 작업을 해야 한다.예를 들어 단추를 누르면 텍스트를 복사할 수 있습니다. DOM은 우리에게 필요한 도구를 제공할 수 없습니다.reflex 도서관이 구조하러 온다.본질적으로 이것은 Haskell의 ghcjs-dom이 실현한 것이다.여기서 JS와 같은 유형과 함수를 찾을 수 있습니다.
일반 JS에서 타사 라이브러리를 사용하지 않는 경우 텍스트 복사 기능은 다음과 같습니다.
function toClipboard(txt){
  var inpEl = document.createElement("textarea");
  document.body.appendChild(inpEl);
  inpEl.value = txt
  inpEl.focus();
  inpEl.select();
  document.execCommand('copy');
  document.body.removeChild(inpEl);
}
일반적인 방법은 이 이벤트 처리 프로그램을 단추에 추가하는 것입니다.
하스켈은 어떻게 될까?우선, 우리는 새로운 JS API 모듈을 창설하여 GHCJS과 함께 작업하고 관련 기능을 정의한다.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MonoLocalBinds #-}
module GHCJS where

import Control.Monad
import Data.Functor (($>))
import Data.Text (Text)
import GHCJS.DOM
import GHCJS.DOM.Document
  (createElement, execCommand, getBodyUnchecked)
import GHCJS.DOM.Element as Element hiding (scroll)
import GHCJS.DOM.HTMLElement as HE (focus)
import GHCJS.DOM.HTMLInputElement as HIE (select, setValue)
import GHCJS.DOM.Node (appendChild, removeChild)
import GHCJS.DOM.Types hiding (Event, Text)
import Reflex.Dom as R

toClipboard :: MonadJSM m => Text -> m ()
toClipboard txt = do
  doc <- currentDocumentUnchecked
  body <- getBodyUnchecked doc
  inpEl <- uncheckedCastTo HTMLInputElement <$> createElement doc
    ("textarea" :: Text)
  void $ appendChild body inpEl
  HE.focus inpEl
  HIE.setValue inpEl txt
  HIE.select inpEl
  void $ execCommand doc ("copy" :: Text) False (Nothing :: Maybe Text)
  void $ removeChild body inpEl
haskell 함수 ghcjs의 거의 모든 줄은 JS 함수에 일치하는 줄이 있습니다.우리가 여기에 익숙하지 않은 toClipboard류는 언급해야 한다.반대로 우리는 MonadWidget을 사용하는데 이것은 MonadJSM을 사용하여 모든 업무를 수행하는 리스트이다.ghcjs-dom 유형 계승 MonadWidget.프로세서가 이벤트에 어떻게 연결되는지 보여 줍니다.
copyByEvent :: MonadWidget t m => Text -> Event t () -> m ()
copyByEvent txt ev =
  void $ performEvent $ ev $> toClipboard txt
처리 프로그램을 이벤트에 연결하는 데 사용되는 새로운 함수 MonadJSM을 보았습니다.이 함수는 performEvent 클래스의 메서드입니다.
class (Reflex t, Monad (Performable m), Monad m) => PerformEvent t m | m -> t where
  type Performable m :: * -> *
  performEvent :: Event t (Performable m a) -> m (Event t a)
  performEvent_ :: Event t (Performable m ()) -> m ()
이제 PerformEvent이 추가되었는지 확인한 후 중단된 작업 위젯을 변경합니다.
todoActive
  :: (EventWriter t TodoEvent m, MonadWidget t m) => Int -> Todo -> m ()
todoActive ix Todo{..} =
  divClass "d-flex border-bottom" $ do
    divClass "p-2 flex-grow-1 my-auto" $
      text todoText
    divClass "p-2 btn-group" $ do
      (copyEl, _) <- elAttr' "button"
        (  "class" =: "btn btn-outline-secondary"
        <> "type" =: "button" ) $ text "Copy"
      (doneEl, _) <- elAttr' "button"
        (  "class" =: "btn btn-outline-secondary"
        <> "type" =: "button" ) $ text "Done"
      (editEl, _) <- elAttr' "button"
        (  "class" =: "btn btn-outline-secondary"
        <> "type" =: "button" ) $ text "Edit"
      (delEl, _) <- elAttr' "button"
        (  "class" =: "btn btn-outline-secondary"
        <> "type" =: "button" ) $ text "Drop"
      copyByEvent todoText $ domEvent Click copyEl
      tellEvent $ leftmost
        [ ToggleTodo ix <$ domEvent Click doneEl
        , StartEditTodo ix <$ domEvent Click editEl
        , DeleteTodo ix <$ domEvent Click delEl
        ]
우리는 새로운 단추 import GHCJS과 특정한 함수 호출 Copy을 추가했다.다른 작업 상태에 사용되는 작은 위젯도 이를 할 수 있다.
여느 때와 마찬가지로 우리가 얻은 결과는 in our repository을 찾을 수 있다.
다음 글에서는 JSFFI(JS 외부 함수 인터페이스)를 어떻게 사용하는지 토론할 것이다.

좋은 웹페이지 즐겨찾기