Haskell

#코딩노예#·2022년 10월 3일
0

Haskell

목록 보기
1/1

실행

$ stack ghci

Configuring GHCi with the following packages: 
GHCi, version 9.0.2: https://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /tmp/haskell-stack-ghci/2a3bbd58/ghci-script
ghci> 


하스켈 특성

Functional : 프로그램의 기본 구성 요소는 함수입니다.
Pure : 하스켈의 함수는 side effects 가 없습니다.
Lazy : 변수 값이 사용될 때 결정됩니다.
Strongly typed : 하스켈의 모든 값과 식은 타입을 가집니다.
Type inferred : 컴파일러가 타입 추론을 합니다.
Garbage-collected : 하스켈은 garbage collection 에 대해 자동적으로 메모리 관리를 해줍니다.
Compiled : 하스켈은 compiled language 입니다.



고차 함수

하스켈은 함수의 인자로 함수를 줄 수 있습니다.
함수를 연달아 적용하려고 할 때 편리합니다.

Prelude> map length["abc", "abcdef"]
[3, 6]


함수 선언

[함수명] [인자] = [내용]

Ex)
Prelude> longerThanOne x = length x > 1
Prelude> longerThanOne "abc"
True


무명 함수

하스켈에서는 \를 이용해서 이름이 없는 함수를 만들 수 있습니다.
한번만 사용하고 말 함수를 선언할 때 편리합니다.

\[함수명] -> [내용]

Ex)
Prelude> filter (\x -> length x > 1) ["abc", "d", "ef"]
["abc", "ef"]


부분 함수 호출

하스켈에서는 인자중 일부를 비워두고 함수를 호출할 수 있습니다.

Prelude> mul3 x = x * 3
Prelude> map mul3 [1,2,3]
[3,6,9]
Prelude> map (*3) [1,2,3]
[3,6,9]

함수를 만들어서 처리해야 할 일을 부분 함수 호출을 이용해서 간단하게 처리할 수 있습니다.



대수적 자료형

하스켈에서는 원하는 데이터 타입을 만들 수 있습니다.

Prelude> data Character = Pororo | Pokemon | Unknown deriving Show
Prelude> Pororo
Pororo
Prelude> Pokemon
Pokemon
Prelude> Unknown 
Unknown
Prelude> data Shape = Point | Rectangle Double Double | Circle Double deriving Show
Prelude> Point
Point
Prelude> Rectangle 10.0 20.0
Rectangle 10.0 20.0
Prelude> Circle 30.0
Circle 30.0


패턴 매칭

함수를 간결하게 짜기에 매우 유용합니다.

Prelude> data Character = Pororo | Pokemon | Unknown deriving Show
Prelude> :{
Prelude| menu 1 = Pororo
Prelude| menu 2 = Pokemon
Prelude| menu _ = Unknown
Prelude| :}
Prelude> menu 1
Pororo
Prelude> menu 2
Pokemon
Prelude> menu 7
Unknown
Prelude> data Shape = Point | Rectangle Double Double | Circle Double deriving Show
Prelude> :{
Prelude| area Point = 0
Prelude| area (Rectangle width height) = width * height
Prelude| area (Circle radius) = 2 * pi * radius
Prelude| :}
Prelude> area(Point)
0.0
Prelude> area(Rectangle 10 20)
200.0
Prelude> area(Circle 3)
18.84955592153876


리스트

타입이 같은 값들의 나열

Prelude> [1, 2, 3, 4, 5]
[1,2,3,4,5]
Prelude> []
[]
Prelude> [1, "Eva"]

<interactive>:38:2: error:
    • No instance for (Num [Char]) arising from the literal ‘1’
    • In the expression: 1
      In the expression: [1, "Eva"]
      In an equation for ‘it’: it = [1, "Eva"]


리스트 제시법

(수학) 집합 조건 제시법 : { x | 자연수에 속하는 x에 대하여 x는 짝수 }

(하스켈) 리스트 제시법 : [x | x <- [1..], even x]
Ex) 짝수로 이루어진 리스트에서 10개만 출력하기

Prelude> take 10 [x | x <- [1..], even x]
[2,4,6,8,10,12,14,16,18,20]


reverse, tail, head

ghci> reverse("asdf")
"fdsa"

ghci> tail("asdf")
"sdf"

ghci> head("asdf")
'a'


Syntax of Expressions

HaskellPython, Java or C
g h f 1g ( h, f, 1 )
g h ( f 1 )g ( h, f ( 1 ) )
g ( h f 1 )g ( h ( f , 1 ) )
g ( h ( f 1 ) )g ( h ( f (1) ) )

HaskellPython, Java or C
a + ba + b
f a + g bf ( a ) + g ( b )
f ( a + g b )f ( a + g ( b ) )


Syntax of Types

TypeLiteralsOperations
Int1, 2, -3+, -, *, div, mod
Integer1, -2, 90000000+, -, *, div, mod
Double0.1, 2.5+, -, *, /, sqrt
BoolTrue, False&&, ||, not
String"abcd", ""reverse, ++


하스켈 프로그램 구조

module Gold where

-- The golden ratio
phi :: Double
phi = (sqrt 5 + 1) / 2

polynomial :: Double -> Double
polynomial x = x ^ 2 - x - 1

f x = polynomial (polynomial x)

main = do
    print (polynomial phi)
    print (f phi)

module Gold where

Gold 라는 모듈을 작성

-- The golden ratio

한줄 주석

{-
	hello
-}

여러줄 주석

phi :: Double
phi = (sqrt 5 + 1) / 2

phi 변수를 Double 형으로 선언 (타입 시그니처)
phi 변수에 sqrt(5+1) / 2를 대입

polynomial :: Double -> Double
polynomial x = x ^ 2 - x - 1

polynomial 함수의 인자, 리턴값 모두 Double 형이다.
x = x ^ 2 -x - 1 연산을 하는 polynomial 함수 선언

f x = polynomial (polynomial x)

x를 인자로 polynomial 함수를 호출한 결과를 다시 인자로 넣어서 polynomial 함수를 호출하는 함수 f를 선언

main = do
    print (polynomial phi)
    print (f phi)

main 함수
do ⇾ IO 역할을 하는 문법 구문



예시 코드 실행

ghci> polynomial x = x ^ 2 - x - 1
ghci> polynomial 3.0
5.0

인터프리터에서 한줄 한줄 작성하기

ghci> :{
ghci| polynomial :: Double -> Double
ghci| polynomial x = x ^ 2 - x - 1
ghci| :}
ghci> polynomial 3.0
5.0

인터프리터에서 여러줄 작성하기

--Example.hs
polynomial :: Double -> Double
polynomial x = x ^ 2 - x - 1
ghci> :load Example.hs 
[1 of 1] Compiling Main             ( Example.hs, interpreted )
Ok, one module loaded.
ghci> polynomial 3.0
5.0

파일로 작성후 인터프리터에서 로드해서 실행하기

파일을 수정했을 때

--Example.hs
polynomial :: Double -> Double
polynomial x = x ^ 2 - x - 5
ghci> :reload
[1 of 1] Compiling Main             ( Example.hs, interpreted )
Ok, one module loaded.
ghci> polynomial 3.0
1.0

:reload로 재로딩



에러 다루기 1

문자열에 불리언을 붙이려고 했을 때 에러

ghci> "string" ++ True

<interactive>:13:13: error:
    • Couldn't match expected type[Char]’ with actual type ‘Bool’
    • In the second argument of ‘(++)’, namely ‘True’
      In the expression: "string" ++ True
      In an equation for ‘it’: it = "string" ++ True

• Couldn't match expected type[Char]’ with actual type ‘Bool’

[Char]형이 나올거라고 기대했는데 Bool 형이 나오네

<interactive>:13:13: error:

인터프리터에서 13번째 줄 13번째 글자에서 에러 발생

In the expression: "string" ++ True

"String" ++ True 식에서 에러 발생

In an equation for ‘it’: it = "string" ++ True

it = "string" ++ True 부분에서 에러 발생



에러 다루기 2

ghci> True + 1

<interactive>:14:6: error:
    • No instance for (Num Bool) arising from a use of ‘+’
    • In the expression: True + 1
      In an equation for ‘it’: it = True + 1

    • No instance for (Num Bool) arising from a use of ‘+’

숫자가 아닌 변수에 + 연산을 가했습니다.



에러 다루기 3

ghci> True +

<interactive>:15:7: error:
    parse error (possibly incorrect indentation or mismatched brackets)

parse error : 구문이 완전하지 않다



산술 연산

ghci> 7 `div` 2
3

정수 나눗셈에서는 div 사용

