haskell study 5

27
Haskell Study 5. high order functions

Upload: nam-hyeonuk

Post on 17-Jan-2017

826 views

Category:

Software


0 download

TRANSCRIPT

Haskell Study

5. high order functions

Curried Functions

커링이란 여러 개의 인자를 받는 함수를 받는 함수를 단일 인자를 받는 함수열로 바꾸는 것을

말합니다. 이렇게만 말하면 이해하기 힘드니 예제를 통해 살펴봅시다.

Prelude> max 4 55

max 함수는 위와 같이 2개의 인자를 받아서 둘 중 큰 숫자를 리턴하는 함수입니다. 그렇다면 이

함수에 단 하나의 인자만 넘겨준다면 어떻게 될까요? max 4 나 max 7처럼 말이죠. 이 경우 Haskell

은 새로운 함수를 리턴합니다. max 4는 4와 어떤 숫자를 비교해 그 중 더 큰 숫자를 리턴하는 함수를,

max 7은 7과 어떤 숫자를 비교해 그 중 더 큰 숫자를 리턴하는 함수를 반환하는 식으로요.

Curried Functions

Prelude> let maxFour = max 4Prelude> maxFour 77Prelude> maxFour 34

Haskell의 함수는 모두 커링되어 있습니다. 실제로 여러 개의 인자를 받는 함수들도 내부적으로는

단 하나의 인자만들 가지며, 하나의 인자를 넘겨서 새로운 함수를 만들고 또 그 함수에 다음 인자를

넘겨서 새로운 함수를 만들고...의 과정을 거쳐 최종적으로 결과 값을 만들어내는 거죠. 그래서 아래와

같은 표현도 가능합니다.

Prelude> (max 4) 55

Curried Functions

이러한 특징은 Haskell의 함수 타입 표현 방식에서부터 나타납니다.

max :: (Ord a) => a -> a -> a

max의 타입선언은 위와 같습니다. 여기서 하나의 인자만 적용된 경우를 생각해보면,

max :: (Ord a) => a -> (a -> a)

a 타입의 인자 하나를 받아서 a -> a, 즉 a 타입의 인자를 하나 받아서 a 타입의 값을 하나 돌려주는

함수를 리턴하는 걸로 볼 수 있죠.

Curried Functions

인자를 3개 받는 함수의 경우는 어떨까요?

multiThree :: (Num a) => a -> a -> a -> amultiThree x y z = x*y*z

이 함수의 타입 선언 역시 아래와 같이 생각할 수 있습니다.

multiThree :: (Num a) => a -> ( a -> ( a-> a ) )

따라서 multiThree 3은 숫자 두 개를 인자로 받아 그 두 숫자와 3을 곱해 리턴하는 함수를

만들어내고, multiThree 3 5 는 숫자 하나를 인자로 받아 3,5와 그 숫자를 곱해 결과를 리턴하는

함수를 만들어내게 되는 거죠. 이렇게 여러 인자를 받는 함수를 하나의 인자를 받아 또 다른 함수를

리턴하는 형태의 반복으로 변환하는 것을 커링이라고 부릅니다.

Curried Functions

이런 커링의 개념을 이용해 부분 적용된 함수(partially applied function)를 만들 수 있습니다.

Haskell에서는 함수를 원래의 인자보다 적은 개수의 인자로 호출했을 때 인자가 부분적으로 적용된

새로운 함수를 얻을 수 있고, 이는 여러 가지 고차 함수에서 굉장히 유용하게 사용됩니다.

중위 함수 역시 이런 부분 적용의 개념을 이용할 수 있습니다. 이를 섹션(section)이라고 합니다.

섹션은 중위 연산자와 적용할 함수를 같이 적은 후 이를 소괄호로 감싸면 됩니다.

Prelude> let divideByTen = (`div` 10)Prelude> divideByTen 232

