Type level map을 사용하여 preload 바람 관련 리소스를 처리합니다.색인 추가

46208 단어 Haskelltech
이 글은 Haskell Advent Calendar 2020 11일째 되는 글 (지연) 이다.
섣달 그날.어떻게 지내십니까?
Haskell에서api server 등을 제작할 때 rdb에 문의할 때 Proeload식의 일을 하는 곳, Type level map을 사용하는 곳은 상당히 사용이 편리하기 때문에 공유합니다.
더 나아가 Indexed Monad를 사용해 눈에 띄는 아쉬움을 개선했다.

preload?


1+N 문제가 발생하지 않도록 사전에 관련 자원에 문의해야 한다.프리페치 같은 거.
posts = Post.find(...) # query Post
# view
for p in posts
  for t in p.tags # ここで問い合わせるとN回なので、事前に問い合わせていて欲しい
    render t

할 일

  • view가 요청한preload
  • 를 정의할 수 있음
  • preload를 실행하는 함수 제공
  • 간단한 실현


  • 사용type-level-sets
  • 작은 함수 m 생략m
  • Proeload용 지원 준비

  • MonadService.queryM은 DB에 문의하는 기존 함수
  • 입니다.
  • 여기서 사용HRR하지만 주제와 무관
  • load는 1-1자원,loadList는 1-n자원의preload 함수
  • (->>)load,loadList를 연결하여 여러 개의 관련 자원을 읽은 맵을 만듭니다.(->>=) 네스트된 연관 ← 아쉬운 점 읽기 1
  • PreLoader.hs
    {-# LANGUAGE DataKinds        #-}
    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE RankNTypes       #-}
    {-# LANGUAGE TypeOperators    #-}
    
    module PreLoader where
    
    import           App
    import qualified Data.Map                           as M
    import           Data.Type.Map
    import           Database.Relational.Monad.BaseType (Relation)
    import           Util
    
    type TMap = M.Map
    type MkLoader ek r e = forall m k v . MonadService m
      => Var k -> [ek] -> Map v -> m (r, Map ((k ':-> M.Map ek e) : v))
    type Loader k a = MkLoader k [a] a
    type ListLoader k a = MkLoader k [a] [a]
    
    (->>) :: Monad m => m (a, t) -> (t -> m b) -> m b
    (->>) f g = f >>= \(_, b) -> g b
    
    (->>=) :: Monad m => m (a, b) -> (a -> b -> m b1) -> m b1
    (->>=) f g = f >>= uncurry g
    
    load ::
      (FromSql SqlValue k1, FromSql SqlValue a, MonadService m1,
       Ord k1) =>
      (t -> Relation () (k1, a))
      -> Var k2
      -> t
      -> Map m2
      -> m1 ([a], Map ((k2 ':-> M.Map k1 a) : m2))
    load query key ids m = do
      rs <- queryM (relationalQuery' (query ids) []) ()
      pure (snd <$> rs, Ext key (M.fromList rs) m)
    
    loadList ::
      (FromSql SqlValue a, FromSql SqlValue b, MonadService m1, Ord a) =>
      (t -> Relation () (a, b))
      -> Var k
      -> t
      -> Map m2
      -> m1 ([b], Map ((k ':-> M.Map a [b]) : m2))
    loadList query key ids m = do
      rs <- queryM (relationalQuery' (query ids) []) ()
      return (snd <$> rs, Ext key (groupList rs) m)
    

    Service 등의 리소스로 preload 수행

  • 여러 레이블이 있는 작업 예제
  • HRR은 위에서 설명한 것과 동일하지만 릴리즈select hoge.fk, hoge.* from hoge where id in (...)

  • Query.hs
    makeInclude ::
      (PersistableWidth b, LiteralSQL a,
       Num a) =>
      Relation () b -> Pi b a -> [a] -> Relation () (a, b)
    makeInclude t k ids = relation $ do
      u <- query t
      wheres $ u ! k `in'` values' ids
      return $ (u ! k) >< u
    
    includeTags :: [TaskId] -> Relation () (TaskId, TaskTag)
    includeTags = makeInclude taskTag #taskId
    
  • 모든 Task 관련 리소스를 읽을 수 있는 함수를 정의합니다
  • .
    Service.hs
    import           Data.Map    (Map)
    import           PreLoader
    import           Entity
    import           Query
    import           Type
    
    type TagsMap = Map TagId [TaskTag]
    
    loadTags :: ListLoader TagId TaskTag
    loadTags = loadList includeTags
    
    loadTaskRelation rs =
      loadTags (Var :: Var "tags") (view #id <$> rs)
    

    설명 View

  • 모든view에서 요청한preload
  • 를 정의할 수 있습니다
    View.hs
    renderTask :: ( IsMember "tags" TagsMap c
                  ) =>
                  Map c -> Task -> ViewM TaskResponse
    renderTask rel t = do
      ts <- getList (t ^. #id) (Var :: Var "tags") rel
      vTags <- mapM renderTag ts
      pure $ TaskResponse
          { id = t ^. #id
          , name = t ^. #name
          , tags = vTags
          }
    
    renderTag :: TaskTag -> ViewM String
    renderTag = pure . view #name
    
  • 다음과 같은 조수를 준비했습니다
  • View/Helper.hs
    get :: (KnownSymbol v, Ord k, IsMember v (M.Map k a) m) => Var v -> Map m -> k -> ViewM a
    get key m rid = maybe e pure $ M.lookup rid $ lookp key m
      where e = ...
    
    getList :: (KnownSymbol v, Ord k, IsMember v (M.Map k [a]) m) => k -> Var v -> Map m -> ViewM [a]
    getList rid key m = pure $ fromMaybe [] $  M.lookup rid $ lookp key m
    

    Handler


    사용만 남았어요.
    Handler.hs
    getTasksR :: AuthUser -> AppM [TaskResponse]
    getTasksR au = do
      xs <- getTasks ...
      related <- snd <$> loadTaskRelation xs TM.Empty
      runViewM $ mapM (renderTask related) xs
    

    이용 예


    필요한preload를 실행하지 않고view를 호출하면 컴파일 오류가 발생합니다
    > renderTask Empty undefined
    
    <interactive>:121:1: error:
        • No instance for (IsMember "tags" TagsMap '[])
            arising from a use of ‘renderTask’
        • In the expression: renderTask Empty undefined
          In an equation for ‘it’:
              it = renderTask Empty undefined
    
    view의 삽입도 적당한 정의가 되었다.
    renderTask :: ( IsMember "tags" TagsMap c
                  , IsMember "followers" UserMap c
                  , IsMember "userImages" ImageMap c -- renderUserで要求されている
                  ) =>
                  Map c -> Task -> ViewM TaskResponse
    renderTask rel t = do
      fs <- getList (t ^. #id) (Var :: Var "followers") rel
      vFollowers <- mapM (renderUser rel) fs
      ...
      
    renderUser :: (IsMember "userImages" ImageMap c) =>
                  Map c -> User -> ViewM String
    renderUser = ...
    

    Indexed Monad


    위의 실현에서 여러 개의preload를 실행하는 코드는 다음과 같다.
    loadTasksRelation rs =
      snd <$> loadTags (Var :: Var "tags") (view #id <$> (rs :: [Task])) TM.Empty
      ->> loadTaskFollowers (Var :: Var "followers") (view #id <$> rs)
      ->>= \xs -> loadUserImages (Var :: Var "userImages") (view #id <$> xs)
    
    상태로 처리하고 싶지만 계산 과정에서 금형이 변하기 때문에 일반적인 경우모나드는 사용할 수 없다.
    Indexed Monad를 사용하면
    loadRel :: ??
    loadRel = do
      loadHoge -- この時点の型:: State ("hoges" -> ...) a
      loadFuga -- この時点の型:: State ("hoges" -> ..., "fuga" -> ...) a
    
    가능할 것 같습니다.
    [참조]
  • 모나드의 새로운 힘!슈퍼 빛의 아름다움 소녀! ~장르 안전광의 아름다움 소녀!기술을 지탱하는 4~
  • 설치(IxState)

  • 다음 라이브러리 사용
  • indexed-extas
  • do-notation
  • 이후의 코드는 실제 응용한 적이 없으니 잠시 운행해 보았으니 주의하십시오m
  • Preloader

  • IxStateT를 사용하는 설치로 변경되었습니다.더 이상 필요 없음(->>)(->>=)
  • PreLoader.hs
    import           Control.Monad.Indexed
    import           Control.Monad.Indexed.State
    import           Control.Monad.Indexed.Trans        (ilift)
    import           Language.Haskell.DoNotation
    import           Prelude                            hiding (Monad (..), pure)
    
    runLoader x = fmap snd $ runIxStateT x Empty
    
    iloadList ::
      (FromSql SqlValue a, FromSql SqlValue b, Ord a) =>
      (t -> Relation () (a, b))
      -> Var k
      -> t
      -> IxStateT AppM (Map m) (Map ((k ':-> M.Map a [b]) : m)) [b]
    iloadList q k ids = do
      rs <- ilift $ queryM (relationalQuery' (q ids) []) ()
      imodify (\x -> Ext k (groupList rs) x)
      return $ snd <$> rs -- not Applicative
    
    

    Service


    이렇게 preload를 실행하는 코드는 다음과 같다.
    Task.hs
    iloadTasksRelation xs = runLoader $ do
      iloadTags (Var @ "tags") (Entity.Task.id <$> xs)
      us <- iloadTaskFollowers (Var @ "followers") (Entity.Task.id <$> xs)
      iloadUserImages (Var @ "userImages") (Entity.User.id <$> us)
    
    괜찮아 보여요.
    이후 서비스와 뷰의 창고와 연결되면 rails,django 같은 논리적인 프리로드 기술도 가능하고(형 안전은 물론) 내년에도 좋은 해를 보낼 수 있다.

    좋은 웹페이지 즐겨찾기