ghci> 7.0 / 2.0
3.5

실수형 나눗셈에서는 / 사용

만약 정수형 변수에 실수형 나눗셈 연산을 가하면?

ghci> :{
ghci| halve :: Int -> Int
ghci| halve x = x / 2
ghci| :}

<interactive>:21:13: error:
    • No instance for (Fractional Int) arising from a use of ‘/’
    • In the expression: x / 2
      In an equation for ‘halve’: halve x = x / 2

에러 발생



조건문

ghci> price = if product == "milk" then 1 else 2


비교 연산자

연산자설명
==같다
<작다
<=작거나 같다
>크다
>=크거나 같다
/=다르다

ghci> "foo" == "bar"
False
ghci> 5.0 <= 7.0
True
ghci> 1 == 1
True
ghci> 2 /= 3
True
ghci> "bike" /= "bike"
False

ghci> :{
ghci| checkPassword password = if password == "swordfish"
ghci| 							then "You're in."
ghci| 							else "ACCESS DENIED!"
ghci| :}
ghci> :type checkPassword 
checkPassword :: String -> String
ghci> checkPassword "Hello"
"ACCESS DENIED!"
ghci> checkPassword "swordfish"
"You're in."

ghci> absoluteValue n = if n < 0 then -n else n
ghci> :type absoluteValue 
absoluteValue :: (Ord a, Num a) => a -> a
ghci> absoluteValue 123
123
ghci> absoluteValue (-123)
123

ghci> :{
ghci| login user password = if user == "unicorn73"
ghci| 						then if password == "f4bulous!"
ghci| 							then "unicorn73 logged in"
ghci| 							else "wrong password"
ghci| 						else "unkown user"
ghci| :}
ghci> :type login
login :: String -> String -> String
ghci> login "hello" "mypassword"
"unkown user"
ghci> login "unicorn73" "mypassword"
"wrong password"
ghci> login "unicorn73" "f4bulous!"
"unicorn73 logged in"


Local Definitions

circleArea :: Double -> Double
circleArea r = pi * rsquare
    where pi = 3.1415926
          rsquare = r * r
ghci> :load main.hs 
[1 of 1] Compiling Main             ( main.hs, interpreted )
Ok, one module loaded.
ghci> circleArea 5
78.539815

circleArea r = let pi = 3.1415926
                   rsquare = r * r
                in pi * rsquare
ghci> :reload
Ok, one module loaded.
ghci> circleArea 5
78.539815

circleArea r = pi * square r
    where pi = 3.1415926
          square x = x * x
ghci> :reload
[1 of 1] Compiling Main             ( main.hs, interpreted )
Ok, one module loaded.
ghci> circleArea 5
78.539815

circleArea r = let pi = 3.1415926
                   square x = x * x
                in pi * square r
ghci> :reload
[1 of 1] Compiling Main             ( main.hs, interpreted )
Ok, one module loaded.
ghci> circleArea 5
78.539815


Immuatability

하스켈에서는 한번 값에 변수 이름을 붙이면 변수 값을 바꿀 수 없습니다.

increment x = let x = x + 1
              in x
ghci> :reload
[1 of 1] Compiling Main             ( main.hs, interpreted )
Ok, one module loaded.
ghci> increment 1
...

x = x + 1 이 아니라 1+1+1+1+... 라는 식으로 계산됩니다.


compute x = let a = x + 1
                a = a * 2
            in a
ghci> :reload
Ok, one module loaded.
ghci> compute 3

<interactive>:19:1: error:
    • Variable not in scope: compute :: t0 -> t
    • Perhaps you meant ‘compare’ (imported from Prelude)

다른 언어에서는 x에 1을 더한 값을 a 넣고 그 값을 다시 2를 곱해서 a에 넣는 식으로 해석되겠지만, 하스켈에서는 변수 a에 대한 값의 업데이트가 불가능하기 때문에 에러가 발생합니다.


x :: Int
x = 5

f :: Int -> Int
f x = 2 * x

g :: Int -> Int
g y = x where x = 6

h :: Int -> Int
h x = x where x = 3
ghci> :reload
[1 of 1] Compiling Main             ( main.hs, interpreted )
Ok, one module loaded.

local definitions 의 변수는 동일한 이름의 다른 변수를 가릴수 있습니다.


f 1 ==> 2
g 1 ==> 6
h 1 ==> 3

f x ==> 10
g x ==> 6
h x ==> 3
ghci> :reload
Ok, one module loaded.


Pattern Matching

greet :: String -> String -> String
greet "Finland" name = "Hei, " ++ name
greet "Italy"   name = "Ciao, " ++ name
greet "England" name = "How do you do, " ++ name
greet _         name = "Hello, " ++ name
ghci> :reload
[1 of 1] Compiling Main             ( main.hs, interpreted )
Ok, one module loaded.
ghci> greet "Finland" "Pekka"
"Hei, Pekka"
ghci> greet "England" "Bob"
"How do you do, Bob"
ghci> greet "Greenland" "Jan"
"Hello, Jan"

describe :: Integer -> String
describe 0 = "zero"
describe 1 = "one"
describe 2 = "an even prime"
describe n = "the number " ++ show n
ghci> :reload
[1 of 1] Compiling Main             ( main.hs, interpreted )
Ok, one module loaded.
ghci> describe 0
"zero"
ghci> describe 2
"an even prime"
ghci> describe 7
"the number 7"

pattern matching 코드를 조건문으로 바꿔보면

describe n = if n == 0 then "zero"
			 else if n==1 then "one"
             else if n==2 then "an even prime"
             else "the number " + show n

login :: String -> String -> String
login "unicorn73" "f4bulous!" = "unicorn73 logged in"
login "unicorn73" _           = "wrong password"
login _           _           = "unkown user"

_ : 아무거나 들어와도 매칭이 된다.

ghci> login "user" "password"
"unkown user"
ghci> login "unicorn73" "password"
"wrong password"
ghci> login "unicorn73" "f4bulous!"
"unicorn73 logged in"

pattern matching 코드를 조건문으로 바꿔보면

login id pw = if id=="unicorn73:
			  then if pw=="f4bulous!" then "unicorn73 logged in"
              	   else "wrong password"
               else "unkown user"


Recursion

 - factorial n = { - n! - }
 
 ===============================================
 (1) n = 1 : 	n! = 1				base
 (2) n > 1 : 	n! = n x (n-1)!		inductive
 ===============================================
 
 n! = n * (n-1) * ... * 2 * 1
 (n-1)! = (n-1) * ... * 2 * 1

factorial 1 = 1
factorial n = n * factorial(n-1)
ghci> factorial 5
120
ghci> factorial 4
24

 - squareSum n = { - 1^2 + 2^2 + ... + n^2 - }
 
 =================================================
 (1) n = 1 : 1 ^ 2 ==> 1
 (2) n > 1 : squareSum (n-1) + n^2
 =================================================
 
 squareSum n
 	= 1^2 + 2^2 + ... + (n-1)^2 + n^2
    = ( 1^2 + 2^2 + ... + (n-1)^2 ) + n^2
    = squareSum (n-1)				+ n^2

squareSum 1 = 1
squareSum n = n^2 + squareSum(n-1)
ghci> squareSum 3
14
ghci> squareSum 4
30

 - fibonacci n = { - n번째 피보나치 수열의 값 - }
 
 =================================================
 	n = 1: 1번째 피보나치 수열의 값 = 1
    n = 2: 2번째 피보나치 수열의 값 = 1
    n > 2:
    	n번째 피보나치 수열의 값 =
        n-1번째 피보나치 수열의 값 + n-2번째 피보나치 수열의 값
 =================================================
        
 - 예시: fibonacci 7 = 13
 
 	1 2 3 4 5 6 7 8 ...
    1 1 2 3 5 8 13 21 ...

fibonacci 1 = 1
fibonacci 2 = 1
fibonacci n = fibonacci(n-1) + fibonacci(n-2)
ghci> fibonacci 5
5
ghci> fibonacci 3
2
ghci> fibonacci 7
13


Indentation

하스켈은 들여쓰기 칸을 맞춰서 해줘야 합니다.

ghci> :{
ghci| k = a + b
ghci|     where a = 1
ghci|           b = 1
ghci| :}

들여쓰기를 잘 맞춰주면 에러 없이 잘 컴파일 됩니다.


들여쓰기를 잘 맞춰주지 않으면

i x = let y = x+x+x+x+x+x
in div y 5

j x = let y = x+x+x
	  +x+x+x
      in div y 5
      
k = a + b
	where a = 1
       b = 1

l = a + b
where
	a = 1
    b = 1
ghci> :reload
[1 of 1] Compiling Main             ( main.hs, interpreted )