뺄셈은 특수하게 (-5)가 표기의 편의상 5의 음수값으로 취급됩니다. 뺄셈에 대해 부분 적용을 하고

싶다면 substract 함수를 이용해야 합니다. (`substract` 5)

High Order Functions

고차 함수(High Order Functions)는 함수를 인자로 받거나 함수를 리턴하는 함수를 말합니다.

엄밀히 말하자면 Haskell에서 인자를 여러 개 받는 함수들은 모두 실제로는 고차함수라고 할 수

있지요. 함수를 인자로 받는 함수를 한 번 만들어봅시다.

applyTwice :: ( a -> a ) -> a -> aapplyTwice f x = f (f x)

타입 서명 (a -> a ) -> a -> a는 이 함수가 a -> a 타입의 함수 하나와 a 타입의 값 하나를 인자로

받아 a 타입의 값을 리턴한다는 의미입니다. 코드 구현을 보면 a -> a 타입의 함수 f를 인자 x에 대해

두 번 적용한 값을 리턴하고 있죠. 함수를 인자로 받을 경우에는 위와 같이 명시적으로 소괄호를 써

줘야합니다. 그냥 값을 여러 개 받는 건지 함수 타입의 값을 여러 개 받는 건지 이해할 수 없기 때문이죠

( 타입 서명에서 -> 이 우측 결합이라는 점을 염두에 두시면 됩니다).

High Order Functions

Prelude> applyTwice (+3) 1016Prelude> applyTwice (++ " HAHA") "HEY""HEY HAHA HAHA"Prelude> applyTwice ("HAHA " ++) "HEY""HAHA HAHA HEY"Prelude> applyTwice(3:) [1][3,3,1]

위와 같이 부분 적용된 함수는 고차함수와 같이 쓸 때 굉장히 유용합니다. 함수를 인자로 넘기고 싶을

때 인자로 넘어가는 함수가 단 하나의 인자만을 가져야한다면, 넘기고자하는 함수에 부분적용을

시켜서 그게 인자를 하나만 받게끔 만든다음 넘겨버리면 되는거죠.

High Order Functions

표준 함수 중 zipWith이라는 함수를 보죠.

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]zipWith _ [] _ = []zipWith _ _ [] = []zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys

이 함수는 zip 함수의 보다 일반화된 버젼입니다. (a->b->c) 타입의 함수를 인자로 받아서, a 타입

리스트와 b 타입 리스트에서 원소를 하나씩 꺼내 거기에 f 함수를 적용한 값으로 이루어진 리스트를

반환하죠. zip 함수는 여기서 각 원소에 대해 일반화된 함수를 적용하는 것이 아닌 (a,b) 꼴의 튜플을

넣어주는 것이라는 차이만 존재합니다.

High Order Functions

Prelude> zipWith (+) [4,2,5,6] [2,6,2,3][6,8,7,9]Prelude> zipWith max [6,3,2,1] [7,3,1,5][7,3,2,5]Prelude> zipWith (*) (replicate 5 2) [1..][2,4,6,8,10]Prelude> zipWith (zipWith (*)) [[1,2,3],[2,3]] [[3,2,2],[4,5]][[3,4,6],[8,15]]

이처럼 명령형 언어에서 주로 반복문, 조건문 등으로 수행되는 작업이 함수형 언어에서는 zipWith과

같은 고차함수를 이용해서 이루어집니다.

map & filter

Haskell에서 많이 쓰이기로는 아마 세 손가락 안에 꼽을 수 있는 함수로 map과 filter를 들 수 있을

것입니다. 우선 map부터 살펴봅시다.

map :: (a -> b) -> [a] -> [b]map _ [] = []map f (x:xs) = f x : map f xs

map 함수는 a -> b 타입의 함수와 a 타입의 리스트를 인자로 받아서 b 타입의 리스트를 리턴합니다.

