하스켈학교 세미나 - haxl

36
Haxl 한주영

Upload: jooyung-han

Post on 16-Mar-2018

259 views

Category:

Software


2 download

TRANSCRIPT

Page 1: 하스켈학교 세미나 - Haxl

Haxl한주영

Page 2: 하스켈학교 세미나 - Haxl

Haxl - 2014년 Open● Facebook 오픈소스 발표● ICFP2014 - Siman Marlow

○ 페이퍼(pdf) ○ 동영상

하스켈은 그냥 공부만을 위한 언어였는데 , 이건 뭔가 프랙티컬 할 것 같은 느낌

Page 3: 하스켈학교 세미나 - Haxl

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)

특정 언어의 라이브러리가 여기 저기 포팅된다는 건 라이브러리 이상의 의미가 있다는 뜻

Page 4: 하스켈학교 세미나 - Haxl

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

굉장히 일반적인 문제에 대한 해법. 널리 활용가능할 것 같음

Page 5: 하스켈학교 세미나 - Haxl

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

Page 6: 하스켈학교 세미나 - Haxl

Functional Pearls 같은 페이퍼● 친절하다.

○ 결과물만 소개하는 대신 라이브러리 설계 과정을 설명해준다 !○ 문제, 핵심 아이디어, 뼈대 코드, 여기에 기능을 하나씩 더해가며 발전시켜 나감

● github.com/facebook/Haxl 의 축약판○ 페이퍼는 핵심아이디어 위주로 설명○ 필요하다면 haxl을 직접 볼 수 있다. (아쉽지만 Initial commit이 이미 어느정도 완성형)○ 실제 코드는 훨씬 복잡 -- 그만큼 현실적

● 만들어진지 얼마되지 않은 라이브러리○ 군더더기가 적다

● 아무나가 아닌 Simon Marlow○ 하스켈 공부하다 마주치는 몇명의 Guru들 중 한 사람○ 특히 Parallel/Concurrent 쪽

https://github.com/simonmar

Page 7: 하스켈학교 세미나 - Haxl

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

Page 8: 하스켈학교 세미나 - Haxl

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

Page 9: 하스켈학교 세미나 - Haxl

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

Page 10: 하스켈학교 세미나 - Haxl

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이라고 볼 수 있음

Page 11: 하스켈학교 세미나 - Haxl

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은 어려울 듯...

Page 12: 하스켈학교 세미나 - Haxl

Rendering a blog

Page 13: 하스켈학교 세미나 - Haxl

● 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

Page 14: 하스켈학교 세미나 - Haxl

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

Page 15: 하스켈학교 세미나 - Haxl

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

쉽게 쌓아올라갈 수 있다.

Page 16: 하스켈학교 세미나 - Haxl

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

Page 17: 하스켈학교 세미나 - Haxl

Blog example을 진행하면서● 동시성을 신경 쓰지 않아도 되고● Data fetch 순서 신경 쓰지 않아도 되고

○ 다른 언어 환경에서 Future/Promise 쓰는 경우에는 중요한 문제. Modularity를 해친다

● Biz logic에 집중할 수 있었다!

Fetch/Haxl을 구현한 다른 라이브러리는 이런 효과가 조금 떨어진다 . Why?

● Applicative에 implicit하게 녹여낸 것이 특징인데 ,● Scala의 경우 명시적으로 사용해야 함

ex) Stitch.traverse(...), Stitch.join(..) 기존의

Page 18: 하스켈학교 세미나 - Haxl

실제 실행될 때는...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할 필요가 없다.

Page 19: 하스켈학교 세미나 - Haxl

Fetch 만들기

Page 20: 하스켈학교 세미나 - Haxl

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

Page 21: 하스켈학교 세미나 - Haxl

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된 상황을 모아서 한번에 처리할 수 있도록 함.

Page 22: 하스켈학교 세미나 - Haxl

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 를 실행시켜보면 알 수 있음

Page 23: 하스켈학교 세미나 - Haxl

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와 같음

Page 24: 하스켈학교 세미나 - Haxl

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 구현하기

Page 25: 하스켈학교 세미나 - Haxl

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가 담겨있어야 한다.

Page 26: 하스켈학교 세미나 - Haxl

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)

Page 27: 하스켈학교 세미나 - Haxl

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이어야 함

Page 28: 하스켈학교 세미나 - Haxl

● 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를 저장

Page 29: 하스켈학교 세미나 - Haxl

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의 유효기간은?

Page 30: 하스켈학교 세미나 - Haxl

추가 확장● 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

Page 31: 하스켈학교 세미나 - Haxl

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())로 모델링 한것.

Page 32: 하스켈학교 세미나 - Haxl

Haxl

Page 33: 하스켈학교 세미나 - 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로 변환되어 실행된다.

Page 34: 하스켈학교 세미나 - Haxl

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

Page 35: 하스켈학교 세미나 - Haxl

Conclusion

Page 36: 하스켈학교 세미나 - Haxl

● 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의 부모클래스