3. 하스켈이란 무엇인가
• General-purpose purely functional programming language
• Non-strict semantics (lazy evaluation)
• Strong static type system based on Hindley-Milner type inference
4. 어느 언어나 그럴 듯한 홍보 문구를 가지고 있다
• 순수 함수만 있어서 코드를 이해하기 쉽고
• 각 함수의 정확성을 분석, 검사하기 쉽고
• 고급 타입 시스템이 실수를 줄여준다
타입
pythagoras_cps :: Int -> Int -> ((Int -> r) -> r)
코드
pythagoras_cps x y = k ->
square_cps x $ x_squared ->
square_cps y $ y_squared ->
add_cps x_squared y_squared $ k
분석
pythagoras_cps 3 4 print
= square_cps x $ x2 -> square_cps y $ y2 -> add_cps x2 y2 print
= square_cps x (x2 -> square_cps y (y2 -> add_cps x2 y2 print))
= square_cps x (x2 -> square_cps y (y2 -> print (add x2 y2))
= (x2 -> square_cps y (y2 -> print (add x2 y2)) (square x)
= ((x2 -> (y2 -> print (add x2 y2)) (square y)) (square x)
= (y2 -> print (add (square x) y2)) (square y)
= print (add (square x) (square y))
우 와! 정말 쉽다~ 대단해!
5. 수학 기호와 닮았다
𝑓: 𝑎, 𝑏 → 𝑅3
𝑓 𝑎, 𝑏 = (𝑎 𝑐𝑜𝑠𝑏, 𝑎 𝑠𝑖𝑛𝑏, 𝑎)
𝑔 𝑥 =
𝑠𝑖𝑛𝑥
𝑥
, 𝑥 < 0
1, 𝑥 = 0
𝑒 𝑥
+ 1, 𝑥 > 0
type Curve = (Double, Doule, Double)
f :: (Double, Double) -> Curve
f a b = (a * cos b, a * sin b, a)
g x
| x < 0 = sin x / x
| x == 0 = 1
| x > 0 = exp x + 1
6. 수학 기호와 닮았다
𝑆 = 𝑓(𝑥) 1 ≤ 𝑥 ≤ 10, 𝑥 𝑖𝑠 𝑖𝑛𝑡𝑒𝑔𝑒𝑟 }
where 𝑓 𝑥 = 𝑥3
− 𝑥
𝑥 𝑟, 𝜃 = 𝑟 𝑐𝑜𝑠𝜃, 𝑟 𝑠𝑖𝑛𝜃, 𝑟
Let 𝑟 𝑡 = 𝑒 𝑡/2, 𝜃 𝑡 =
𝑡
2
𝛼 𝑡 = 𝑥 𝑟 𝑡 , 𝜃 𝑡
s = [ f x | x <- [1..10] ]
where f x = x*x*x - x
x r theta = (r * cos theta, r * sin theta, r)
a t = let r = exp (t/2)
theta = t / (sqrt 2)
in x r theta
7. 수학 = = 하스켈
A real vector space is a set V, whose elements are called vectors, together with two binary operations + ∶
𝑉 × 𝑉 → 𝑉 and • ∶ 𝑅 × 𝑉 → 𝑉 , called addition and scalar multiplication, which satisfy the following eight
axioms for all 𝒖, 𝒗, 𝒘 ∈ 𝑉 and 𝑟, 𝑠 ∈ 𝑹
• 𝒖 + 𝒗 = 𝒗 + 𝒖
• 𝒖 + 𝒗 + 𝒘 = 𝒖 + 𝒗 + 𝒘
• there is an element 𝟎 in 𝑉 s.t. 𝟎 + 𝒖 = 𝒖
• 𝑟𝑠 ⋅ 𝒖 = 𝑟 ⋅ 𝑠 ⋅ 𝒖
• 𝑟 + 𝑠 ⋅ 𝒖 = 𝑟 ⋅ 𝒖 + 𝑠 ⋅ 𝒖
• r ⋅ 𝒖 + 𝒗 = 𝑟 ⋅ 𝒖 + 𝑟 ⋅ 𝒗
• 0 ⋅ 𝒖 = 𝟎
• 1 ⋅ 𝒖 = 𝟏
8. 수학 = = 하스켈
class Monoid a where
mempty :: a
mappend :: a -> a -> a
mconcat :: [a] -> a
mconcat = foldr mappend mempty
class (Functor t, Foldable t) => Traversable t where
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
sequenceA :: Applicative f => t (f a) -> f (t a)
mapM :: Monad m => (a -> m b) -> t a -> m (t b)
sequence :: Monad m => t (m a) -> m (t a)
chainCPS :: ((a -> r) -> r) -> (a -> ((b -> r) -> r)) -> ((b -> r) -> r)
chainCPS s f = k -> s $ x -> f x $ k
foldMap (g . f) = g . foldMap f
foldMap f = fold . fmap f
foldMap g . fmap f = foldMap (g . f) = g . foldMap f
instance Arrow Circuit where
arr f = Circuit $ a -> (arr f, f a)
first (Circuit cir) = Circuit $ (b, d) ->
let (cir', c) = cir b
in (first cir', (c, d))
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
f >=> g = x -> f x >>= g
9. 하스켈에서 가능한 것들
• 소수 구하기
primes = sieve [2..] where sieve (p:xs) = p : sieve [ n | n <- xs, n `mod` p > 0 ]
• 문자열을 공백으로 분리 “Hello world” => [“Hello”, “world”]
unfoldr (b -> fmap (const . (second $ drop 1) . break (==' ') $ b) . listToMaybe $ b)0 ]
• N-queen 알고리즘
queens n = foldM f [] [1..n] where
f qs _ = [q:qs | q <- [1..n] qs, q `notDiag` qs]
q `notDiag` qs = and [abs (q - qi) /= i | (qi,i) <- qs `zip` [1..]]
• 입력을 알파벳 순으로 정렬
main = (mapM_ putStrLn . Data.List.sort . lines) =<< getContents
10. 사건의 전말
온라인 저지 사이트 ALGOSPOT
문제 ID: WEIRD (https://www.algospot.com/judge/problem/read/WEIRD)
In mathematics, weird numbers are natural numbers that are abundant but not semiperfect. In other words, a
natural number N is a weird number if and only if:
• Sum of its proper divisors (i.e. less than N ) is greater than the number.
• No subset of its divisors sum to N.
For example, the set of proper divisors of 12 is { 1, 2, 3, 4, 6 } . The sum of these numbers exceed 12, however,
12 is not a weird number since 1 + 2 + 3 + 6 = 12.
However, 70 is a weird number since its proper divisors are {1, 2, 5, 7, 10, 14, 35} and no subset sums to 70 .
Write a program to determine if the given numbers are weird or not.
11. 문제 분석
S(N) = N의 약수들의 집합 (N은 제외)
• S의 원소들의 합이 N보다 크다 (abundant)
• S의 부분집합 중 그 합이 N이 되는 집합이 있다 (semiperfect)
• N이 abundant하면서 semiperfect하지 않으면 weired number
• 예시
• S(12) = {1, 2, 3, 4, 6}
• sum(S) = 16 > 12 -> 12는 abundant
• sum({1,2,3,6}) = 12 = N -> 12는 semiperfect
• 따라서 12는 weired number가 아니다
12. 간단한 0-1 KNAPSACK 문제
• S(N)을 알면 abundant 검사는 단순하다
• semiperfect 검사는 0-1 knapsack 문제
• 약수들은 정수이므로 다이나믹 프로그래밍으로 해결 가능
• 𝑆 𝑁 = 𝑠1, 𝑠2, … , 𝑠 𝑘 (𝑠1 ≤ 𝑠2 ≤ … )
• 𝑇 𝑖, 𝑗 = 𝑆 𝑁 의 𝑠1, 𝑠2, … , 𝑠𝑖 까지 써서 만들 수 있는, j 이하의 최대 합
• 𝑇 𝑖, 𝑗 =
0 𝑖 = 0
𝑇 𝑖 − 1, 𝑗 𝑠𝑖 > 𝑗
max 𝑇 𝑖 − 1, 𝑗 , 𝑇 𝑖 − 1, 𝑗 − 𝑠𝑖 + 𝑠𝑖 𝑜𝑡ℎ𝑒𝑟𝑤𝑖𝑠𝑒
• 𝑇 𝑘, 𝑁 = 𝑁 이면 N은 semiperfect
19. 이렇게 계산하는 이유?
• 점화식을 그대로 계산하면 같은 원소를 중복 계산
• 계산 시간이 지수함수적으로 증가 (가장 단순한 예: 피보나치 수의 재귀적 계산)
• 큰 문제가 작은 문제를 포함
• 작은 문제를 풀어두면 큰 문제를 풀 수 있다
• 테이블을 계산해두면 비슷한 문제들을 풀 때 재활용 가능
• 메모리를 희생하고 속도를 얻는다
20. 하스켈식 해법
1 divisors n = [ x | x <- [1..(n-1)], n `mod` x == 0]
2
3 knapsack n divs = table where
4 table :: Int -> Int -> Int
5 table 0 j = 0
6 table i j
7 | si > j = table (i-1) j
8 | otherwise = maximum [ table (i-1) j, table (i-1) (j-si) + si ]
9 where si = divs !! i
10
11 isWeird n = abundant && not semiperfect where
12 divs = divisors n
13 abundant = sum divs > n
14 semiperfect = (knapsack n divs) (length divs - 1) n == n
15
16 main = do
17 putStrLn $ "is 12 weird?: " ++ show (isWeird 12)
18 putStrLn $ "12 70 weird?: " ++ show (isWeird 70)
21. 하스켈식 해법
1 divisors n = [ x | x <- [1..(n-1)], n `mod` x == 0]
2
3 knapsack n divs = table where
4 table :: Int -> Int -> Int
5 table 0 j = 0
6 table i j
7 | si > j = table (i-1) j
8 | otherwise = maximum [ table (i-1) j, table (i-1) (j-si) + si ]
9 where si = divs !! i
10
11 isWeird n = abundant && not semiperfect where
12 divs = divisors n
13 abundant = sum divs > n
14 semiperfect = (knapsack n divs) (length divs - 1) n == n
15
16 main = do
17 putStrLn $ "is 12 weird?: " ++ show (isWeird 12)
18 putStrLn $ "12 70 weird?: " ++ show (isWeird 70)
점화식 중복 계산
22. 진짜 하스켈식 해법
• 테이블 자체를 재귀적으로 정의하면 안된다
• 테이블의 원소를 재귀적으로 정의하면 된다
• ???
knapsack n divs len = table where
table = array ((0,0),(len,n))
$! [((0,w),0) | w <- [0..n]]
++ [((i,w),v) | i<-[1..len], w<-[0..n],
let v = maximum [at(i-1,w), divs!i + at(i-1,w-(divs!i))]]
at (i, w) = if w < 0 then -n else table ! (i,w)
23. 메모이제이션(MEMOIZATION)
• 테이블 계산하는 게 메모이제이션
• 하스켈에서는 테이블의 원소를 재귀적으로 정의해도 메모이제이션이 가능하다
• 지연 평가(Lazy Evaluation)
• 데이터 불변성(Data Immutability)
• 고정점 연산자(Fixed Point Operator)
29. 해결 방법
• 철저한 평가(Strict Evaluation)
• 표현식이 너무 길어지기 전에 계산을 강제로 수행
• 원시 타입(Unboxed type)
• 정수 표현에 C와 같은 바이트 사용
• 테이블의 두 행만 메모리에 유지
• 필요없는 행은 바로 가비지 컬렉션이 되도록
30. 해결 방법
knapsack :: Int -> UArray Int Int -> Int -> UArray Int Int
knapsack n divs len = a 0 (runSTUArray $ newArray (0,n) (0::Int)) where
a prevRow prevA
| prevRow == len = prevA
| True = currA `seq` a (prevRow+1) currA where
currA = runSTUArray $ newListArray (0,n) [w `seq` (v `seq` v) | w <- [0..n],
let ith = divs ! prevRow, let v = maximum [at w, ith + at (w-ith)]]
at w = if w < 0 then -n else prevA ! w
31. 해결 방법
knapsack :: Int -> UArray Int Int -> Int -> UArray Int Int
knapsack n divs len = a 0 (runSTUArray $ newArray (0,n) (0::Int)) where
a prevRow prevA
| prevRow == len = prevA
| True = currA `seq` a (prevRow+1) currA where
currA = runSTUArray $ newListArray (0,n) [w `seq` (v `seq` v) | w <- [0..n],
let ith = divs ! prevRow, let v = maximum [at w, ith + at (w-ith)]]
at w = if w < 0 then -n else prevA ! w
철저한 평가
32. 해결 방법
knapsack :: Int -> UArray Int Int -> Int -> UArray Int Int
knapsack n divs len = a 0 (runSTUArray $ newArray (0,n) (0::Int)) where
a prevRow prevA
| prevRow == len = prevA
| True = currA `seq` a (prevRow+1) currA where
currA = runSTUArray $ newListArray (0,n) [w `seq` (v `seq` v) | w <- [0..n],
let ith = divs ! prevRow, let v = maximum [at w, ith + at (w-ith)]]
at w = if w < 0 then -n else prevA ! w
Unboxed 타입
33. 해결 방법
knapsack :: Int -> UArray Int Int -> Int -> UArray Int Int
knapsack n divs len = a 0 (runSTUArray $ newArray (0,n) (0::Int)) where
a prevRow prevA
| prevRow == len = prevA
| True = currA `seq` a (prevRow+1) currA where
currA = runSTUArray $ newListArray (0,n) [w `seq` (v `seq` v) | w <- [0..n],
let ith = divs ! prevRow, let v = maximum [at w, ith + at (w-ith)]]
at w = if w < 0 then -n else prevA ! w
일부 행만 유지
34. 성능을 측정해보자
• 다시 N = 500000에 대해 테스트
• 메모리 사용 1/1000
• 속도 2.5배