타입 서명만 봐도 대충 어떻게 동작할 지 짐작할 수 있죠. map 함수는 간단히 말해 리스트의 각 원소에

대해 주어진 함수를 적용시킨 리스트를 리턴하는 함수입니다.

map & filter

Prelude> map (+3) [1,2,3,4,5][4,5,6,7,8]Prelude> map (replicate 3) [3..6][[3,3,3],[4,4,4],[5,5,5],[6,6,6]]Prelude> map fst [(1,2),(9,5),(3,7),(4,6)][1,9,3,4]

map을 이용하는 표현식은 거의 대부분 list comprehension을 이용해서도 구현할 수 있습니다.

예를 들어 map (+3) [1,2,3,4,5]는 [x+3 | x <- [1,2,3,4,5]]하고 동일한 의미죠. 하지만 식이

복잡하고 여러 개의 리스트에 대해 적용될 경우 map을 쓰는 것이 훨씬 가독성이 좋아집니다.

map & filter

다음은 filter 함수입니다. 우선 어떻게 구현되어 있는지부터 봅시다.

filter :: (a -> Bool) -> [a] -> [a]filter _ [] = []filter p (x:xs) | p x = x : filter p xs | otherwise = filter p xs

p x 가 True라면 x를 결과에 포함시키고, 그렇지 않다면 결과에서 제외합니다. 즉, 리스트에 대해

주어진 조건을 만족하는 원소들만 남긴 리스트를 반환하는 함수인 거죠.

map & filter

Prelude> filter (>3) [1,5,3,1,1,6,4,2][5,6,4]Prelude> filter even [1..10][2,4,6,8,10]Prelude> filter (`even` ['a'..'z']) "abAFYUAkwfyunA""abkwfyun"

filter 역시 map과 마찬가지로 list comprehension을 이용해서도 구현 가능한 경우가 많습니다.

간단한 경우에는 filter, map을 쓰든 list comprehension을 쓰든 상관없으니 자신이 생각하기에 더

가독성이 뛰어난 방식으로 작성하시면 됩니다.

lambda

람다는 기본적으로 필요할 때 한 번만 쓰기 위한 익명 함수(anonymous functions)입니다. 백 슬래쉬

(\) 뒤에 인자 목록을 적고, -> 뒤에 함수의 본체를 적는 방식으로 표현합니다.

Prelude> map (\x -> x + 3) [1,2,3,4,5][4,5,6,7,8]Prelude> filter (\x -> x `mod` 3 == 0 || x `mod` 5 == 0) [1,2,3,4,5][3,5]

위와 같이 map, filter등에 함수로 넘길 때 람다식을 이용하면 코드가 간편해지는 경우가 많습니다.

연습 문제

• inverse어떤 숫자들의 리스트가 주어져 있을 때 그 숫자들의 역수로 이루어진 리스트를 반환하는 함수를

만들어 봅시다. 예를 들어 inverse [1,2,4,5] 는 [1.0, 0.5, 0.25, 0.2]를 반환해야합니다.

• longList리스트의 리스트를 인자로 받아 원소 개수가 3개 이상인 리스트들만 남기는 함수를 작성해봅시다.

예를 들어 longList [[1,2,3,4,5],[1,2],[],[1,2,3,4],[1]] 은 [[1,2,3,4,5],[1,2,3,4]]를

반환해야합니다.

• dot2차원 벡터(두 수의 페어pair로 표현합시다)의 리스트 두 개를 인자로 받아 리스트의 각 원소에 대해

내적(각 성분의 곱의 합 - x1*x2 + y1*y2)을 수행한 리스트를 리턴하는 함수를 작성해봅시다. 예를

들어 dot [(1,2),(3,5)] [(4,7),(6,8)]은 [18, 58]을 리턴해야합니다.

fold

fold개념은 map, filter만큼이나 자주 쓰이는 개념입니다. Haskell에는 foldl과 foldr이라는 두 개의