main.hs:2:1: error:
    parse error (possibly incorrect indentation or mismatched brackets)
  |
2 | in div y 5
  | ^
Failed, no modules loaded.

첫번째 줄부터 에러가 발생합니다.


들여쓰기를 맞춰주면

i x = let y = x+x+x+x+x+x
      in div y 5

j x = let y = x+x+x
	          +x+x+x
      in div y 5
      
k = a + b
	where a = 1
          b = 1

l = a + b
    where
	    a = 1
        b = 1

Indentation Rule

1. 같이 그룹화 되어야할 것들은 같은 라인에 맞춘다.
2. 너무 길어서 여러줄에 해야 한다면 한두개 정도 공백을 넣어서 들여쓰기를 한다.



Recursion and Helper Functions

주어진 인자와 주어진 타입으로 재귀함수를 작성할때 주어진 인자만 가지고 변수가 부족한 경우 Helper Functions을 사용할 수 있습니다.

helper variables를 도입해서 재귀호출을 하는 Helper Functions을 만들고 본 함수에서 Helper Functions를 호출합니다.


repeatString

repeatString n str = repeatHelper n str ""

repeatHelper n str result = if (n==0)
							then result
                            else repeatHelper (n-1) str (result++str)
ghci> repeatString 3 "Hello "
"Hello Hello Hello "

패턴 매칭 이용

repeatString n str = repeatHelper n str ""

repeatHelper 0 _ result = result
repeatHelper n str result = repeatHelper (n-1) str (result++str)

fibonacci

--fibonacci numbers, fast version
fibonacci :: Integer -> Integer
fibonacci n = fibonacci' 0 1 n

fibonacci' :: Integer -> Integer -> Integer -> Integer
fibonacci' a b 1 = b
fibonacci' a b n = fibonacci' b (a+b) (n-1)
ghci> fibonacci 5
5


Guards

if then else 문을 간결하게 표현하고 싶을 때 사용합니다.

f x y z
 | condition1 = something
 | condition2 = other
 | otherwise = somethingother

otherwise ==> True 모든 조건을 불만족 시키면 실행


Guards패턴 매칭과 달리 조건식에 범위가 올 수 있습니다.

describe :: Int -> String
describe n
 | n == 2	 = "Two"
 | even n	 = "Even"
 | n==3		 = "Three"
 | n>100	 = "Big!!"
 | otherwise = "The number "++show n

factorial n
 | n<0		 = -1
 | n==0		 = 1
 | otherwise = n * factorial (n-1)

패턴 매칭Guards는 함께 사용할 수 있습니다.

guessAge :: String -> Int -> String
guessAge "Griselda" age
    | age < 47 = "Too low!"
    | age > 47 = "Too high!"
    | otherwise = "Correct!"
guessAge "Hansel" age
    | age < 12 = "Too low!"
    | age > 12 = "Too high!"
    | otherwise = "Correct!"
guessAge name age = "Wrong name!"
ghci> guessAge "Griselda" 30
"Too low!"
ghci> guessAge "Griselda" 60
"Too high!"
ghci> guessAge "Griselda" 47
"Correct!"
ghci> guessAge "Bob" 30
"Wrong name!"
ghci> guessAge "Hansel" 10
"Too low!"


Lists

하스켈에서 리스트의 원소는 같은 타입으로 이루어져 있어야 합니다.

ghci> [1,2,4,8]
[1,2,4,8]
ghci> [True, False]
[True,False]
ghci> ["Hi", "Bye"]
["Hi","Bye"]
ghci> [ [1,2], [3,4] ]
[[1,2],[3,4]]
ghci> [1,5,"hi"]

<interactive>:15:2: error:
    • No instance for (Num String) arising from the literal ‘1’
    • In the expression: 1
      In the expression: [1, 5, "hi"]
      In an equation for ‘it’: it = [1, 5, "hi"]

List Operations

head :: [a] -> a            -- 첫번째 원소 반환
tail :: [a] -> [a]          -- 첫번째 원소를 제외한 모든 원소 반환
init :: [a] -> [a]          -- 마지막 원소를 제외한 모든 원소 반환
take :: Int -> [a] -> [a]   -- 앞에서 부터 n개의 원소 반환
drop :: Int -> [a] -> [a]   -- 앞에서 부터 n개의 원소를 제외한 원소 반환
(++) :: [a] -> [a] -> [a]   -- 리스트 합치기
(!!) :: [a] -> Int -> a     -- 리스트 인덱스 접근
reverse :: [a] -> [a]       -- 리스트 뒤집기
null :: [a] -> Bool         -- 리스트 비어 있는지 확인
length :: [a] -> Int        -- 리스트 길이 확인
ghci> list = [1..10]

ghci> head list
1

ghci> tail list
[2,3,4,5,6,7,8,9,10]

ghci> init list
[1,2,3,4,5,6,7,8,9]

ghci> take 5 list
[1,2,3,4,5]

ghci> drop 5 list
[6,7,8,9,10]

ghci> list2 = [11..20]
ghci> list ++ list2
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]

ghci> list !! 0
1

ghci> reverse list
[10,9,8,7,6,5,4,3,2,1]

ghci> null list
False

ghci> length list
10

정렬

ghci> import Data.List
ghci> sort [1, 0, 5, 3]
[0,1,3,5]


Immutability

하스켈에서는 한번 값에 이름을 붙여 변수를 만들면 그 값을 변경할 수 없습니다.

Prelude> list = [1,2,3,4]
Prelude> reverse list
[4,3,2,1]
Prelude> list
[1,2,3,4]
Prelude> drop 2 list
[3,4]
Prelude> list
[1,2,3,4]

listreverse 함수와 drop 함수를 사용해보면 원래 리스트의 값은 변하지 않고 새로운 리스트가 반환됩니다.


Mutability vs Immutability

Mutability : 변수 값을 변경할 수 없어서 새로운 변수를 만들어 반환하는 Immutability 와 달리 그냥 변수 값을 변경하여 반환하면 되기 때문에 메모리가 절약됩니다.

Immutability : 여러 함수에서 변수를 포인터로 참조하고 있는 경우 특정 함수에서 변수 값을 변경해 버렸을 때 다른 함수의 실행에 영향이 생겨 예기치 못한 오류가 발생할 수 있는데, 변수의 값의 변경이 불가능하면 이러한 오류를 방지할 수 있습니다.



Type Inference and Polymorphism

Type Inference (타입 변수) : 어떠한 타입도 될 수 있는 변수

[a] -> a
-------------------------------------------------
[a] ==> 여러 타입이 올 수 있는 리스트
 a  ==> 여러 타입이 올 수 있는 변수

Polymorphism (다형성) : 여러 타입을 가질 수 있는 것
Polymorphism 함수 : 여러 타입이 올 수 있는 함수

head :: [a] -> a

Type Inference (타입 유추) : 타입을 유추하는 것

head :: [a] -> a
head [True,False] :: Bool
-------------------------------------------------
[a] -> a
[Bool] -> a
a ==> Bool
f xs ys = [head xs, head ys]
-------------------------------------------------
head :: [a] -> a
xs ==> [a]
ys ==> [a]
f xs ys ==> [a]

f :: [a] -> [a] -> [a]
f :: [a] -> [a] -> [a]
g zs = f "Moi" zs
-------------------------------------------------
"Moi" ==> [a]
zs ==> [a]

String ==> [Char]
[a] ==> [Char]
zs ==> [Char]

g zs ==> [a]
g zs ==> [Char]

g :: [Char] -> [Char]

용어

type parameter : 리스트 안에 들어오는 타입
Ex) [Char]type parameterChar

parameterized type : 리스트처럼 파라미터를 갖는 타입

parametric polymorphism : 타입 변수를 사용하는 형태



Maybe Type

함수를 호출했을 때 정상 뿐만 아니라 비정상 결과도 나올때 사용합니다.

NothingJust 생성자를 사용합니다.
Nothing은 그 자체가 생성자, Just는 또다른 파라미터를 받습니다.

정상적인 경우에는 Just parameter를 리턴하고 비정상적인 경우에는 Nothing을 리턴합니다.


Prelude> Nothing :: Maybe Int
Nothing
Prelude> Just 0 :: Maybe Int
Just 0
Prelude> Just (-1) :: Maybe Int
Just (-1)
Prelude> Just "a camel"
Just "a camel"
Prelude> :t Just "a camel"
Just "a camel" :: Maybe [Char]

아래 처럼 login 함수를 짜면?

def login(pwd)
  if pwd == "f4bulous!":
  	return "unicorn73"
  else if pwd == "swordfish"
  	return "megahacker"
  else
  	return ""

