2. Haxl - 2014년 Open
● Facebook 오픈소스 발표
● ICFP2014 - Siman Marlow
○ 페이퍼(pdf)
○ 동영상
하스켈은 그냥 공부만을 위한 언어였는데, 이건
뭔가 프랙티컬 할 것 같은 느낌
3. 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)
특정 언어의 라이브러리가 여기 저기
포팅된다는 건 라이브러리 이상의 의미가
있다는 뜻
4. 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
굉장히 일반적인 문제에 대한
해법. 널리 활용가능할 것 같음
5. 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
6. Functional Pearls 같은 페이퍼
● 친절하다.
○ 결과물만 소개하는 대신 라이브러리 설계 과정을 설명해준다!
○ 문제, 핵심 아이디어, 뼈대 코드, 여기에 기능을 하나씩 더해가며 발전시켜 나감
● github.com/facebook/Haxl 의 축약판
○ 페이퍼는 핵심아이디어 위주로 설명
○ 필요하다면 haxl을 직접 볼 수 있다. (아쉽지만 Initial commit이 이미 어느정도 완성형)
○ 실제 코드는 훨씬 복잡 -- 그만큼 현실적
● 만들어진지 얼마되지 않은 라이브러리
○ 군더더기가 적다
● 아무나가 아닌 Simon Marlow
○ 하스켈 공부하다 마주치는 몇명의 Guru들 중 한 사람
○ 특히 Parallel/Concurrent 쪽
https://github.com/simonmar
7. 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
8. 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
9. Typical example
do 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
10. Typical example
do 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이라고 볼 수 있음
11. 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은 어려울 듯...
13. ● 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
14. blog :: Fetch Html
blog = renderPage <$> leftPane <*> mainPane
mainPane :: Fetch Html
mainPane = do
posts <- getAllPostsInfo :: Fetch [PostInfo]
let ordered = … 최신 글 5개
contents <- mapM (getPostContent . postId) ordered
return $ renderPosts (zip ordered content)
leftPane:: Fetch Html
leftPane = renderSidePane <$> popularPosts <*> topics
data PostInfo = PostInfo {
postId:: PostId,
postDate:: Date,
postTopic:: String }
Concurrency를 직접 사용하지 않는다
그냥 Monad/Applicative/Traversable 일뿐
Quiz
15. 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
쉽게 쌓아올라갈 수 있다.
16. popularPosts :: Fetch Html
popularPosts = do
pids <- getPostIds
views <- mapM getPostViews pids
let orderd :: [PostId] = … 뷰가 가장 많은 5개 …
contents <- mapM getPostDetails ordered
return (renderPostList contents)
topics :: Fetch Html
topics = do
posts <- getAllPostsInfo
let topicCounts :: Map String Int = … 토픽 별 갯수 …
return (renderTopics topicCounts)
직접 Batch를 신경쓰지 않아도 된다
Quiz
17. Blog example을 진행하면서
● 동시성을 신경 쓰지 않아도 되고
● Data fetch 순서 신경 쓰지 않아도 되고
○ 다른 언어 환경에서 Future/Promise 쓰는 경우에는 중요한 문제. Modularity를 해친다
● Biz logic에 집중할 수 있었다!
Fetch/Haxl을 구현한 다른 라이브러리는 이런 효과가 조금
떨어진다.
Why?
● Applicative에 implicit하게 녹여낸 것이 특징인데,
● Scala의 경우 명시적으로 사용해야 함
ex) Stitch.traverse(...), Stitch.join(..) 기존의
18. 실제 실행될 때는...
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할 필요가 없다.
20. 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 -> a
runFetch f = case f of
Done a -> a
Blocked c -> runFetch c
runFetchIO :: Fetch a -> IO a
runFetchIO f = case f of
Done a -> return a
Blocked c -> putStrLn “fetch” >> runFetchIO c
21. 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 = liftM
pure = … define ...
(<*>) = ap
return = pure
(>>=) = … define ...
따로 Applicative를
구현하여 Block된 상황을
모아서 한번에 처리할 수
있도록 함.
22. Applicative vs Monad
Blocked (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 b
ap m1 m2 = do
x1 <- m1
x2 <- m2
return (x1 x2)
Done f <*> x = f <$> x
Blocked c <*> x = Blocked (c <*> x)
Blocked가 Remote data fetch라면 Monad
`ap`를 이용하는 경우 순차적으로 data fetch가
두 번 발생한다고 볼 수 있다.
Custom applicative instance를 이용하면 이
경우 한 번만 fetch하면 된다.
runFetchIO 를 실행시켜보면 알 수 있음
23. 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와 같음
24. 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 구현하기
25. dataFetch :: Request a → Fetch a
dataFetch 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-fetching
concurrency를 직접 사용하고
batch로 이득을 볼 수 있음
fetch가 끝나면 box에는
FetchSuccess가 담겨있어야 한다.
26. 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)
27. Fetch a - #5
● Adding a cache, first trial
newtype DataCache = DataCache (forall a. HashMap (Request a) a)
lookup :: Request a → DataCache → Maybe a
lookup key (DataCache m) = Map.lookup key m
insert :: Request a → a → DataCache → DataCache
insert 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이어야 함
28. ● 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를 저장
29. dataFetch :: Request a → Fetch a
dataFetch 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의 유효기간은?
30. 추가 확장
● 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
31. 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())로 모델링 한것.
33. 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로 변환되어 실행된다.
34. Fun with Haxl by Simon Marlow
type PostId = Int
type PostContent = String
data BlogRequest a where
FetchPosts :: BlogRequest [PostId]
FetchPostContent :: PostId -> BlogRequest PostContent
getPostIds :: GenHaxl u [PostId]
getPostIds = dataFetch FetchPosts
getPostContent :: PostId -> GenHaxl u PostContent
getPostContent = 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
36. ● 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의 부모클래스