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

저자: 니키타 안니시모프
안녕하세요!반사 기반 웹 응용 프로그램 개발에 주력하기 위해 일련의 강좌를 계속합시다.
이 부분에서, 우리는 작업 목록에 각종 조작을 수행할 수 있는 능력을 추가할 것이다.

Todo의 작업


작업을 완료됨으로 선택하고 편집하고 삭제할 수 있습니다.
우선, 상태를 추가해서 Todo 유형을 확장합니다.작업이 완료되지 않은 경우 편집할 수 있습니다.
data TodoState
  = TodoDone
  | TodoActive { stateEdit :: Bool }
  deriving (Generic, Eq, Show)

data Todo = Todo
  { todoText  :: Text
  , todoState :: TodoState }
  deriving (Generic, Eq, Show)

newTodo :: Text -> Todo
newTodo todoText = Todo { todoState = TodoActive False, .. }
그리고 시스템에서 발생하는 이벤트를 정의합니다.우리의 상업 프로젝트에서, 우리는 이를 위해 두 가지 방법을 사용했다.첫 번째 방법은 모든 가능한 사건을 하나의 구조 함수로 매거하고 처리 함수를 실현하는 것을 의미하며 이 함수는 발생하는 이벤트에 따라 상태를 업데이트한다.
data TodoEvent
  = NewTodo Todo
  | ToggleTodo Int
  | StartEditTodo Int
  | FinishEditTodo (Text,Int)
  | DeleteTodo Int
  deriving (Generic, Eq, Show)
이런 방법의 장점은 시스템에서 구체적으로 어떤 사건이 발생했는지, 그리고 그것이 가지고 있는 업데이트를 볼 수 있다는 것을 포함한다. (모든 것은 traceEvent 함수로 이루어진 것이다.)그러나 이러한 장점을 이용하는 것은 항상 가능한 것이 아니다. 특히 사건이 최종적으로 분석하기 어려운 대량의 데이터를 포함할 때.값의 변화를 볼 필요가 있다면, 이벤트는 어떤 상황에서도 변할 수 있습니다. Dynamic 함수 traceDyn 를 사용하여 값을 추적할 수 있습니다.
두 번째 방법은 반군 Endo 으로 표시된 업데이트 함수(대략적으로 말하면 매개 변수와 결과 유형이 겹치는 함수의 추상화)를 사용하는 것이다.이런 방법의 본질은 업데이트 이벤트가 가지고 있는 값이 업데이트 논리 자체를 정의하는 함수라는 것이다.이러한 상황에서 우리는 이벤트 값을 표시하는 능력을 잃었다. (사실은 이 능력이 항상 유용하지 않다는 것을 증명한다.) 그러나 뚜렷한 장점은 현재 상태에 접근하거나 모든 가능한 이벤트를 포함하는 유형을 만들거나, 받은 이벤트에 따라 업데이트된 상태를 정의하는 단일 처리 프로그램이 필요하지 않다는 것이다.
이 강좌에서 우리는 두 번째 방법을 사용할 것이다.
루트 위젯의 구조를 변경합니다.
rootWidget :: MonadWidget t m => m ()
rootWidget =
  divClass "container" $ do
    elClass "h2" "text-center mt-3" $ text "Todos"
    newTodoEv <- newTodoForm
    rec
      todosDyn <- foldDyn appEndo mempty $ leftmost [newTodoEv, todoEv]
      delimiter
      todoEv <- todoListWidget todosDyn
    blank
우리가 먼저 본 것은 RecursiveDo 확장된 사용 (따라서 사용해야 함) 이다.이것은 reflex 응용 프로그램 개발에서 가장 보편적인 실천 중 하나이다. 왜냐하면 페이지 밑에 발생하는 이벤트가 페이지 맨 위 요소에 영향을 주는 경우가 매우 흔하기 때문이다.이 예에서 이벤트todoEv는 정의todosDyn에 사용되고 todosDyn는 이벤트가 나오는 작은 위젯의 매개 변수입니다.
다음에 우리는 todoEv 함수 매개 변수의 업데이트를 볼 것이다.여기에는 새로운 기능foldDyn이 적용됐다.이벤트 목록을 받아들여 이벤트 목록에서 이벤트가 발생했을 때 발생한 이벤트를 되돌려줍니다.목록에 있는 두 이벤트가 정해진 시간에 발생하면 가장 왼쪽에 있는 이벤트로 돌아갑니다.퀘스트 열은 목록이 아니라 leftmost 에 나타납니다IntMap.우선, 이렇게 하는 것은 우리가 표지부를 통해 요소에 직접 접근할 수 있도록 하기 위해서이다.type Todos = IntMap Todo는 목록을 업데이트하는 데 사용됩니다.각 이벤트를 별도의 구조 함수로 정의할 경우 다음과 같이 처리 함수를 정의해야 합니다.
updateTodo :: TodoEvent -> Todos -> Todos
updateTodo ev todos = case ev of
  NewTodo todo           -> nextKey todos =: todo <> todos
  ToggleTodo ix          -> update (Just . toggleTodo) ix todos
  StartEditTodo ix       -> update (Just . startEdit) ix todos
  FinishEditTodo (v, ix) -> update (Just . finishEdit v) ix todos
  DeleteTodo ix          -> delete ix todos