⇾ 로그인 실패를 빈 문자열로 가정하고 코드 처리
⇾ 시간 지나면 위 가정을 까먹고 코딩
⇾ 잘못된 코드를 짤 수 있음

Haskell에서는 Nothing을 통해 예외 처리 가능

-- given a password, return (Just username) if login succeeds, Nothing otherwise
login :: String -> Maybe String
login "f4bulous!" = Just "unicorn73"
login "swordfish" = Just "megahacker"
login _           = Nothing

비밀번호를 맞게 입력하면 Just username을 리턴하고 잘못된 비밀번호를 입력하면 Nothing을 리턴합니다.

*Main> login "f4bulous!"
Just "unicorn73"
*Main> login "swordfish"
Just "megahacker"
*Main> login "aaa"
Nothing

-- Multiply an Int with a Maybe Int. Nothing is treated as no multiplication at all.
perhapsMultiply :: Int -> Maybe Int -> Int
perhapsMultiply i Nothing = i
perhapsMultiply i (Just j) = i*j   -- Note how j denotes the value inside the Just

곱셈을 할때 두번째 숫자가 안주어지면 첫번째 숫자를 반환하고 두 숫자 모두 주어지면 계산 결과를 리턴합니다.

*Main> perhapsMultiply 3 (Just 2)
6
*Main> perhapsMultiply 5 Nothing
5

safeHead :: [a] -> Maybe a
safeHead xs = if null xs then Nothing else Just (head xs)

head를 할때 빈 리스트가 주어지면 오류가 발생하는데,
Maybe Type을 이용하면 safeHead함수를 만들수 있습니다.

*Main> safeHead [1..5]
Just 1
*Main> safeHead []
Nothing

값이 들어있는 리스트가 들어오면 head 연산 결과를 리턴하고 비어있는 리스트가 들어오면 Nothing을 리턴합니다.



Either type

비정상적인 상황이 여러개 일때 이를 구분하기 위해 사용합니다.
Either a b로 두개의 인자를 받습니다.
Left and Right 두개의 생성자를 갖습니다.
Left 생성자는 a라는 인자를 갖고 Right 생성자는 b라는 인자를 갖습니다.


*Main> Left 0 :: Either Int Bool
Left 0
*Main> Left "Abc" :: Either String Int
Left "Abc"
*Main> Right True :: Either Int Bool
Right True

readInt :: String -> Either String Int
readInt "0" = Right 0
readInt "1" = Right 1
readInt s = Left ("Unsupported string: " ++ s)
*Main> readInt "0"
Right 0
*Main> readInt "1"
Right 1
*Main> readInt "5"
Left "Unsupported string: 5"

iWantAString :: Either Int String -> String
iWantAString (Right str)   = str
iWantAString (Left number) = show number
*Main> iWantAString (Left 123)
"123"
*Main> iWantAString (Right "abc")
"abc"

[Left 1, Right "foo", Left 2] :: [Either Int String]


Case 문

case <value> of <pattern> -> <expression>
                <pattern> -> <expression>

describe :: Integer -> String
describe 0 = "zero"
describe 1 = "one"
describe 2 = "an even prime"
describe n = "the number " ++ show n
describe :: Integer -> String
describe n = case n of 0 -> "zero"
                       1 -> "one"
                       2 -> "an even prime"
                       n -> "the number " ++ show n

패턴 매칭을 사용했을 때 보다 더 간결하게 코드를 짤 수 있습니다.


언제 case문을 사용할까?

parse 한다 ⇾ 구조를 파악한다.

-- parse country code into country name, returns Nothing if code not recognized
parseCountry :: String -> Maybe String
parseCountry "FI" = Just "Finland"
parseCountry "SE" = Just "Sweden"
parseCountry _ = Nothing

flyTo :: String -> String
flyTo countryCode = case parseCountry countryCode of Just country -> "You're flying to " ++ country
                                                     Nothing -> "You're not flying anywhere"

case 문을 사용하지 않고 패턴 매칭을 사용했다면, "You're flying to {Country Name}"을 여러번 입력해야 해서 반복으로 인해 비효율적이었을 것입니다.


motivate :: String -> String
motivate "Monday"    = "Have a nice week at work!"
motivate "Tuesday"   = "You're one day closer to weekend!"
motivate "Wednesday" = "3 more day(s) until the weekend!"
motivate "Thursday"  = "2 more day(s) until the weekend!"
motivate "Friday"    = "1 more day(s) until the weekend!"
motivate _           = "Relax! You don't need to work today!"
motivate :: String -> String
motivate day = case distanceToSunday day of
  6 -> "Have a nice week at work!"
  5 -> "You're one day closer to weekend!"
  n -> if n > 1
       then show (n - 1) ++ " more day(s) until the weekend!"
       else "Relax! You don't need to work today!"


area :: String -> Double -> Double
area "square" x = square x
area "circle" x = pi * square x
  where square x = x * x

where 절은 바로 위에 줄 까지만 영향을 미치기 때문에, 컴파일 에러가 발생합니다.

area :: String -> Double -> Double
area shape x = case shape of
  "square" -> square x
  "circle" -> pi * square x
  where square x = x*x

case문을 사용하면 위의 두줄이 한줄의 case문으로 묶이기 때문에, where절을 사용했을 때 에러가 발생하지 않습니다.


distanceToSunday :: String -> Int
distanceToSunday "Monday"    = 6
distanceToSunday "Tuesday"   = 5
distanceToSunday "Wednesday" = 4
distanceToSunday "Thursday"  = 3
distanceToSunday "Friday"    = 2
distanceToSunday "Saturday"  = 1
distanceToSunday "Sunday"    = 0
distanceToSunday :: String -> Int
distanceToSunday d = case d of
  "Monday"    -> 6
  "Tuesday"   -> 5
  "Wednesday" -> 4
  "Thursday"  -> 3
  "Friday"    -> 2
  "Saturday"  -> 1
  "Sunday"    -> 0

반복이 너무 많을 때 반복을 줄이기 위해 case문을 사용할 수 있습니다.



Recap: Pattern Maching

case number of 0 -> "zero"
               1 -> "one"
               _ -> "not zero or one"
-- getElement (Just i) gets the ith element (counting from zero) of a list, getElement Nothing gets the last element
getElement :: Maybe Int -> [a] -> a
getElement (Just i) xs = xs !! i
getElement Nothing xs = last xs
direction :: Either Int Int -> String
direction (Left i) = "you should go left " ++ show i ++ " meters!"
direction (Right i) = "you should go right " ++ show i ++ " meters!"


함수형 프로그래밍

고차원 함수

applyTo1 :: (Int -> Int) -> Int
applyTo1 f = f 1

addThree :: Int -> Int
addThree x = x + 3

applyTo1의 첫번째 인자는 Int형 인자를 받아 Int형 값을 반환하는 함수가 들어갑니다.
그래서 applyTo1의 첫번째 인자로 addThree를 줄 수 있습니다.

applyTo1 addThree
  ==> addThree 1
  ==> 1 + 3
  ==> 4
*Main> applyTo1 addThree 
4

고차원 함수 + 다형성

doTwice :: (a -> a) -> a -> a
doTwice f x = f (f x)

doTwice addThree 3
doTwice :: (Int -> Int) -> Int -> Int
doTwice addThree 3
	==> addThree (addThree 3)
    ==> addThree 6
    ==> 9
*Main> doTwice addThree 3
9

makeCool :: String -> String
makeCool str = "WOW" ++ str ++ "!"
doTwice makeCool "Haskell"
(String -> String) -> String -> String
doTwice makeCool "Haskell"
	==> makeCool (MakeCool "Haskell")
    ==> makeCool "WOWHaskell!"
    ==> WOWWOWHaskell!!
*Main> doTwice makeCool "Haskell"
"WOWWOWHaskell!!"


리스트를 이용한 함수형 프로그래밍

map :: (a -> b) -> [a] -> [b]

리스트의 모든 원소에 해당 함수를 적용합니다.

*Main> map addThree [1..3]
[4,5,6]

filter :: (a -> Bool) -> [a] -> [a]

주어진 리스트에서 조건을 만족하는 것만 끄집어냅니다.

positive :: Int -> Bool
positive x = x > 0
*Main> filter positive [0,1,-1,3,-3]
[1,3]

onlyPositive xs = filter positive xs
mapBooleans f = map f [False,True]
filter :: (a -> Bool) -> [a] -> [a]
filter positive xs
positive :: Int -> Bool
a => Int

onlyPositive :: [Int] -> [Int]
*Main> :t onlyPositive 
onlyPositive :: [Int] -> [Int]
*Main> :t mapBooleans 
mapBooleans :: (Bool -> b) -> [b]

