하스켈학교 세미나 - haxl
TRANSCRIPT
Haxl한주영
Haxl - 2014년 Open● Facebook 오픈소스 발표● ICFP2014 - Siman Marlow
○ 페이퍼(pdf) ○ 동영상
하스켈은 그냥 공부만을 위한 언어였는데 , 이건 뭔가 프랙티컬 할 것 같은 느낌
Haxl 공개로 인해 비슷한 구현이 줄줄이...● Stitch (Twitter)
○ Scala 라이브러리(오픈 소스 아님)○ Introducing Stitch(YouTube)
● muse○ Clojure 라이브러리○ https://github.com/kachayev/muse
● Fetch○ Scala(.js) 라이브러리○ http://47deg.github.io/fetch/
● Jobba (Futurice)○ Scala 라이브러리(오픈 소스 아님)○ An example of functional design(Blog post)
특정 언어의 라이브러리가 여기 저기 포팅된다는 건 라이브러리 이상의 의미가 있다는 뜻
Haxl?Haxl is a Haskell library that simplifies access to remote data, such as databases or web-based services. Haxl can automatically
● batch multiple requests to the same data source,
● request data from multiple data sources concurrently,
● cache previous requests.
… your data-fetching code can be much cleaner and clearer
굉장히 일반적인 문제에 대한 해법. 널리 활용가능할 것 같음
There is no Fork: an Abstraction for Efficient, Concurrent, and Concise Data Access
Marlow, Simon, et al. "There is no fork: An abstraction for efficient, concurrent, and concise data access." ACM SIGPLAN Notices. Vol. 49. No. 9. ACM, 2014.
APA
Functional Pearls 같은 페이퍼● 친절하다.
○ 결과물만 소개하는 대신 라이브러리 설계 과정을 설명해준다 !○ 문제, 핵심 아이디어, 뼈대 코드, 여기에 기능을 하나씩 더해가며 발전시켜 나감
● github.com/facebook/Haxl 의 축약판○ 페이퍼는 핵심아이디어 위주로 설명○ 필요하다면 haxl을 직접 볼 수 있다. (아쉽지만 Initial commit이 이미 어느정도 완성형)○ 실제 코드는 훨씬 복잡 -- 그만큼 현실적
● 만들어진지 얼마되지 않은 라이브러리○ 군더더기가 적다
● 아무나가 아닌 Simon Marlow○ 하스켈 공부하다 마주치는 몇명의 Guru들 중 한 사람○ 특히 Parallel/Concurrent 쪽
https://github.com/simonmar
Key Point● Implicit concurrency via <*>
f <*> a
● Applicative는 branch를 들여다 볼 수 있음● f와 a를 모두 들여다보고 Batch/Concurrent fetching을 가능하게 함
● Caching이 가능해졌고 , 이에 따라● consistent 한 결과를 얻을 수 있고● replay 가능해 진 것은 덤
m >>= f
● m의 결과에 의존적
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Summary● Applicative abstraction for implicit concurrency
○ Concurrency monad + Applicative (to introduce concurrency)
● Battery Included (Cache)○ Performance & Consistent result
● With No Extra Cost○ mapM = traverse○ sequence = sequenceA○ ApplicativeDo
Typical exampledo a ← friendsOf x
b ← friendsOf y
return (length (intersect a b))
● friendsOf x와 friendsOf y는 independent ⇒ concurrent ● x,y 에 대해 friendsOf 라는 동일 서비스에 요청 ⇒ batch● x와 y가 같다면 x에 대해서만 요청 ⇒ cache
Typical exampledo a ← friendsOf x
b ← friendsOf y
return (length (intersect a b))
length <$> liftA2 intersect (friendsOf x) (friendsOf y)
ApplicativeDo 확장GHC 8.0.1에 추가됨
● 원래는 <*>와 ap는 같은 동일하게 동작해야 하지만● 관찰가능한 차이점이 없기 때문에 <*>를 최적화된 구현으로 동작하도록 변경 =>일종의 Hack이라고 볼 수 있음
Scala와 잠깐 비교def friendsOf(id: UserId): Future[Set[User]] = …
def numCommonFriends(x: UserId, y: UserId): Future[Int] =
for {
xs <- friendsOf(x)
ys <- friendsOf(y)
} yields (xs & ys).size
Cache는 global/implicit으로 적용 가능Batching은 어려울 듯...
Rendering a blog
● Types○ data PostId○ data Date○ data PostContent○ data PostInfo = PostInfo { postId:: PostId, postDate:: Date, postTopic:: String }
● DSL○ getPostIds :: Fetch [PostId]○ getPostInfo :: PostId → Fetch PostInfo○ getPostContent :: PostId → Fetch PostContent○ getPostViews :: PostId → Fetch Int
blog :: Fetch Htmlblog = renderPage <$> leftPane <*> mainPane
mainPane :: Fetch HtmlmainPane = do posts <- getAllPostsInfo :: Fetch [PostInfo] let ordered = … 최신 글 5개 contents <- mapM (getPostContent . postId) ordered return $ renderPosts (zip ordered content)
leftPane:: Fetch HtmlleftPane = renderSidePane <$> popularPosts <*> topics
data PostInfo = PostInfo { postId:: PostId, postDate:: Date, postTopic:: String }
Concurrency를 직접 사용하지 않는다그냥 Monad/Applicative/Traversable 일뿐
Quiz
getAllPostsInfo :: Fetch [PostInfo]getAllPostsInfo = do ids <- getPostIds mapM getPostInfo ids
getPostDetails :: PostId -> Fetch (PostInfo, PostContent)getPostDetails pid = … getPostInfo/getPostContent … 를 어떻게 결합할까? (,) <$> getPostInfo pid <*> getPostContent pid
직접 Batch를 신경쓰지 않아도 된다
Quiz
쉽게 쌓아올라갈 수 있다.
popularPosts :: Fetch HtmlpopularPosts = do pids <- getPostIds views <- mapM getPostViews pids let orderd :: [PostId] = … 뷰가 가장 많은 5개 … contents <- mapM getPostDetails ordered return (renderPostList contents)
topics :: Fetch Htmltopics = do posts <- getAllPostsInfo let topicCounts :: Map String Int = … 토픽 별 갯수 … return (renderTopics topicCounts)
직접 Batch를 신경쓰지 않아도 된다
Quiz
Blog example을 진행하면서● 동시성을 신경 쓰지 않아도 되고● Data fetch 순서 신경 쓰지 않아도 되고
○ 다른 언어 환경에서 Future/Promise 쓰는 경우에는 중요한 문제. Modularity를 해친다
● Biz logic에 집중할 수 있었다!
Fetch/Haxl을 구현한 다른 라이브러리는 이런 효과가 조금 떨어진다 . Why?
● Applicative에 implicit하게 녹여낸 것이 특징인데 ,● Scala의 경우 명시적으로 사용해야 함
ex) Stitch.traverse(...), Stitch.join(..) 기존의
실제 실행될 때는...topics, popularPosts, mainPane 세 군데에서 getPostIds로 Block된다. ⇒ 세번 fetch하는대신 한번만 하고, 그 결과 [PostId]를 각각의 Continuation에서 처리한다 .
topics와 mainPane은 getPostInfo를 위해 Block되고, popularPosts는 getPostViews에서 Block된다.⇒ getPostInfo요청과 getPostViews요청을 나누고 중복제거하여 Concurrent하게 fetch
이 단계에서 topics는 Done, popularPosts와 mainPane은 각각 getPostInfo와 getPostContent에서 Block된다. (blog입장에서는 여전히 Block상태)⇒ 다시 각각의 묶음으로 Concurrent fetch 진행
만약 Cache가 추가된다면 2단계 mainPane에서 가져온 PostInfo중 3단계 popularPosts에서 필요한 PostInfo와 겹치는 내용이 있으면 추가로 fetch할 필요가 없다.
Fetch 만들기
Fetch a - #1● Concurrency monad● Can pause and be resumed (resumption monad)
○ cooperative concurrency ( interleave/roundrobin 등을 구현해볼 수 있음 )
data Fetch a = Done a | Blocked (Fetch a)
계산이 끝났거나 (Done)뭔가에 의해 Block되었음. Block된 상황이 해결되면 Fetch a로 계속 이어감(continuation)이 경우, Continuation에서 필요한 데이터를 remote에서 가져와야 하는 것으로 볼 수 있음.
runFetch :: Fetch a -> arunFetch f = case f of Done a -> a Blocked c -> runFetch c
runFetchIO :: Fetch a -> IO arunFetchIO f = case f of Done a -> return a Blocked c -> putStrLn “fetch” >> runFetchIO c
A Poor Man's Concurrency Monad
Fetch a - #2● Applicative concurrency● “There is no fork”
data Fetch a = Done a | Blocked (Fetch a)
instance Applicative Fetch where pure = return
Done g <*> Done y = Done (g y) Done g <*> Blocked c = Blocked (g <$> c) Blocked c <*> Done y = Blocked (c <*> Done y) Blocked c <*> Blocked d = Blocked (c <*> d)
GHC 7.10 Guideline says
fmap = liftMpure = … define ...(<*>) = apreturn = pure(>>=) = … define ...
따로 Applicative를 구현하여 Block된 상황을 모아서 한번에 처리할 수 있도록 함.
Applicative vs MonadBlocked (Done (+1)) <*> Blocked (Done 1)⇒ Blocked (Done (+1) <*> Done 1)⇒ Blocked (Done (1 + 1))
Blocked (Done (+1)) <*> Blocked (Done 1)⇒ Blocked ((+1) <$> Blocked (Done 1))⇒ Blocked (Blocked ((+1) <$> Done 1)⇒ Blocked (Blocked (Done (1 + 1)))
With (<*>) = ap
ap :: (Monad m) => m (a->b) -> m a -> m bap m1 m2 = do x1 <- m1 x2 <- m2 return (x1 x2)
Done f <*> x = f <$> xBlocked c <*> x = Blocked (c <*> x)
Blocked가 Remote data fetch라면 Monad `ap`를 이용하는 경우 순차적으로 data fetch가 두 번 발생한다고 볼 수 있다.
Custom applicative instance를 이용하면 이 경우 한 번만 fetch하면 된다.
runFetchIO 를 실행시켜보면 알 수 있음
Fetch a - #3● Fetching data (Request)
dataFetch :: Request a -> Fetch a
data Fetch a = Done a | forall r . Blocked (Request r) (r -> Fetch a)
Blocked 생성자는 Block을 초래한 Request를 포함하고, Continuation은 Request의 결과(r)에 대한 함수 모양으로 바뀌었다.
하지만 multiple request를 batch로 처리할 때 이를 모델링할 수 없다!결과와 Continuation의 연결을 유지하기 어려움.
r은 결과 값의 타입
Free monad의 liftF와 같음
Fetch a - #4● Mutable reference holding result● Enter IO monad
dataFetch :: Request a -> Fetch a
data BlockedRequest = forall a . BlockedRequest (Request a) (IORef (FetchStatus a))
data Result a = Done a | Blocked (Seq BlockedRequest) (Fetch a)
newtype Fetch a = Fetch { unFetch :: IO (Result a) } Fetch는 IO를 wrapping{new/read/write}IORef를 위해 IO가 필요하다.Continuation으로 직접 넘겨주는 대신 Continuation이 readIORef로 읽어간다.
data FetchStatus a = NotFetched | FetchSuccess a
Quiz. Applicative/Monad 구현하기
dataFetch :: Request a → Fetch adataFetch request = Fetch $ do box ← newIORef NotFetched let br = BlockedRequest request box let cont = Fetch $ do FetchSuccess a ← readIORef box return (Done a) return (Blocked (singleton br) cont)
IO에서 ● fetch결과를 담을 변수 IORef 를 만들고● 요청과 변수를 binding ● Continuation ‘Fetch’에서는 변수에서 결과를 읽어간다.
● 그럼 writeIORef는 어디서???
fetch :: [BlockedRequest] → IO ()
application-specific data-fetchingconcurrency를 직접 사용하고batch로 이득을 볼 수 있음
fetch가 끝나면 box에는 FetchSuccess가 담겨있어야 한다.
runFetch (Fetch h) = do r ← h case r of Done a → return a Blocked br cont → do fetch (toList br) runFetch cont
runFetch :: Fetch a → IO a
Fetch로 wrapping된 IO를 실행그 결과가 Done이면 끝Blocked이면 `fetch`로 데이터를 가져온다음continuation으로 재귀
list traverse같음대신, 한 단계 마다 `fetch`로 데이터를 가져와서 다음 단계로 넘겨준다. (side effect)
Fetch a - #5● Adding a cache, first trial
newtype DataCache = DataCache (forall a. HashMap (Request a) a)
lookup :: Request a → DataCache → Maybe alookup key (DataCache m) = Map.lookup key m
insert :: Request a → a → DataCache → DataCacheinsert key val (DataCache m) = DataCache $ unsafeCoerce (Map.insert key val m)
The use of unsafe features to implement a purely functional API is common practice in Haskell
Request a 에 대해 결과 a 를 저장하는 cache를 만들 수 있다.그런데 결과만 저장한다면 같은 round에서 발생하는 중복 요청에 대응할 수 없다!
Request a는 Eq/Hashable이어야 함
● Adding a cache, second trial
newtype DataCache = DataCache (forall a. HashMap (Request a) (IORef (FetchStatus a)))
lookup :: Request a → DataCache → Maybe (IORef (FetchStatus a))
insert :: Request a → IORef (FetchStatus a) → DataCache → DataCache
newtype Fetch a = Fetch { unFetch :: IORef DataCache → IO (Result a) }
Fetch의 IO는 Cache를 전달받는다 .State처럼 Cache를 인자로 받고 수정된 Cache를 반환하는 대신,이번에도 IORef(변수)에 Cache를 저장해두고 , 업데이트한다 !State 모나드로 바꿀 수 있을까?
IORef에 FetchStatus를 저장
dataFetch :: Request a → Fetch adataFetch req = Fetch $ \ref -> do cache <- readIORef ref case lookup req cache of Nothing -> do … 기존처럼 box 만들고 cache update ... Just box -> do r <- readIORef box case r of FetchSuccess result -> return (Done result) NotFetched -> return (Blocked Seq.empty …)
Cache에 FetchStatus가 있나?
No → FetchStatus 추가
Yes → FetchSuccess 인가?
Yes → Done
No → Blocked empty
\ref -> Cache의 유효기간은?
추가 확장● Exception/Failure
○ data Result a = Done a | Blocked … | Throw SomeException○ throw :: Exception e => e -> Fetch a○ catch :: Exception e => Fetch a -> (e -> Fetch a) -> Fetch a○ data FetchStatus a = NotFetched | FetchSuccess a | FetchFailure SomeException
● Flexibility (Generalize request type)○ dataFetch :: (DataSource req, Request req a) => req a -> Fetch a
○ class DataSource req where fetch :: [BlockedRequest req] -> PerformFetch
○ data PerformFetch = SyncFetch (IO ()) | AsyncFetch (IO() -> IO())○ scheduleFetches :: [PerformFetch] → IO ()
type Request req a = ( Eq (req a) , Hashable (req a) , Typeable (req a) , Show (req a) , Show a )
MyRequest a라는 타입은 DataSource class와 연관되어야 하며, fetch는 이제 DataSource class의 메쏘드가 되었다.scheduleFetches는 각 DataSource별 fetch action(sync/async)을 scheduling
scheduleFetches - 엄청난 한 줄 data PerformFetch = SyncFetch (IO()) | AsyncFetch (IO() → IO())
scheduleFetches :: [PerformFetch] → IO()scheduleFetches fetches = asyncs syncs where asyncs = foldr (.) id [f | AsyncFetch f ← fetches] syncs = sequence_ [io | SyncFetch io ← fetches]
fetch메쏘드는 `async` 패키지 등을 이용하여 구현할 수 있다.
do a1 <- async (getURL url1) a2 <- async (getURL url2) page1 <- wait a1
이 때 wait을 하기 전 뭔가 다른 일을 할 수 있다. 이를 AsyncFetch(IO() → IO())로 모델링 한것.
Haxl
Fun with Haxl by Simon Marlow
*HaxlBlog> run $ (,) <$> mapM getPostContent [1..3] <*> mapM getPostContent [4..6]
select postid,content from postcontent where postid in (6,5,4,3,2,1)
(["example content 1","example content 2","example content 3"],["example content 4","example content
5","example content 6"])
Haxl/Sqlite 이용하여 간단한 예제를 보여준다.mapM, <*>로 결합된 계산이 하나의 query로 변환되어 실행된다.
Fun with Haxl by Simon Marlowtype PostId = Inttype PostContent = String
data BlogRequest a where FetchPosts :: BlogRequest [PostId] FetchPostContent :: PostId -> BlogRequest PostContent
getPostIds :: GenHaxl u [PostId]getPostIds = dataFetch FetchPosts
getPostContent :: PostId -> GenHaxl u PostContentgetPostContent = dataFetch . FetchPostContent
instance DataSource u BlogRequest where fetch (BlogDataState db) _flags _userEnv blockedFetches = SyncFetch $ batchFetch db blockedFetches
instance StateKey BlogRequest where data State BlogRequest = BlogRequestState SQLiteHandle
newtype GenHaxl u a -- Functor/Applicative/Monad
dataFetch :: (DataSource u r, Request r a) => r a -> GenHaxl u a
class (DataSourceName req, StateKey req, Show1 req) => DataSource u req where fetch :: State req -> Flags -> u -> [BlockedFetch req] -> PerformFetch
data BlockedFetch r = forall r. BlockedFetch (r a) (ResultVar a)
putSuccess :: ResultVar a -> a -> IO ()putFailure :: (Exception e) => ResultVar a -> e -> IO ()
data PerformFetch = SyncFetch (IO()) | AsyncFetch (IO() -> IO())
class Typeable f => StateKey (f :: * -> *) where data State f
runHaxl :: Env u -> GenHaxl u a -> IO a
Conclusion
● Monad로 추상화하기○ Fetch로 일단 타입 만들고, 여기에 갖가지 기능 덧붙임
● IO 감추기○ IO/IORef를 사용하되 Fetch타입 바깥으로 드러나지 않도록○ Clean interface
● 타입 맞춰주기○ unsafeCoerce :: forall a b. a -> b
● Typeable○ 동적 타입?
● Free Monad 유행○ data Free f a = Pure a | Free (f (Free f a))
● 언어확장/런타임확장○ ApplicativeDo○ GHC’s runtime에 Unloading기능 추가
● Break the rule○ Applicative는 Monad의 부모클래스