비록 이 함수를 정의할 필요는 없지만, 우리는 여기서 다른 몇 개의 보조 함수를 사용했다. 어쨌든 우리는 더 많은 기능을 필요로 한다.
startEdit :: Todo -> Todo
startEdit todo = todo { todoState = TodoActive True }

finishEdit :: Text -> Todo -> Todo
finishEdit val todo = todo 
  { todoState = TodoActive False, todoText = val }

toggleTodo :: Todo -> Todo
toggleTodo Todo{..} = Todo {todoState = toggleState todoState,..}
  where
    toggleState = \case
      TodoDone     -> TodoActive False
      TodoActive _ -> TodoDone

nextKey :: IntMap Todo -> Int
nextKey = maybe 0 (succ . fst . fst) . maxViewWithKey
새 요소를 추가하는 함수도 변경되었습니다. 현재 되돌아오는 것은 작업 자체가 아니라 이벤트입니다.새 작업이 추가되면 필드 정리도 추가합니다.
newTodoForm :: MonadWidget t m => m (Event t (Endo Todos))
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
  pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl
함수 appEndo 가 작업 변경 사항을 되돌려줍니다.그것도 경미한 변화가 생겼다.
todoListWidget :: MonadWidget t m => Dynamic t Todos -> m (Event t (Endo Todos))
todoListWidget todosDyn = rowWrapper $ do
  evs <- listWithKey 
    (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget
  pure $ switchDyn $ leftmost . M.elems <$> evs
우리가 발견한 첫 번째 일은 todoListWidget 함수가 simpleList 대체되는 것이다.그것들은 첫 번째 매개 변수의 유형에 있어서 서로 다르다. 첫 번째 함수 수용 목록 listWithKey, 두 번째 함수 수용 목록 [].목록은 키를 눌러 정렬됩니다.여기서 되돌아오는 값은 매우 중요하다.모든 작업이 이벤트 (삭제, 변경) 를 되돌려줍니다.구체적인 경우Map 함수는 다음과 같습니다.
listWithKey
  :: MonadWidget t m
  => Dynamic t (Map Int Todo)
  -> (Int -> Dynamic t Todo -> m (Event t TodoEvent))
  -> m (Dynamic t (Map Int TodoEvent))

Note: this function is a part of the reflex package and has a more complex type. Here we show the specialized type.


여기서 우리는 모든 listWithKey 값에 대해 익숙한 leftmost 함수를 사용한다.표현식Map의 유형은 다음과 같다: leftmost . elems <$> evs.우리는 함수Dynamic t (Event t TodoEvent)를 사용하여 switchDyn에서 검색Event을 한다.이 함수는 내부 이벤트가 발생했을 때 발생한 이벤트를 되돌려줍니다.DynamicDynamic가 동시에 발생하면 Event의 이벤트가 업데이트될 때까지 이전 Event로 돌아갑니다.함수Dynamic의 조작 방식은 다르다. switchPromtlyDyn 업데이트, Dynamic 업데이트 전에 존재하는 이벤트의 발생과 현재 포함Dynamic의 이벤트의 트리거가 동시에 발생하면 포함Dynamic의 새로운 이벤트를 되돌려준다.만약 이런 상황이 불가능하다면 Dynamic를 사용하는 것이 가장 좋다. switchDyn 함수가 더욱 복잡하기 때문에 추가 작업을 수행할 수 있고 순환을 만들 수 있기 때문이다.
임무가 다른 상태를 얻었는데 이것이 바로 임무를 대표하는 함수도 왜 바뀌었는가이다.
todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))
todoWidget ix todoDyn = do
  todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
    TodoDone         -> todoDone ix todoText
    TodoActive False -> todoActive ix todoText
    TodoActive True  -> todoEditable ix todoText
  switchHold never todoEvEv
여기서 우리가 사용하는 것은 새로운 함수switchPromtlyDyn다.다음과 같은 유형이 있습니다.
dyn
  :: (Adjustable t m, NotReady t m, PostBuild t m)
  => Dynamic t (m a)
  -> m (Event t a)