wrapJust xs = map Just xs
*Main> wrapJust [1..5]
[Just 1,Just 2,Just 3,Just 4,Just 5]

-- a predicate that checks if a string is a palindrome
palindrome :: String -> Bool
palindrome str = str == reverse str

-- palindromes n takes all numbers from 1 to n, converts them to strings using show, and keeps only palindromes
palindromes :: Int -> [String]
palindromes n = filter palindrome (map show [1..n])
*Main> palindrome "1331"
True
*Main> palindromes 150
["1","2","3","4","5","6","7","8","9","11","22","33","44","55","66","77","88","99","101","111","121","131","141"]

substringsOfLength :: Int -> String -> [String]
substringsOfLength n string = map shorten (tails string)
  where shorten s = take n s
*Main> substringsOfLength 3 "hello"
*Main> ["hel","ell","llo","lo","o",""]


부분 호출

부분적인 인자만 가지고 함수를 호출하는 것입니다.


Prelude> add a b = a + b
Prelude> map (add 3) [1,2,3]
[4,5,6]

부분 호출을 사용하지 않으면?

addThree = add 3
map addThree [1,2,3]

addThree라는 함수를 만들어야 합니다.


between :: Integer -> Integer -> Integer -> Bool
between lo high x = x < high && x > lo
*Main> between 3 7 5
True
*Main> between 3 6 8
False
*Main> (between 1 5) 2
True
*Main> let f = between 1 5 in f 2
True
*Main> map (between 1 3) [1,2,3]
[False,True,False]
*Main> :t between 
between :: Integer -> Integer -> Integer -> Bool
*Main> :t between 1
between 1 :: Integer -> Integer -> Bool
*Main> :t between 1 2
between 1 2 :: Integer -> Bool
*Main> :t between 1 2 3
between 1 2 3 :: Bool

between 1 2 3
= (between 1) 2 3
= ((between 1) 2) 3

*Main> map (drop 1) ["Hello", "World!"]
["ello","orld!"]
*Main> map (*3) [1..5]
[3,6,9,12,15]
*Main> map (/2) [1..5]
[0.5,1.0,1.5,2.0,2.5]


전위와 중위 표기법

*Main> (+) 1 5
6

중위 표기법으로 쓰던 연산자를 전위 표기법으로 쓰고 싶으면 ()로 묶어줘야 합니다.


Prelude> zipWith + [1..3] [4..6]

<interactive>:1:11: error:

중위 표기 연산자인 +를 괄호로 묶어 주지 않아서 오류가 발생했습니다.

Prelude> zipWith (+) [1..3] [4..6]
[5,7,9]

Prelude> (+1) `map` [1,2,3]
[2,3,4]

원래 전위 표기법로 사용해야 하는 함수를 중위 표기법으로 사용하고 싶으면 ` `로 묶어 줘야 합니다.



람다

let big x = x > 7 in filter big [1,10,100]
Prelude> filter (\x -> x > 7) [1,10,100]
[10,100]

람다식을 사용하는게 훨씬 간결합니다.


Prelude> filter (\x -> reverse x == x) ["ABCD","ABBA","AAA"]
["ABBA","AAA"]


. 과 $ 연산자

(f.g) x ==> f (g x)

. 연산자는 두개의 함수를 조합하는 연산자입니다.


double x = 2 * x
quadruple = double.double
*Main> quadruple 4
16

*Main> f = quadruple . (+1)
*Main> g = (+1) . quadruple 
*Main> f 5
24
*Main> g 6
25
*Main> third = head . tail . tail
*Main> third [1..5]
3

*Main> notEmpty x = not (null x)
*Main> filter notEmpty [[1,2,3],[],[4]]
[[1,2,3],[4]]
*Main> filter (not . null) [[1,2,3],[],[4]]
[[1,2,3],[4]]

f $ x ==> f x

괄호 붙이기 귀찮을 때 $를 사용합니다.

*Main> head (reverse "abcd")
'd'
*Main> head $ reverse "abcd"
'd'

*Main> reverse (map head (map reverse (["Haskell","pro"] ++ ["dodo","lyric"])))
"cool"
*Main> (reverse . map head . map reverse) (["Haskell","pro"] ++ ["dodo","lyric"])
"cool"
*Main> reverse . map head . map reverse $ ["Haskell","pro"] ++ ["dodo","lyric"]
"cool"


whatFollows 함수 다시 작성

substringsOfLength :: Int -> String -> [String]
substringsOfLength n string = map shorten (tails string)
  where shorten s = take n s

whatFollows :: Char -> Int -> String -> [String]
whatFollows c k string = map tail (filter match (substringsOfLength (k+1) string))
  where match sub = take 1 sub == [c]

1. substringsOfLength 함수 사용 X

whatFollows c k string = map tail (filter match (map shorten (tails string)))
  where shorten s = take (k+1) s
        match sub = take 1 sub == [c]

2. 부분 호출

whatFollows c k string = map tail (filter match (map (take (k+1)) (tails string)))
  where match sub = take 1 sub == [c]

3. ., $ 연산자 사용

whatFollows c k string = map tail . filter match . map (take (k+1)) $ tails string
  where match sub = take 1 sub == [c]

4. 람다식 사용

whatFollows c k string = map tail . filter (\sub -> take 1 sub == [c]) . map (take (k+1)) $ tails string

5. string 없애기

whatFollows c k = map tail . filter (\sub -> take 1 sub == [c]) . map (take (k+1)) . tails

6. 람다를 연산자 섹션으로 변경

    \sub -> take 1 sub == [c]
=== \sub -> (==[c]) (take 1 sub)
=== \sub -> (==[c]) ((take 1) sub)
=== \sub -> ((==[c]) . (take 1)) sub
=== ((==[c]) . (take 1))
=== ((==[c]) . take 1)
whatFollows c k = map tail . filter ((==[c]) . take 1) . map (take (k+1)) . tails


리스트를 다루는 함수형 프로그래밍 예제

Prelude> takeWhile even [2,4,1,2,3]
[2,4]
Prelude> dropWhile even [2,4,1,2,3]
[1,2,3]

takeWhile 함수는 Bool값이 True이면 뽑아라
dropWhile 함수는 Bool값이 True이면 버려라


Prelude> elem 3 [1..3]
True

elem 함수는 첫번째로 주어진 인자가 두번째로 주어진 리스트에 포함되어 있나?


findSubstring :: String -> String -> String
findSubstring chars = takeWhile (\x -> elem x chars)
                      . dropWhile (\x -> not $ elem x chars)

findSubstring 함수는 두번째로 오는 문자열에서 첫번째로 오는 문자가 포함된 가장 길고 앞에 오는 서브 스트링을 반환

*Main> findSubstring "a" "bbaabaaaab"
"aa"
*Main> findSubstring "abcd" "xxxyyyzabaaxxabcd"
"abaa"

*Main> zipWith (++) ["John","Mary"] ["Smith","Cooper"]
["JohnSmith","MaryCooper"]
*Main> zipWith take [4,3] ["Hello","Warden"]
["Hell","War"]

zipWith 함수는 리스트의 같은 인덱스 원소끼리 합칩니다.


*Main> id 3
3
*Main> map id [1..3]
[1,2,3]

id 함수는 인자를 그대로 반환합니다.

*Main> filter id [True,False,True,True]
[True,True,True]
*Main> dropWhile id [True,True,False,True,False]
[False,True,False]

*Main> const 3 0
3
*Main> map (const 5) [1..4]
[5,5,5,5]
*Main> filter (const True) [1..5]
[1,2,3,4,5]

const 함수는 뒤에 오는 인자를 까먹고 앞에 오는 인자만 리턴합니다.



리스트와 재귀

: 연산자는 앞에 원소를 뒤에 리스트에 넣습니다.

*Main> 1 : []
[1]
*Main> 1 : [2,3]
[1,2,3]
*Main> tail (1 : [2,3])
[2,3]
*Main> head (1 : [2,3])
1

리스트를 만드는 연산자에는 :[]가 있습니다.

*Main> 1 : 2 : 3 : []
[1,2,3]
[x,y,z]

=> x:y:z:[]
=> x:(y:(z:[]))


리스트 만들기

descend 0 = []
descend n = n : descend(n-1)
*Main> descend 4
[4,3,2,1]
*Main> descend 10
[10,9,8,7,6,5,4,3,2,1]

descend 함수를 재귀적으로 호출하여 주어진 숫자부터 1까지를 원소로 갖는 리스트 생성


