2016년 7월 21일 목요일

type List[A] = Free[({type λ[+α] = (A, α)})#λ, Unit]


Stackless Scala With Free Monads
Rúnar Óli Bjarnason <-- to type 'ú' : Option e + u
2014


문제: State 모나드가 있다.

def zipIndex[A](as: List[A]): List[(Int, A)] = ...

List[A]를 Index와 pair로 만들어주는 zipIndex를 구현하는데...
여기 굳이 State 모나드를 사용할 필요는 없다.

하지만 여기선.. 문제 상황을 만들기 위해 굳이 State 모나드를 사용할 수 있다.

case class State[S,+A](runS: S => (A, S)) {
  def map
  def flatMap
}

def get[S]: State[S,S] = State(s => (s,s))
def put[S](s:S): State[S,Unit] = State(_ => ((),s))
def pure[S, A](a:A): State[S,A] = State(s => (a,s))

index를 상태로 본다면, 0에서 시작, 1씩 증가시키면서 설정하면 된다. State[Int, List[Int,A]]인 셈.

def zipIndex[A](as: List[A]): List[(Int, A)] =
 as.foldLeft(pure[Int, List[(Int,A)]](List()))((acc,a) => for {
  xs <- acc
  i <- get
  _ <- put(i+1)
} yield (i, a) :: xs).runS(0)._1.reverse

foldLeft는 tailrec이지만, 여기서는 그 결과가 State[Int, List[(Int,A)]]의 연산 과정. runS(0)에서 비로소 전체 List를 순회하면서 0,1,2,...상태가 바뀌고 List를 만들어낸다. 이 과정은 State.flatMap의 체인으로 진행.

그런데.. flatMap 구현을 보면..

case class State[S,+A](runS: S => (A, S)) {
  def flatMap[B](f: A => State[S,B]): State[S,B] =
    State[S,B](s => { val (a,s') = runS(s); f(a).runS(s') })
}

runS를 부르면 flatMap을 부르고, flatMap은 runS를 부르고, 다시 flatMap을 부르고... 그래서 List가 크면 금방 StackOverflowError가 뜬다.

이 문제는.. 사실 Scala에서 좀더 엄격하게 함수형 프로그래밍을 하고자 할 때 발생한다. 함수형 프로그래밍을 널리 전파하려는 Rúnar 입장에서는 이 문제를 해결해줘야 사람들 불만이 없을 거다.

해결책:

1. TailRec보다 좀더 일반화된 Trampoline(monad)
2. Trampoline에서 StackOverflowError를 회피하고 -- flatMap을 타입으로 표현한 것이 주요 방법
3. 이를 일반화시켜 Free Monad를 완성

그래서 Haskell Free Monad와 다른 모양이 나오게 되었다.

data Free f a = Pure a | Free f (Free f a)

sealed trait Free[S[+_], +A] {
 private case class FlatMap[S[+_],A,+B](a: Free[S,A], f: A => Free[S,B]) extends Free[S,B]
}

case class Done[S[+_],+A](a: A) extends Free[S,A]
case class More[S[+_],+A](k: S[Free[S,A]]) extends Free[S,A]

Done/More는 각각 Pure/Free에 매핑되는데, FlatMap은 스택 문제로 추가된 것이다.









2016년 6월 27일 월요일

Asymptotic Improvement of Computations over Free Monad - Janis Voigtländer

Leaf에 label을 붙인 binary tree는 ..

data F a = B a a deriving (Show, Functor)
type T a = Free F a

(이를 위해서는 DeriveFunctor 확장을 켜야함)

leaf a = return a
node a b = warp (B a b)

(여기서 wrap은 Free 생성자를 한번더 추상화한 것. MonadFree 에 선언)

fullTree :: (MonadFree F m) => Int -> m String
fullTree 0 = leaf ""
fullTree n = do
  s <- fullTree (n-1)
  node (leaf (s ++ "1")) (leaf (s ++ "0"))

> (fullTree 3) :: T String
Free (N (Free (N (Free (N (Pure "111") (Pure "110"))) (Free (N (Pure "101") (Pure "100"))))) (Free (N (Free (N (Pure "011") (Pure "010"))) (Free (N (Pure "001") (Pure "000"))))))





root부터 지그재그(왼쪽오른쪽왼쪽..)로 내려가서 맨끝 leaf찾는 함수 zigzag

zigzag :: T a -> a
zigzag = zig
  where zig (Pure a) = a
        zig (Free (N a b)) = zag a
        zag (Pure a) = a
        zag (Free (N a b)) = zig b


사실 fullTree n의 n에 linear하게 동작해야겠지만 zigzag (fullTree n)은 quadratic 복잡도로 동작한다. 이유는 fullTree로 생성되는 트리 구조가 leaf노드를 확장해나가는 방식이 painter 알고리즘과 같기 때문이다.

그림에서처럼 leaf노드가 확장될 때마다 루트까지의 부모 노드를 모두 새로 생성한다.

반면 zigzag (improve (fullTree n)) 은 linear하게 동작한다.

improve :: Functor f => (forall m. MonadFree f m => m a) -> Free f a

이때 fullTree가 선택하는 m은 Free F가 아니라 Codensity (Free F) 이다. 

이에 따라 zigzag (fullTree N)과 zigzag (improve (fullTree N))의 실행속도를 비교해보면 다음과 같다.



improve m = lowerCodensity m

[a]는 a가 무엇이든 Monoid가 됩니다.
Free Monad(Free f a)는 Functor f가 무엇이든 Monad가 됩니다. 말하자면 Free f a 는 f a의 리스트입니다. ([]에 해당하는 것이 return a)입니다.

(++) :: [a] → [a] → [a] 는 Monoid append입니다. 즉 두 개의 리스트를 이어붙입니다.
m >>= k :: Free f a → (a → Free f b) → Free f b 는 f a 리스트를 이어붙이는 것과 같습니다(append). 앞부분이 f f (return a) 로 끝나는 리스트이면, k a로 얻어지는 f f f (return b)응 이어붙여서 f f f f f (return b)가 됩니다.

a ++ b ++ c ++ d ... 는 O(n²) 의 시간복잡도를 가집니다. 왼쪽 우선 결합때문입니다.
m >>= f >>= g ... 역시 마찬가지입니다.

newtype DList a = DL { unDL:: [a] → [a] } 는 (a++) 형식으로 리스트를 표현하여 append를 상수 시간에 처리하고(함수합성), 누적 append의 결과를 최종 O(n)으로 구할 수 있습니다. (DList는 앞서 John Hughes의 difference list)

fromList = DL . (++) -- fromList as = DL (as ++)
append xs ys = DL (unDL xs . unDL ys)
toList = ($[]) . unDL

newtype Codensity m a = Codensity { runCodensity :: forall b. (a → m b) → m b} 는 (m >>=) 형식으로 Free monad를 표현하여 append(실제는 >>=)를 뒤쪽(오른쪽) 우선으로 합성하여 최종 결과를 O(n)으로 얻을 수 있습니다.
lift m = Codensity (m >>=)
m >>= k = Codensity (\c → runCodensity m (\a → runCodensity (k a) c)) -- k a 뒤에 c를 이어붙이고, 그 앞에 m을 prepend한다고 볼 수 있음
lowerCodensity a = runCodensity a return -- toList처럼 원래의 타입(모나드)로 돌아가려면 그냥 끝내기(return)하면 됨

Codensity의 장점은 Free Monad의 성능을 개선하기 위해 정말 짧은 코드만 필요하다는 것입니다. 컴파일러 지원이 필요하지도 않고 추가 구현도 필요없이.. 아래와 같은 한줄짜리 improve 만 끼워넣으면 된다는!!!

improve :: Functor f => (forall m. MonadFree f m => m a) → Free f a
improve m = lowerCodensity m

여기서 m은 Free f a 인데, 정확히는 Free f a 처럼 동작하는 Codensity (Free f) a입니다. 이것을 다시 lowerCodensity로 Free f a로 변환하는 것입니다.  Free f a는 왼쪽우선 >>=로 연결된 리스트지만 Codensity (Free f) a는 모두 오른쪽 우선 결합된 >>= 리스트가 되어서 결과적으로 >>=리스트를 순회하는(interpreting) 복잡도가 linear로 개선되는 것이죠.

(이를 위해서는! improve의 첫번째 타입처럼 MonadFree f m => m a, 즉 loose하게 생성되어야 합니다. 명시적으로 Free/Pure를 사용하는 대신 wrap/return으로 생성되어야 한다는 얘기입니다. 그래야 improve를 도입함으로써 wrap/return이 Free대신 Codensity로 적용되면서 improve가 동작할 수 있게 됩니다)

즉 run fa 라고 할 곳에 run (improve fa) 라고 하면 자동으로 속도가 빨라진다는 얘기!!! 놀랍죠. improve는 사실상 identity이지만 side-effect로 속도가 빨라지는 구조로 변환하는 역할을 합니다.

참고 http://www.janis-voigtlaender.eu/papers/AsymptoticImprovementOfComputationsOverFreeMonads.pdf

2016년 6월 26일 일요일

newtype DList a = DL { unDL :: [a] -> [a] }

newtype DList a = DL { unDL :: [a] -> [a] }

https://hackage.haskell.org/package/dlist-0.7.1.2/docs/Data-DList.html
Difference list는 List append를 O(1)에 할 수 있는 리스트 표현법이다.

append       :: DList a -> DList a -> DList a
append xs ys = DL (unDL xs . unDL ys)



DList는 prefix로 append할 내용을 함수 형태로 저장하기 때문에 두 DList를 합친다는 것은 함수 합성에 불과하다. 함수 합성은 당연히 상수시간.



write/append only. 


--

하스켈의 Difference list 

하스켈의 기본 리스트는 single linked list라서 a ++ b는 a의 길이만큼 시간이 걸리는데, 이 자체는 크게 문제가 안됩니다만 (a ++ b) ++ c 와 같이 왼쪽 우선 합성이기 때문에 여러개를 이어붙여야 하는 경우 처음에 a 만큼, 두번째는 a + b 만큼.. 이런식으로 prefix를 계속 반복하는 문제가 생깁니다. 일부러 a ++ (b ++ c)라고 우선순위를 조절할 수 있다면 모를까.. ㅠ.ㅠ 

Difference list는 (a++) . (b++) . (c++) 식으로 append가 되기 때문에 append 자체는 O(1)(함수합성)로 끝나고, append가 모두 끝난다음 한번에 []를 인자로 넘겨서 a ++ b ++ c(원하는 리스트)를 만들어냅니다. 이 과정은 맨 뒤 함수부터 적용되므로 앞서와 같은 a가 불필요가하게 반복되는 문제가 없습니다. 

이런 방법은 문자열에 특화된 type ShowS = String -> String 에서도 이미 적용되어 있습니다. 


2016년 6월 21일 화요일

scheduleFetches fetches = asyncs syncs (HAXL)

scheduleFetches :: [PerformFetch] → IO ()
scheduleFetches fetches = asyncs syncs
where
asyncs = foldr (.) id [f | AsyncFetch f ← fetches]
syncs = sequence_ [io | SyncFetch io ← fetches]

Haxl을 소개하는 There is no fork 페이퍼에 나오는 코드.
이 코드에 감탄한 사람들이 있다.

data PerformFetch = SyncFetch (IO ()) | AsyncFetch (IO() -> IO())

이 타입을 이해하기도 조금 헷갈린다. IO() -> IO() 부분이 그런데..이는 fetch::[BlockedRequest] -> PerformFetch 함수의 일반적인 구현을 봐야 이해가 된다.

myFetch reqs =
AsyncFetch $ \inner -> do
asyncs <- mapM fetchAsync reqs
inner
mapM_ wait async

async/await 패턴을 사용할 때 그 사이를 채우기 위한 인자를 받을 수 있다.
사실상 SyncFetch는 거의 없을 것 같긴 하다.

하나씩 따져보자.
fetches = [Async (\inner -> async f1 >> inner >> await f1), Sync f2, Sync f3, Async (\inner -> async f4 >> inner >> await f4)] 로 주어지면, (async/await은 우선 개념적으로만)

scheduleFetches는 async f1 >> async f4 >> f2 >> f3 >> await f4 >> await f1 처럼 동작하는 IO()를 만들어준다! (그러니 scheduleFetches라는 이름이 나오는 것)

asyncs = foldr (.) id [f | AsyncFetch f <- fetches]
= foldr (.) id [\inner -> async f1 >> inner >> await f1, \inner -> async f4 >> inner >> await f4]
= (\inner -> async f1 >> inner >> await f1) . (\inner -> async f4 >> inner >> await f4)
= \inner -> (\inner -> async f1 >> inner >> await f1) ((\inner -> async f4 >> inner >> await f4) inner)
= \inner -> (\inner -> async f1 >> inner >> await f1) (async f4 >> inner >> await f4)
= \inner -> async f1 >> (async f4 >> inner >> await f4) >> await f1

syncs = sequence_ [io | SyncFetch io <- fetches]
= sequence_ [f2, f3]
= f2 >> f3

asyncs syncs = (\inner -> async f1 >> (async f4 >> inner >> await f4) >> await f1) (f2 >> f3)
= async f1 >> (async f4 >> (f2 >> f3) >> await f4) >> await f1

AsyncFetch 를 위해서는 async 패키지 같은 걸 쓴다고 봐야한다. (이 패키지 만든 사람이 Simon Marlow다)

g(f input)


g (f input)

-- Why Functional Programming Matters by John Hughes

 부연설명이 필요하다.

f는 input을 output으로 .. output은 다시 g의 input이 된다. Haskell은 f와 g를 함께 실행할 수 있다. 즉, f와 g의 실행이 overlap되는 것. 그럼에도 input/output으로 엄격히 동기화된다. 

g가 입력을 읽을 때 비로소 f가 시작되고, f는 출력을 만들고 나면 suspend된다. g가 또 입력을 읽으려 하면 f는 resume하여 또 출력을 만들 수 있다. 

g와 f는 코루틴으로 동작하는 셈이다!


2016년 4월 21일 목요일

non-deterministic calculation

며칠전 '별다방손코딩'을 다른 이들에게 소개하며 같이 풀어본 문제다. 쉬워보여서.. https://www.hackerrank.com/challenges/manasa-and-stones

0에서 시작하여 (a 혹은 b)를 (n-1)번 더했을 때 나올 수 있는 값을 모두 찾으라는 것인데, 문제 자체는 매우 간단해서 a만 (n-1)번 더한 것과 b만 (n-1)번 더한 것을 n개의 등차수열로 만들면 된다.

하지만 문제가 말하는 바 그대로를 살펴보면 non-deterministic calculation이다. 즉 0에 x를 n번 더한 결과는? 당연히 x*n이지만.. x가 (a 혹은 b)라면 그 결과가 어느 한 값이 아니라 위처럼 여러 가능한 값을 가진다. 심지어 n이 (c 혹은 d)라면? x가 세 값 중 어느 값이라면? .. 더하기 뿐 아니라 연산이 더 복잡하다면?

Haskell에서는 non-deterministic value를 List로 표현하였을 때 (+) 연산을 lifting해주는 것만으로 쉽게 답을 구할 수 있다.

iterate (+ x) 0 == [0, x, 2*x, 3*x, ...]  -- 0에 '더하기 x'를 반복 적용하는 것이다.
[a,b,c] !! 2 == c    -- (!!)는 index 연산자다.
iterate (+ x) 0 !! (n-1)  --  0에 x를 n-1번 더한 값을 얻을 수 있다.

(+)는 Num -> Num -> Num 이지만, x 가 [1,2] 가 된다면 바로 적용할 수 없으므로 lift해줘야 한다.

iterate (liftA2 (+) x) [0] !! (n-1)

여기엔 중복된 값이 있으므로 마지막에 nub(유일 값만 추출하기)을 적용하면 된다.

lastStone n a b = nub $ iterate(liftA2 (+) [a,b]) [0] !! (n-1)

2016년 2월 5일 금요일

(.:) = (.) (.) (.) -- concatMap

Haskell의 함수중 (.)만큼 많이 쓰이는 것이 있을까? compose 연산자다.

f . g = f (g x) 

그런데 (.)의 두번째 인자는 입력을 하나만 처리한다. concat xss 와 map f xs 의 concat 과 map 을 합성하려면?

concat . map -- 성립하지 않는다.

map 을 인자 하나면 처리하면 여전히 함수이며, 이는 concat의 입력타입([[x]])과 일치하지 않기 때문이다. 그래서...

concatMap f = concat . map f

map의 첫 인자를 적용한 상태로 합성하면 된다.  하지만 f를 제거하여 합성하고 싶다면???

concatMap = concat .: map

여기서 (.:)는 외우기 참 쉬운데, 정의는 모양 그대로 점셋(.:)을 펼쳐놓은 것 (.)(.)(.) 이며, 타입 역시 모양 그대로 왼쪽점 하나(왼쪽항이 인자 하나), 오른쪽 점 둘(오른쪽 항이 인자 둘을 처리)의 합성이다.

(.:) :: (c->d) -> (a->b->c) -> a->b->d
(.:) = (.)(.)(.)


2016년 2월 4일 목요일

length(intersect(friendsOf x)(friendsOf y))

Facebook의 HAXL소개에 늘 나오는 대표적 예제

iterate(group>=>(sequence[length,head]))[1]

국내엔 개미수열로 알려진 'look and say' 수열

#define BITS_TO_BYTES(bits) ((bits)/8 + !!((bits)%8))

C에서 비트필드배열 만들 때 쓸만할 것 같음.
#define BITS_A 5
#define BITS_B 3
#define BITS_C 2
char bitfield[BITS_TO_BYTES(BITS_A + BITS_B + BITS_C)];