그것은 dyn에 봉인된 작은 위젯을 입력 매개 변수로 받아들인다.이는 매번 업데이트Dynamic 이후에Dynamic 업데이트가 있다는 것을 의미한다.출력 값은 작은 위젯이 되돌아오는 값을 가진 이벤트입니다.우리의 예에서 전용 유형은 다음과 같다.
dyn
  :: MonadWidget t m
  => Dynamic t (m (Event t (Endo Todos)))
  -> m (Event t (Event t (Endo Todos)))
여기서 우리는 다른 사건에 끼워 넣은 사건을 만났다.DOM 패키지의 두 함수는 이런 유형에 대해 작업을 수행할 수 있다. reflexcoincidence.첫 번째 함수는 외부와 내부 이벤트가 동시에 발생할 때만 발생하는 이벤트를 되돌려줍니다.이것은 우리의 상황이 아니다.기능switchHold의 유형은 다음과 같습니다.
switchHold :: (Reflex t, MonadHold t m) => Event t a -> Event t (Event t a) -> m (Event t)
이 함수는 외부 이벤트가 발생할 때마다 새 이벤트로 전환됩니다.외부 이벤트가 처음 발생할 때까지 첫 번째 매개 변수로 전달되는 이벤트는 유지됩니다.이것이 바로 우리가 예시에서 이 함수를 사용하는 방식이다.목록을 처음 변경하기 전에 목록에 이벤트가 나타나지 않기 때문에 switchHold 이벤트를 사용합니다.말 그대로 한 번도 없었던 특별한 사건이었다.
함수never는 서로 다른 상태에 대해 서로 다른 작은 부품을 사용한다.
todoActive :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
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"
    pure $ Endo <$> leftmost
      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
      , update (Just . startEdit) ix  <$ domEvent Click editEl
      , delete ix <$ domEvent Click delEl
      ]

todoDone :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
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"
    pure $ Endo <$> leftmost
      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
      , delete ix <$ domEvent Click delEl
      ]

todoEditable :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
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
    pure $
      tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)

editTodoForm :: MonadWidget t m => Text -> m (Dynamic t Text)
editTodoForm todo = do
  editIEl <- inputElement $ def
    & initialAttributes .~
      (  "type" =: "text"
      <> "class" =: "form-control"
      <> "placeholder" =: "Todo")
    & inputElementConfig_initialValue .~ todo
  pure $ value editIEl
여기에 사용된 모든 함수는 이전에 설명되어 있기 때문에 우리는 모든 함수를 상세하게 설명하지 않을 것이다.

최적화


다시 todoWidget 함수로 돌아가겠습니다.
listWithKey
  :: MonadWidget t m
  => Dynamic t (Map Int Todo)
  -> (Int -> Dynamic t Todo -> m (Event t TodoEvent))
  -> m (Dynamic t (Map Int TodoEvent))
이 기능은 전송된 데이터listWithKey에 대한 모든 업데이트가 각 개별 요소에 대한 업데이트를 시작하는 방식으로 작동합니다.개별 요소를 변경해도 값이 변경되지 않음에도 불구하고 업데이트는 각 요소에 전달됩니다.이제 Dynamic 함수로 돌아갑시다.
todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))
todoWidget ix todoDyn = do
  todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
    TodoDone         -> todoDone ix todoText
    TodoActive False -> todoActive ix todoText
    TodoActive True  -> todoEditable ix todoText
  switchHold never todoEvEv
기억하시는 바와 같이 todoWidget 함수는 업데이트할 때마다 dyn 업데이트됩니다.목록의 한 요소의 변경 사항이 각각 요소에 전달되는 것을 감안하면 작업의 전체 DOM 부분이 재구성됩니다. (브라우저의 '개발자' 패널을 사용하여 검사할 수 있습니다.)분명히 이것은 우리가 원하는 것이 아니다.이때todoDyn 기능이 구조 역할을 한다.

todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t TodoEvent)
todoWidget ix todoDyn' = do
  todoDyn <- holdUniqDyn todoDyn'
  todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
    TodoDone         -> todoDone ix td
    TodoActive False -> todoActive ix td
    TodoActive True  -> todoEditable ix td
  switchHold never todoEvEv
우리는 이미 이 줄을 추가했다. DOM이게 어떻게 된 일입니까?문제는 holdUniqDyn가 실행 중이지만 포함된 값은 변하지 않는다는 것이다.함수todoDyn <- holdUniqDyn todoDyn'는 이렇게 작동하기 때문에 전달된Dynamic이 실행되고 값이 바뀌지 않으면 출력holdUniqDyn이 실행되지 않기 때문에 우리의 예에서Dynamic는 불필요하게 재구성되지 않는다.

우리가 얻은 결과는 볼 수 있다in our repository.
다음 부분에서 우리는 사건을 관리하는 또 다른 방법과 GHCJS-DOM 라이브러리의 사용을 고려할 것이다.

좋은 웹페이지 즐겨찾기