_iterate f 0 x = [x]
_iterate f n x = x : _iterate f (n-1) (f x)
*Main> :type iterate 
iterate :: (a -> a) -> a -> [a]
*Main> _iterate (*2) 4 3
[3,6,12,24,48]

_iterate (*2) 4 3
==> 3 : _iterate (*2) 3 6
==> 3 : 6 : _iterate (*2) 2 12
==> 3 : 6 : 12 : _iterate (*2) 1 24
==> 3 : 6 : 12 : 24 : _iterate (*2) 0 48
==> 3 : 6 : 12 : 24 : [48]
==> [3, 6, 12, 24, 48]

*Main> xs = "terve"
*Main> _iterate tail (length xs) xs
["terve","erve","rve","ve","e",""]

split :: Char -> String -> [String]
split c [] = []
split c xs = start : split c (drop 1 rest)
  where start = takeWhile (/=c) xs
        rest = dropWhile (/=c) xs

fooxzoo 를 기준으로

start : foo
rest : xzoo
split c (drop 1 rest) : zoo
foo : zoo

*Main> split 'x' "fooxxbarxquux"
["foo","","bar","quu"]
*Main> split 'x' "fooxzoo"
["foo","zoo"]


리스트 패턴 매칭

myhead :: [Int] -> Int
myhead [] = -1
myhead (first:rest) = first

mytail :: [Int] -> [Int]
mytail [] = []
mytail (first:rest) = rest

myhead [1,2,3,4,5]
==> myhead (1:(2:(3:(4:(5:[])))))
==> 1
*Main> myhead [1,2,3,4,5]
1

tail [1,2,3,4,5]
tail (1:(2:(3:(4:(5:[])))))
==> (2:(3:(4:(5:[]))))
==> [2,3,4,5]
*Main> tail [1,2,3,4,5]
[2,3,4,5]

sumFirstTwo :: [Integer] -> Integer
-- this equation gets used for lists of length at least two
sumFirstTwo (a:b:_) = a+b
-- this equation gets used for all other lists (i.e. lists of length 0 or 1)
sumFirstTwo _       = 0
(a:b:_) ==> (a:(b:_))

*Main> sumFirstTwo [1]
0
*Main> sumFirstTwo [1,2]
3
*Main> sumFirstTwo [1,2,4]
3

describeList :: [Int] -> String
describeList []         = "an empty list"
describeList (x:[])     = "a list with one element"
describeList (x:y:[])   = "a list with two elements"
describeList (x:y:z:xs) = "a list with at least three elements"
*Main> describeList [1,3]
"a list with two elements"
*Main> describeList [1,2,3,4,5]
"a list with at least three elements"

[1,3] ==> (x:y:[]) ==> 원소가 두개인 리스트
[1,2,3,4,5] ==> (1:2:3:[4,5]) ==> 원소가 3개 이상인 리스트



리스트 사용하기

sumNumbers :: [Int] -> Int
sumNumbers [] = 0
sumNumbers (x:xs) = x + sumNumbers xs
*Main> sumNumbers [1,2,3]
6
*Main> sumNumbers [1,2,3,4]
10

리스트 원소들의 합을 구하는 함수


myMaximum :: [Int] -> Int
myMaximum [] = 0       -- actually this should be some sort of error...
myMaximum (x:xs) = go x xs
  where go biggest [] = biggest
        go biggest (x:xs) = go (max biggest x) xs

리스트 원소중 가장 큰 원소를 찾는 함수

myMaximum [1,5,3,12]

==> go 1 [5,3,12]
==> go 5 [3,12]
==> go 5 [12]
==> go 12 []
==> 12
*Main> myMaximum [1,5,3,12]
12

countNothings :: [Maybe a] -> Int
countNothings [] = 0
countNothings (Nothing : xs) = 1 + countNothings xs
countNothings (Just _  : xs) = countNothings xs

Nothing이 몇개인지 세는 함수

*Main> countNothings [Nothing, Just 1, Just 3]
1
*Main> countNothings [Nothing, Just 1, Nothing]
2


리스트 만들고 사용하기

doubleList :: [Int] -> [Int]
doubleList [] = []
doubleList (x:xs) = 2*x : doubleList xs

doubleList [1,2,3]
=== doubleList (1:(2:(3:[])))
==> 2*1 : doubleList (2:(3:[]))
==> 2*1 : (2*2 : doubleList (3:[]))
==> 2*1 : (2*2 : (2*3 : doubleList []))
==> 2*1 : (2*2 : (2*3 : []))
=== [2*1, 2*2, 2*3]
==> [2,4,6]
*Main> doubleList [1,2,3]
[2,4,6]

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

비어있는 리스트가 들어오면 그대로 리턴
(함수 fx에 적용) : (xsmap 함수 재귀호출)
==> 리스트의 모든 원소에 f함수가 적용됩니다.


filter :: (a -> Bool) -> [a] -> [a]
filter _pred []    = []
filter pred (x:xs)
  | pred x         = x : filter pred xs
  | otherwise      = filter pred xs

xpred 함수를 적용한 결과가 True이면 (xs) : (xsfilter 함수 재귀호출)
아니면 xsfilter 함수 재귀호출



Tail Recursion and Lists

Tail Recursion : 재귀함수의 위치가 함수의 맨 마지막에 위치한 경우, 돌아왔을 때 크게 할일이 없음


-- Not tail recursive!
doubleList :: [Int] -> [Int]
doubleList [] = []
doubleList (x:xs) = 2*x : doubleList xs
-- Tail recursive version
doubleList :: [Int] -> [Int]
doubleList xs = go [] xs
    where go result [] = result
          go result (x:xs) = go (result++[2*x]) xs

Tail recursion vs Not Tail recursion

Tail recursion이 빠를 때도 있고 Not Tail recursion이 빠를수도 있다.

그러면 어떤걸 써야 하나?
그냥 코드를 읽기 편한쪽으로 짜면된다.



리스트 제시법

Mapping:

*Main> [ 2*i | i <- [1,2,3] ]
[2,4,6]

Filtering:

*Main> [ i | i <- [1..7], even i ]
[2,4,6]

Mapping + Filtering:

[ f x | x <- lis, p x ]
==> map f (filter p lis)

*Main> [ first ++ " " ++ last | first <- ["John", "Mary"], last <- ["Smith","Cooper"] ]
["John Smith","John Cooper","Mary Smith","Mary Cooper"]
first <- ["John", "Mary"]
	last <- ["Smith", "Cooper"]
    	do something with (first, last)

리스트 제시법을 map으로 바꿔보면

concat (map (\x -> map (\y -> first ++ " " ++ last) ["Smith", "Cooper"]) ["John", "Marry"])

concat :: [ [a] ] -> [a]
concat [] = xs
concat (x:xs) = xs ++ concat xss

리스트 안의 리스트를 빼내는 함수
Ex) [ ["John"], ["Smith"] ] ==> ["John", "Smith"]


*Main> [ reversed | word <- ["this","is","a","string"], let reversed = reverse word ]
["siht","si","a","gnirts"]

식이 복잡해지면 로컬 변수를 만들 수 있는데, 앞에 let이라는 키워드를 사용해야 합니다.


firstLetters string = [ char | (char:_) <- words string ]
*Main> words "Hello World!"
["Hello","World!"]

words string에 의해서 char는 첫번째 단어의 첫번째 문자가 되고 _는 두번째 단어의 첫번째 문자가 됩니다.

*Main> firstLetters "Hello World!"
"HW"


커스텀 연산자 만들기

!#$%&*+./<=>?@\^|-~ 연산자를 이용해서 커스텀 연산자를 만들 수 있습니다.
연산자는 ( )로 감싸줘야 합니다.
만들어진 연산자는 함수입니다.


(<+>) :: [Int] -> [Int] -> [Int]
xs <+> ys = zipWith (+) xs ys
*Main> [1,2,3] <+> [4,5,6]
[5,7,9]

(+++) :: String -> String -> String
a +++ b = a ++ " " ++ b
*Main> "Hello" +++ "World"
"Hello World"


빈자리의 타입 알아내기

타입을 추론하고 싶은 곳에 __name을 넣어주면 컴파일러가 타입을 추론해줍니다.