함수가 있습니다. 우선 foldl (left fold)부터 봅시다.

sum' :: (Num a) => [a] -> asum' xs = foldl (\acc x -> acc + x) 0 xs

위는 foldl을 이용한 sum 함수의 구현입니다. foldl은 두 개의 인자를 받는 함수와 시작값 그리고

리스트를 원소로 받아 시작값으로부터 리스트의 원소에 대해 인자로 받은 함수를 반복적으로 적용시켜

최종 결과 값을 얻습니다. foldl은 리스트의 왼쪽부터 접기(fold) 때문에 누적값이 왼쪽에 옵니다. 다음

페이지에 예제 흐름이 있으니 참고해주세요.

fold

sum’ :: (Num a) => [a] -> asum’ xs = foldl (\acc x -> acc + x) 0 xs

foldl (\acc x -> acc + x) 0 [1,2,3]

(\0 1 -> 0 + 1)

foldl (\acc x -> acc + x) 1 [2,3]

(\1 2 -> 1 + 2)

foldl (\acc x -> acc + x) 3 [3]

(\3 3 -> 3 + 3)

foldl (\acc x -> acc + x) 6 []

result : 6

fold

또 다른 예제로 elem 함수를 foldl을 이용해서 구현해봅시다.

elem' :: (Eq a) => a -> [a] -> Boolelem' x xs = foldl (\acc y -> if x == y then True else acc) False xs

foldl에서 시작값을 False로 둔 다음, xs의 각 원소에 대해 찾고자 하는 원소가 있을 경우 계산중인

누적값을 True로 바꾸고, 아니면 그냥 그 값을 유지시킨 채 다음 원소로 넘어갑니다. 따라서 찾고자 하는

원소가 있을 경우 중간에 계산 값이 True로 바뀐 채 끝까지 갈 것이므로 True가, 찾고자 하는 원소가 없을

경우 시작값 False에서 값이 한 번도 바뀌지 않은 채 끝까지 갈 것이므로 False가 리턴될 것입니다.

foldr은 foldl과 완전히 동일한 동작을 하지만, 리스트의 오른쪽 끝부터 시작해서 왼쪽 방향으로 원소를

하나씩 탐색하며 함수에 적용한다는 점이 다릅니다. 따라서 인자로 받는 함수도 왼쪽에 리스트의 값,

오른쪽에 계산중인 값이 들어오게 됩니다.

fold

foldl과 foldr하고 유사하게 foldl1, foldr1이 있습니다. 이 함수들은 동작 방식은 foldl, foldr과

완전히 같으나 계산의 시작값을 리스트의 첫 번째 원소로 삼는다는 점 하나만이 다릅니다. fold를 좀

더 이해하기 위해 Haskell의 표준 함수 몇 가지를 foldl를 이용해서 정의해봅시다. 천천히 읽으면서 왜

그렇게 되는 지 고민해보시면 아마 fold의 동작을 완벽히 이해할 수 있을 것입니다.

map f xs = foldr (\x acc -> f x:acc) [] xs

reverse xs = foldl (\acc x -> x:acc) []

filter p xs = foldr (\x acc -> if p x then p:acc else acc) [] xs

product xs = foldl (*) 1 xs

$

Haskell의 독특한 함수중 $라는 함수가 있습니다. 이 함수는 함수 적용(function application)

이라고 불리는데, 다음과 같이 정의되어 있습니다.

($) :: (a -> b) -> a -> bf $ x = f x

이 함수는 정말로 아무 일도 하지 않습니다. 다만 주어진 식을 실행할 뿐이죠. 이게 어디에 쓸모가

있을까싶지만, 이 함수는 함수형 언어에서 필연적일 수 밖에 없는 괄호의 남발을 줄여주는 역할을

합니다. 왜냐하면 $ 함수는 우측 결합이기 때문에, $ 오른쪽의 식을 먼저 계산한 다음 왼쪽에 그 결과를

