리스트는 하스켈의 복합자료형입니다.
C언어나 자바를 학습한 프로그래머라면
배열(Array)의 개념을 이해하고
있을 것 입니다.
C언어 자료구조 - 배열의 구조
C의 대표적 자료 구조인 배열에 대하여 알아본다. 배열은 대부분의 언어에 기본으로 장착되어 있는 자료형태이다. 컴퓨터는 모든 데이터를 0과1의 이진 데이터의 형태로 저장한다. 이진법과 C의
digiconfactory.tistory.com
같은 배열이라도 프로그래밍 언어에
따라 여러가지 변형을 합니다.
자바에서는 콜렉션 프레임워크에서
배열객체(ArrayList) 클래스로
두 자료구조의 장점을 합쳐놓은
자료형을 제공하기도 합니다.
그런데 너무 복잡해보입니다.
리스트도 따지고 보면 배열의
일종이라고 볼 수 있습니다.
엄밀히 말하면 배열과 리스트는
다른 것이지만 현대 언어에서
많은 경우 배열 대신 리스트를
사용하는 것은 배열이 진화한
형태가 리스트라고 볼 수 있기 때문이죠.
우리가 새로운 프로그래밍을 배울때
주어진 응용 프로그래밍을
사용만 한다면 자료 구조에 대해서
완전하게 그 내부구조까지는
알지 못한채 주어진 자료형을
사용해야 하는데 그러다 보니
매번 새로운 언어에 적응하는데
시간이 더 걸리게 됩니다.
하스켈에서의 리스트는 자바나
파이썬 등 다른 언어와는 또 다른
특성을 갖기 때문에 문법적으로
차이가 있습니다만 직관적으로
더 쉬울 수 있습니다.
직관적이라는 것은 몇번
코드를 쳐보고 실행해보면
금방 적응할 수 있다는 말입니다.
리스트의 기초에 대해서 알아 보겠습니다.
GHCi를 사용하겠습니다.
우선 리스트는 [ ] 로 만들 수 있습니다.
ghci> myList = [1,2,3,4,5]
ghci> myList
[1,2,3,4,5]
간단합니다.
주의할 점은 하스켈의 리스트는
한가지 자료형, 즉 동일한 자료구조만
저장이 가능합니다.
숫자형이면 숫자형,
문자형이면 문자형
문자열이면 문자열,
부동소수점 수면 부동소수점 수
교차해서 저장은 불가능합니다.
ghci> mixedList = ['a', 'b', 1, 2]
<interactive>:33:24: error:
? No instance for (Num Char) arising from the literal ‘1’
? In the expression: 1
In the expression: ['a', 'b', 1, 2]
In an equation for ‘mixedList’: mixedList = ['a', 'b', 1, ....]
위의 코드는 문자형과 숫자가
하나의 리스트에 저장이 되지
않음을 알려줍니다.
참고로 문자형과 문자열은 다른 것 입니다.
list1 = ['a', "Hello"] 라고 할당하려 해도
오류를 표시합니다.
하스켈이 봤을 땐 'a'는 type Char (문자형)이고
"Hello"는 [Char] 입니다.
하스켈에서 문자열은 리스트입니다.
"Hello"는 아래와 같은 의미입니다.
ghci> string1 = ['H','e','l','l','o']
ghci> string1
"Hello"
리스트가 문자열이란 것은
C언어 등에서 문자열을 다루어 봤다면
이해할 수 있는 부분입니다.
각 문자의 아스키코드(유니코드)를
리스트의 요소로 할당합니다.
한글도 입력이 가능한데
아래와 같이 유티코드를 출력함을
볼 수 있습니다.
ghci> myString2 = ["한글"]
ghci> myString2
["\54620\44544"]
파이썬 프로그래머라면 리스트에
온갖 자료형을 혼합하여 저장할 수
있다는 것을 알고 있을 겁니다.
하스켈에서는 그렇지 않습니다.
한가지 타입(type)의 자료형만
저장이 가능하다는 점을
반드시 기억하도록 합니다.
리스트는 하스켈에서 가장
빈번하게 사용되는 자료형입니다.
리스트의 연산자와 각종 기본 함수는
상당히 유용합니다.
파이썬도 버전업이 될 수록
상당히 프로그래머에게 편의적인
문법으로 변해가는데 하스켈도
이에 못지 않습니다.
리스트 연산의 기본인 연결입니다.
++ 연산자를 사용합니다.
*숫자형 리스트를 연결합니다.
ghci> list1 = [1,3,5,7,9]
ghci> list2 = [2,4,6,8,10]
ghci> list1 ++ list2
[1,3,5,7,9,2,4,6,8,10]
*문자형(Char) 리스트를 연결합니다.
다만 문자형끼리는 연산이 안됩니다.
't' ++ 'r' 이런 것은 안됩니다.
이것은 문자형(Char)이지 리스트가
아니기 때문입니다.
ghci> ['t','r'] ++ ['e', 'e']
"tree"
*문자열과 문자열을 연결 시켜서 하나의
문자열을 만들어 냅니다.
하나의 문자열 -> 하나의 리스트
ghci> string1 ++ string2
"helloHaskell"
ghci> hello = string1 ++ " " ++ string2
ghci> hello
"hello Haskell"
*문자열 자체를 담은 리스트도
연결이 가능합니다.
ghci> str1 = ["Hello"]
ghci> str2 = ["World!"]
ghci> str3 = str1 ++ [" "] ++ str2
ghci> str3
["Hello"," ","World!"]
++ 연산자는 시간이 많이 걸립니다.
하스켈 리스트의 알고리즘에서
어떤 리스트에 새로운 리스트를
추가하려면 기존 리스트를 모두
점검한 후 끝에다 새로운 리스트를
연결하도록 합니다.
리스트가 길어지면 성능의 문제가
될 수 있습니다.
이럴 때 리스트의 앞쪽으로 연결하는
: 연산자를 사용하는 이점이 있습니다.
ghci> 1:2:[3]
[1,2,3]
ghci> 1:2:[]
[1,2]
ghci> 1:2:3:[4,5,6,7]
[1,2,3,4,5,6,7]
ghci> 'H':"ello"
"Hello"
ghci> 'H':'e':"llo"
"Hello"
리스트의 요소에는 인덱스가
있습니다. 다른 언어들 처럼
최초의 인덱스는 0에서 시작합니다.
연산자는 !! 입니다.
ghci> "Haskell Learning" !! 0
'H'
ghci> "Haskell Learning" !! 1
'a'
인덱스 범위를 벗어나면 오류를
출력합니다.
리스트 안에 리스트가 들어있으면
리스트 중첩 상태입니다.
ghci> myList = [[1,2,3], [4,5,6], [7,8,9]]
ghci> myList
[[1,2,3],[4,5,6],[7,8,9]]
ghci> myList !! 0
[1,2,3]
ghci> myList !! 1
[4,5,6]
ghci> myList !! 2
[7,8,9]
리스트의 길이는 차이가 날 수 있지만
리스트안의 타입은 일치해야 합니다.(중요)
위에서 설명한 리스트 연산자 ++와 :도
중첩된 리스트에서 사용가능합니다.
ghci> myList = [[1,2,3], [4,5,6], [7,8.0]]
ghci> myList
[[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0]]
ghci> myList ++ [[1,3,5]]
[[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0],[1.0,3.0,5.0]]
ghci> [10,20]:myList
[[10.0,20.0],[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0]]
리스트는 비교연산할 수 있습니다.
첫번째 요소부터 비교해서 그 다음
리스트와 비교합니다.
약간 tricky 하므로 하스켈을
좀 가지고 놀면서 적응할
필요가 있습니다.
ghci> [3,2,1] > [2,1]
True
ghci> [2,1] > [2,1]
False
ghci> [2,1,5] > [2,1]
True
ghci> [2,0,5] > [2,1]
당연한 이야기지만 리스트에 대해서
기본으로 내장된 함수가 있습니다.
아래의 예제의 함수들의 이름은
자기 설명적입니다.
사용방식이 상당히 유저친화적이라서
좋습니다.
거의 엑셀함수를 사용하는 느낌입니다.
head tail last init 이름과 내용만 봐도
알 수 있습니다.
sum 이런 것은 엑셀 함수와 같네요.
이것만 봐도 하스켈이 요새
떠오르는 이유를 알 수 있을 것 같습니다.
ghci> head [15, 30, 45, 60, 75]
15
ghci> tail [15, 30, 45, 60, 75]
[30,45,60,75]
ghci> last [15, 30, 45, 60, 75]
75
ghci> init [15, 30, 45, 60, 75]
[15,30,45,60]
ghci> length [15, 30, 45, 60, 75]
5
ghci> null [15, 30, 45, 60, 75]
False
ghci> reverse [15, 30, 45, 60, 75]
[75,60,45,30,15]
ghci> take 2 [15, 30, 45, 60, 75]
[15,30]
ghci> drop 2 [15, 30, 45, 60, 75]
[45,60,75]
ghci> minimum [15, 30, 45, 60, 75]
15
ghci> maximum [15, 30, 45, 60, 75]
75
ghci> sum [15, 30, 45, 60, 75]
225
ghci> product [15, 30, 45, 60, 75]
91125000
ghci> 15 `elem` [15, 30, 45, 60, 75]
True
ghci> 7 `elem` [15, 30, 45, 60, 75]
False
원래 프로그래밍이란 이렇게
쉽고 직관적이어야 하지 않았나?
객체지향 프로그래밍이
보급된지도 20년이나 지났습니다.
요새는 뭐랄까 코드만 가지고
개발하는 경우는 거의 없죠.
날것의 코드를 짜는 능력 +
디자이너같은 도구와
통합개발환경을
사용하는 능력이 점점 더
중요해지고 있습니다.
프로그래밍 언어에 대해서는
C를 비롯하여 많이 알면 좋겠지만
그것이 항상 생산성 향상으로
이어지지는 않는 것 같습니다.
그래서 이렇게 모듈을 결합해서
만드는 하스켈의 스타일은
장점이 많은 것 같습니다.
다음은 레인지를 사용하는 방법에
대해서 알아보겠습니다.
리스트가 길어지면 일일히
쓰는게 귀찮아 지는데
Range 기능으로 빠르게
리스트 요소를 채울수 있습니다.
ghci> list1 = [1 .. 7]
ghci> list1
[1,2,3,4,5,6,7]
ghci> list2 = ['A' .. 'z']
ghci> list2
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz"
위의 코드에서 보면 처음 숫자와 마지막 숫자
사이의 범위의 리스트를 만들어 줍니다.
두번째 예시는 문자형 아스키코드를
순서대로 출력합니다.
아스키코드에 +1 연산을 하면서
긔것을 다 합쳐서 문자열 하나(리스트 1개)
를 만들었습니다.
ghci> ['ㄱ' .. 'ㄴ']
"\12593\12594\12595\12596"
앞쪽은 아스키 코드지만 유니코드도
가능합니다.
이런식으로 유니코드도 바로
확인할 수 있네요.
아래 파이썬 코드와 비교해보면
하스켈 리스트의 Range 가 훨씬 간략한
문법임을 알 수 있습니다.
약간 언어별로 호불호를 따지면 안되는데
이런 장점에 대해서는 알고 있을
필요가 있어 보입니다.
>>> for i in range(10):
print(i , end=', ')
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
뿐만 아니라 아래와 같이
원래는 기본 알고리즘으로
작성해서 만들었던 숫자들도
리스트로 바로 만들 수 있습니다.
홀수, 짝수, N의 배수를 만들 수 있습니다.
ghci> [1,3 .. 30]
[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29]
ghci> [2,4 .. 20]
[2,4,6,8,10,12,14,16,18,20]
ghci> [0,5 .. 50]
[0,5,10,15,20,25,30,35,40,45,50]
이 문법은 잘 사용하면 좋은데
잘못사용하면 Tricky 하니까
적당한 리스트를 생성하는데만
사용하도록 합니다.
복잡한 수열을 만들기는 어렵지만
C같이 원초적인 언어계열에서는
for 루프와 if 문으로 만들어야
하는 수열을 직관적으로 만들 수
있다는 장점이 있습니다.
잘 보면 약간 수학책에 쓰는
느낌이 듭니다. (자동 수학 노트?)
역시 하스켈은 수학자들이 많이
사용하는 언어 같네요.
무한리스트도 만들어 낼 수 있는데
하스켈은 lazy 한 언어라서
최종값을 내기전에 중간계산결과를
계속 돌려줍니다.
무한리스트는 cycle 이나 repeat로
만들 수 있는데 이 포스팅은 기초니까
생략하겠습니다.
주의할 점은 부동소수점 수로
연산하면 오차가 발생합니다.
(list comprehension)
리스트 컴프리헨션은 문법적으로
복잡한 코드를 수학식 처럼
축소시키는 것을 의미합니다.
코드의 간결함을 추구하는
파이썬에도 있습니다.
하스켈에서는 여러가지
응용법이 있는데 여기서는
기본적인 것들을 훑고 가겠습니다.
기본적인 형태는 아래와 같습니다.
대략 보면... 수학에서 집합기호와
비슷합니다.
읽어보면,
x에 대해서 정의하는 것 입니다.
x는 1부터 10 사이의 정수입니다.
라고 리스트를 만들어 줍니다.
ghci> [ x | x <- [1 .. 10]]
[1,2,3,4,5,6,7,8,9,10]
홀수와 짝수 5의 배수를 만들때는
아래와 같이 합니다.
ghci> [ x*2 | x <- [1 .. 10]]
[2,4,6,8,10,12,14,16,18,20]
ghci> [ x*2-1 | x <- [1 .. 10]]
[1,3,5,7,9,11,13,15,17,19]
ghci> [ x*5 | x <- [1 .. 10]]
[5,10,15,20,25,30,35,40,45,50]
약간의 노파심이지만
리스트 컴프리헨션은 좋은데
C나 자바의 루프와 제어문을
사용해서도 똑같은 결과를
만들 수 있어야 합니다.
컴퓨터의 제어원리에 대해서
모르고 이것부터 바로 사용하면
코딩을 못하지는 않겠지만
그래도 좀 아쉬울 겁니다.
하스켈의 학습자라면 적어도
하나 이상의 프로그래밍 언어에서
충분히 프로그램을 만들 수 있다고
보기 때문에 상세하게
설명하지는 않겠습니다.
가장 기본적인 것들이므로
인터넷 자료만 찾아도 쉽게
배울 수 있습니다.
*다음은 조건을 추가하는 부분입니다.
x는 1부터 10까지의 정수이고,
x는 5보다 크다.
이런 식으로 작성할 수 있습니다.
ghci> [ x | x <- [1 .. 10], x >= 5]
[5,6,7,8,9,10]
ghci> [ x*2 | x <- [1 .. 25], x*2 >= 30]
[30,32,34,36,38,40,42,44,46,48,50]
ghci> [ x*2 | x <- [1 .. 25], x `mod` 7 == 0]
[14,28,42]
다음과 같이 사용도 가능합니다.
리스트에 들어가는 수의
조건을 추가하는 것이죠.
조건은 여러개를 추가할 수 있습니다.
ghci> [x | x <- [1..7], x/=3, x/=5]
[1,2,4,6,7]
이밖에도 리스트의 다양한 사용방법이
있겠지만 기초에 관한 포스팅이므로
여기서 마무리를 하겠습니다.