ghci> filter _hole [True,False]
<interactive>:3:8: error:
    • Found hole: _hole :: Bool -> Bool
      Or perhaps ‘_hole’ is mis-spelled, or not in scope
    • In the first argument of ‘filter’, namely ‘_hole’
      In the expression: filter _hole [True, False]
      In an equation for ‘it’: it = filter _hole [True, False]
    • Relevant bindings include
        it :: [Bool] (bound at <interactive>:3:1)
      Valid hole fits include
        not :: Bool -> Bool
          (imported from ‘Prelude’ at main.hs:1:1
           (and originally defined in ‘GHC.Classes’))
        id :: forall a. a -> a
          with id @Bool
          (imported from ‘Prelude’ at main.hs:1:1
           (and originally defined in ‘GHC.Base’))
        pred :: forall a. Enum a => a -> a
          with pred @Bool
          (imported from ‘Prelude’ at main.hs:1:1
           (and originally defined in ‘GHC.Enum’))
        succ :: forall a. Enum a => a -> a
          with succ @Bool
          (imported from ‘Prelude’ at main.hs:1:1
           (and originally defined in ‘GHC.Enum’))

keepElements :: [a] -> [Bool] -> [a]
keepElements xs bs = _doIt (zip xs bs)
main.hs:2:22: error:
    • Found hole: _doIt :: [(a, Bool)] -> [a]
      Where: ‘a’ is a rigid type variable bound by
               the type signature for:
                 keepElements :: forall a. [a] -> [Bool] -> [a]
               at main.hs:1:1-36
      Or perhaps ‘_doIt’ is mis-spelled, or not in scope
    • In the expression: _doIt (zip xs bs)
      In an equation for ‘keepElements’:
          keepElements xs bs = _doIt (zip xs bs)
    • Relevant bindings include
        bs :: [Bool] (bound at main.hs:2:17)
        xs :: [a] (bound at main.hs:2:14)
        keepElements :: [a] -> [Bool] -> [a] (bound at main.hs:2:1)
      Valid hole fits include
        mempty :: forall a. Monoid a => a
          with mempty @([(a, Bool)] -> [a])
          (imported from ‘Prelude’ at main.hs:1:1
           (and originally defined in ‘GHC.Base’))


튜플

서로 다른 타입들의 값을 하나로 묶을 수 있는 자료 구조

TypeExample
(String, String)("Hello", "Hello!")
(Int, Bool)(1, True)

fsd 함수 : 튜플에서 앞에 값을 리턴
snd 함수 : 튜플에서 뒤에 값을 리턴

fst :: (a, b) -> a
snd :: (a, b) -> b
ghci> fst ("Hello", "World")
"Hello"
ghci> snd ("Hello", "World")
"World"

zip 함수 : 두 리스트에서 같은 인덱스의 원소끼리 묶는 함수
unzip 함수 : zip한 값을 다시 푸는 함수

ghci> zip [1,2,3] [True,False,True]
[(1,True),(2,False),(3,True)]
ghci> unzip it
([1,2,3],[True,False,True])

partition 함수 : 테스트 함수와 리스트를 받아서 테스트를 통과한 값의 리스트와 통과하지 못한 값의 리스트를 반환합니다.

ghci> import Data.List
ghci> partition (>0) [-1,1,-4,3,2,0]
([1,3,2],[-1,-4,0])

swap 함수 : 튜플의 원소를 서로 바꿉니다.

swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
ghci> swap ("Hello", False)
(False,"Hello")

sumIf 함수 : 튜플 쌍에서 첫번째 원소가 True인 쌍의 숫자만 다 더하겠다.

sumIf :: [(Bool,Int)] -> Int
sumIf [] = 0
sumIf ((True,x):xs) = x + sumIf xs
sumIf ((False,_):xs) = sumIf xs
ghci> sumIf [(True,1),(False,10),(True,100)]
101


Folding

sumNumbers :: [Int] -> Int
sumNumbers [] = 0
sumNumbers (x:xs) = x + sumNumbers xs

myMaximum :: [Int] -> Int
myMaximum [] = 0
myMaximum (x:xs) = go x xs
    where go biggest [] = biggest
        go biggest (x:xs) = go (max biggest x) xs

countNothings :: [Maybe a] -> Int
countNothings [] = 0
countNothings (Nothing : xs) = 1 + countNothings xs
countNothings (Just _  : xs) = countNothings xs

sumNumbers 함수 : 리스트 원소의 합을 구하는 함수
myMaximum 함수 : 리스트의 최대값을 구하는 함수
countNothings 함수 : Nothing의 갯수를 세는 함수


foldr 함수를 이용하면 위와 같이 리스트를 다루는 수많은 함수들을 재작성할 수 있습니다.
foldr 함수는 Foldable data type(리스트 같은 타입)을 오른쪽 끝에서 부터 차근 차근 계산해 나가는 함수입니다.

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f y []     = y
foldr f y (x:xs) = f x (foldr f y xs)

foldrsumNumbers 함수 구현하기

foldr (+) 0 [1,2,3] ==> foldr (+) 0 (1:2:3:[])
                    ==> 1 + (foldr (+) 0 (2:3:[]))
                    ==> 1 + (2 + (foldr (+) 0 (3:[])))
                    ==> 1 + (2 + (3 + (foldr (+) 0 [])))
                    ==> 1 + (2 + (3 + 0))

foldrmap 함수 구현하기

map g xs = foldr helper [] xs
  where helper y ys = g y : ys


타입 클래스

타입들의 집합
이 타임들에 사용할 수 있는 동일한 이름의 함수가 있음

ghci> :type (+)
(+) :: Num a => a -> a -> a

a안에 Num이라는 타입 클래스에 속한 타입만 들어갈 수 있다.

ghci> :type (==)
(==) :: Eq a => a -> a -> Bool

a안에 Eq라는 타입 클래스에 속한 타입만 들어갈 수 있다.


다형성(Polymorphism)

Ex) a -> a
a에 아무 조건 없는 다형성
parametric polymorphism

Ex) Num a => a -> a -> a
a에 올 수 있는 타입은 Num 타입 클래스에 속함
ad-hoc polymorphism (overloading)



타입 제약

타입 클래스 함수를 사용할 때 구체적인 타입을 지정해줘야 할 수 도 있습니다.

addTrue :: Bool -> Bool
addTrue b = b + True
main.hs:2:15: error:
    • No instance for (Num Bool) arising from a use of ‘+’
    • In the expression: b + True
      In an equation for ‘addTrue’: addTrue b = b + True
ghci> :type (+)
(+) :: Num a => a -> a -> a

+ 연산을 하려면 기본적으로 Num 타입 클래스에 속해야 하는데, Bool는 이에 속하지 않아서 오류가 발생했습니다.


f :: (a -> a) -> a -> Bool
f g x = x == g x
main.hs:2:11: error:
    • No instance for (Eq a) arising from a use of ‘==’
      Possible fix:
        add (Eq a) to the context of
          the type signature for:
            f :: forall a. (a -> a) -> a -> Bool
    • In the expression: x == g x
      In an equation for ‘f’: f g x = x == g x

f 함수에서 인자의 타입을 a로 주었는데, == 연산을 진행하기 위해서는 타입이 Eq 클래스에 속해야 합니다.
그래서 오류가 발생하지 않게 하려면 인자의 타입을 더 구체적으로 지정해주어야 합니다.

a 타입이 Eq 클래스에 속한다고 선언

f :: Eq a => (a -> a) -> a -> Bool
f g x = x == g x
ghci> :reload
[1 of 1] Compiling Main             ( main.hs, interpreted )
Ok, one module loaded.

만약 상세하게 타입을 지정해주는게 어렵다면?

ghci> f g x = x == g x
ghci> :type f
f :: Eq t => (t -> t) -> t -> Bool

함수를 정의하고 해당 함수의 타입을 출력해보면 됩니다.



Eq 클래스

==, /= 연산자를 위한 타입을 제공

ghci> 1 == 2
False
ghci> 1.0 /= 2.0
True
ghci> "foo" == "bar"
False
ghci> [1,2] == [1,2]
True
ghci> [[1,2],[3,4]] /= [[1,2],[]]
True

Int, Double, String, List 모두 Eq 클래스에 속합니다.


ghci> (/x -> x+1) == (\x -> x+2)

<interactive>:50:5: error: parse error on input ‘->’
ghci> (\x -> x+1) == (\x -> x+2)