적용시키기 때문이죠.

$

sum (map (+3) [1..100])sum $ map (+3) [1..100]

sum (map (+3) (filter even [1..100]))sum $ map (+3) $ filter even [1..100]

위와 같이 여러 식이 중첩되어 있을 때 $ 함수를 적절히 적용하면 과도하게 많은 괄호의 숫자를 줄일 수

있습니다. 코드에 괄호가 너무 많아 산만하다는 생각이 든다면 $ 함수를 이용하시면 됩니다. 또 $는 그

자체로 함수이기 때문에, 함수의 리스트에 대해 인자를 넘길 때도 이용할 수 있습니다.

Prelude> map ($3) [(4+), (10*), (^2)][7,30,9]

Function Composition

Haskell에서 함수 합성(function composition)는 수학에서의 함수 합성과 굉장히 유사합니다.

합성함수는 . 함수를 이용해 만들 수 있으며, (f . g) x 는 f (g x)와 동일합니다. . 함수가 어떻게

정의되어 있는지 봅시다.

(.) :: (b -> c) -> (a -> b) -> a -> cf . g = \x -> f (g x)

합성함수 역시 코드를 깔끔하게 작성할 수 있게 만들어줍니다. 예를 들어 어떤 숫자의 리스트를 받아

그 원소를 모두 음수로 바꾸는 함수를 생각해봅시다.

allNegate xs = map (\x -> negate (abs x)) xsallNegate' xs = map (negate . abs) xs

Function Composition

함수 합성은 기본적으로 우측 결합이기 때문에 한 번에 많은 개수의 함수도 합성해낼 수 있습니다.

foo xs = map (\x -> negate (sum (tail x))) xsfoo' xs = map (negate . sum . tail) xs

Prelude> foo' [[1..5],[3..6],[1..7]][-14,-15,-27]

Function Composition

Haskell의 모든 함수는 기본적으로 커링되기 때문에, 함수의 인자를 생략할 수 있습니다. 이를

함수의 point-free definition 이라고 합니다. 예를 들어 아래의 함수

sum xs = foldl (+) 0 xs

sum = foldl (+) 0

과 같이 정의할 수 있습니다. foldl (+) 0은 커링에 의해 부분 적용되어 인자를 하나 받는 함수를

리턴하기 때문이죠. 이러한 표현 방법이 데이터 보다는 함수 그 자체에 집중하게 만들어주기 때문에 더

가독성이 좋은 경우들이 있습니다.

Function Composition

앞에서 말한 point-free definition을 할 때에도 합성 함수가 종종 이용됩니다. 만약 함수가 아래와

같이 정의되어 있다면, poin-free definition을 할 수 없습니다.

fn x = ceiling (negate (tan (cos (max 50 x))))

함수가 인자로 받는 오른 쪽에 닫는 괄호가 있기 때문이죠. x를 생략하고 cos (max 50)이라고

적어버리면 의미가 완전히 달라집니다. cos 함수의 인자로 max 50이라는 함수를 넘기고 있는

셈인데, 이 건 타입 정의에서부터 말이 안되죠. 반면 합성 함수를 사용할 경우 아래와 같이 point-free

definition이 가능해지죠.

fn = ceiling . negate . tan . cos . max 50

연습 문제

• finalPosition2차원 평면 상에서 첫 위치와 (x,y) 좌표 이동 목록이 주어졌을 때 이동한 후 최종 위치를 구하는

함수를 작성해봅시다. 예를 들어 finalPosition (3,5) [(1,1), (-1,2), (2,4)]은

(5,12)를 리턴해야합니다.

• nineDigit어떤 정수(Integer)가 주어졌을 때, 그 숫자의 자릿수중 9의 개수가 몇 개인지를 리턴하는 함수를

만들어봅시다. 예를 들어 nineDigit 9991235819913 은 5를 리턴해야합니다.