<interactive>:51:13: error:
    • No instance for (Eq (Integer -> Integer))
        arising from a use of ‘==’
        (maybe you haven't applied a function to enough arguments?)
    • In the expression: (\ x -> x + 1) == (\ x -> x + 2)
      In an equation for ‘it’: it = (\ x -> x + 1) == (\ x -> x + 2)

람다 함수는 Eq 클래스에 포함되지 않습니다.


nub 함수 : 리스트에서 중복을 제거해줍니다.

ghci> import Data.List
ghci> :type nub
nub :: Eq a => [a] -> [a]
ghci> nub [3,5,3,1,1]
[3,5,1]


Ord

순서를 지을 수 있는 값들의 타입들을 포함하고 있습니다.

compare :: Ord a => a -> a -> Ordering
(<) :: Ord a => a -> a -> Bool
(>) :: Ord a => a -> a -> Bool
(>=) :: Ord a => a -> a -> Bool
(<=) :: Ord a => a -> a -> Bool
max :: Ord a => a -> a -> a
min :: Ord a => a -> a -> a

compare 함수 : 비교 결과에 따라 LT(less than), EQ(Equal), GT(greater than)을 반환합니다.


ghci> compare 1 1
EQ
ghci> compare 1 3
LT
ghci> compare 4 2
GT
ghci> compare 'a' 'z'
LT

ghci> max 10 5
10
ghci> max "abc" "wb"
"wb"
ghci> min 5 3
3
ghci> min [1,2,3] [4,5,6]
[1,2,3]

import Data.List

-- from the module Data.Ord
-- compares two values "through" the function f
comparing :: (Ord a) => (b -> a) -> b -> b -> Ordering
comparing f x y = compare (f x) (f y)

-- from the module Data.List
-- sorts a list using the given comparison function
-- sortBy :: (a -> a -> Ordering) -> [a] -> [a]


-- sorts lists by their length
sortByLength :: [[a]] -> [[a]]
sortByLength = sortBy (comparing length)
ghci> sortByLength [[1,2,3],[4,5],[4,5,6,7]]
[[4,5],[1,2,3],[4,5,6,7]]

sortByLength 함수 : 리스트 내부의 리스트들을 길이에 따라 정렬해줍니다.



Num, Integral, Fractional, Floating

Num 클래스 : 산술 연산을 포함하는 클래스

(+) :: Num a => a -> a -> a
(-) :: Num a => a -> a -> a
(*) :: Num a => a -> a -> a
negate :: Num a => a -> a    -- 0-x
abs :: Num a => a -> a       -- absolute value
signum :: Num a => a -> a    -- -1 for negative values, 0 for 0, +1 for positive values
fromInteger :: Num a => Integer -> a

ghci> :type 123
123 :: Num a => a
ghci> 123 :: Int
123
ghci> 123 :: Double
123.0

123의 타입을 출력해보면 Num으로 나오는데, 그 이유는 123Num 클래스에 있는 Int, Double, Float 등 여러 타입으로 쓰일 수 있기 때문입니다.


Integral 클래스 : IntInteger 같은 숫자들을 표현하는 타입들

div :: Integral a => a -> a -> a
mod :: Integral a => a -> a -> a

Integral에 속하는 타입들은 Num에도 속합니다.


Fractional 클래스 : / 연산을 할 수 있는 타입

(/) :: Fractional a => a -> a -> a

Floating 클래스 : 실수를 다루는 타입들

sqrt :: Floating a => a -> a
sin :: Floating a => a -> a

Floating 타입에 속하는 타입들은 Fractional에도 속합니다.



Read와 show

show :: Show a => a -> String
read :: Read a => String -> a

show : 다른 타입의 값을 String 타입으로 변환합니다.
read : 문자열 타입의 값을 다른 타입으로 변환합니다.

ghci> show 5
"5"
ghci> read "5" :: Int
5
ghci> read "5" :: Double
5.0


Foldable

foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

Foldable 클래스 : fold할 수 있는 타입을 모아 놓은 것
Ex) list, Maybe


ghci> :type length
length :: Foldable t => t a -> Int
ghci> length [1,2,3]
3
ghci> length (Just 1)
1
ghci> length Nothing
0

foldr (+) 1 Nothing   ==> 1
foldr (+) 1 (Just 3)  ==> 4
length Nothing        ==> 0
length (Just 'a')     ==> 1

타입 클래스에서 공통적으로 사용할 수 있는 이름이 궁금하면?

ghci> :info Num
type Num :: * -> Constraint
class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
  {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
        -- Defined in ‘GHC.Num’
instance Num Word -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’

:info [Type Class]로 검색 가능



Data.map

key-value를 검색하는 작업의 경우 리스트 보다 맵을 사용하는게 더 효율적입니다.

import qualified Data.Map as Map

Map을 import 합니다.

ghci> values = Map.fromList [("z",3),("w",4)]
ghci> :type values
values :: Num a => Map.Map String a

Map.fromList : 리스트를 받아서 맵을 만들어줍니다.

ghci> Map.insert "x" 7 values
fromList [("w",4),("x",7),("z",3)]

Map.insert : 맵에 새로운 key-value를 삽입한 결과를 반환합니다.
원래 values의 값이 변하지는 않습니다. (불변성)

ghci> values
fromList [("w",4),("z",3)]
ghci> Map.lookup "z" values
Just 3
ghci> Map.lookup "banana" values
Nothing

Map.lookup : 주어진 key에 해당하는 value값을 찾아 리턴합니다.

ghci> values = Map.empty
ghci> values
fromList []

Map.empty : 비어있는 맵을 만듭니다.


위 함수들의 타입을 봐보면

ghci> :type Map.fromList
Map.fromList :: Ord k => [(k, a)] -> Map.Map k a

(key, value)로 이루어진 리스트를 받아서 맵을 만듭니다.

ghci> :type Map.lookup
Map.lookup :: Ord k => k -> Map.Map k a -> Maybe a

keyMap을 받는데 여기서 value값을 Maybe 타입으로 리턴합니다.

ghci> :type Map.insert
Map.insert :: Ord k => k -> a -> Map.Map k a -> Map.Map k a

key, value, Map을 받아서 해당 key-value가 포함된 맵을 리턴합니다.

ghci> :type Map.empty
Map.empty :: Map.Map k a

비어있는 맵을 리턴합니다.


withdraw 함수 : 은행 계좌에서 돈을 인출하는 함수

import qualified Data.Map as Map

withdraw :: String -> Int -> Map.Map String Int -> Map.Map String Int
withdraw account amount bank =
  case Map.lookup account bank of
    Nothing  -> bank                                   -- account not found, no change
    Just sum -> Map.insert account (sum-amount) bank   -- set new balance

ghci> bank = Map.fromList [("Bob",100),("Mike",50)]
ghci> withdraw "Bob" 80 bank
fromList [("Bob",20),("Mike",50)]
ghci> bank
fromList [("Bob",100),("Mike",50)]

Bob의 계좌에서 80을 인출합니다.
함수 실행 후 원래 bank 맵의 값은 변하지 않습니다.

fromList [("Bob",20),("Mike",50)]
ghci> withdraw "Bozo" 1000 bank1
fromList [("Bob",20),("Mike",50)]

없는 사용자로 인출을 하면 맵의 값이 변하지 않습니다.

withdraw 함수의 내부적인 동작

ghci> Map.lookup "Bob" bank
Just 100

먼저 해당 사용자가 존재하는지 맵에 검색을 합니다.

ghci> Map.insert "Bob" (100-80) bank
fromList [("Bob",20),("Mike",50)]

그 후 해당 사용자의 value값에서 인자로 들어온 값을 뺍니다.



Data.Array

Array 타입 import

import Data.Array

ghci> :type array
array :: Ix i => (i, i) -> [(i, e)] -> Array i e

하스켈에서 배열의 인덱스에는 Int 타입 뿐만 아니라 Ix 클래스에 해당하는 다른 여러 타입들도 올 수 있습니다.


ghci> arrInt = listArray (7,11) ["seven", "eight", "nine", "ten", "eleven"]
ghci> arrInt
array (7,11) [(7,"seven"),(8,"eight"),(9,"nine"),(10,"ten"),(11,"eleven")]

listArray : 인덱스와 리스트를 받아 배열을 만들어줍니다.


ghci> arrInt ! 7
"seven"
ghci> arrInt ! 8
"eight"
ghci> arrInt ! 20
"*** Exception: Ix{Integer}.index: Index (20) out of range ((7,11))

! : 인덱스에 해당하는 값을 리턴해줍니다.


ghci> array (7,11) [(11,"eleven"),(10,"ten"),(9,"nine"),(8,"eight"),(7,"seven")]
array (7,11) [(7,"seven"),(8,"eight"),(9,"nine"),(10,"ten"),(11,"eleven")]

array : 정렬되지 않은 리스트를 정렬 시켜서 배열로 만들어줍니다.


ghci> arrInt
array (7,11) [(7,"seven"),(8,"eight"),(9,"nine"),(10,"ten"),(11,"eleven")]
ghci> arr = arrInt // [(8,"EIGHT")]
ghci> arr
array (7,11) [(7,"seven"),(8,"EIGHT"),(9,"nine"),(10,"ten"),(11,"eleven")]

// : 해당하는 인덱스의 값을 업데이트한 배열을 리턴합니다.

0개의 댓글