이번 장에서는 Shape를 Graphic으로 변경하여 윈도우에 그리는 코드를 작성해보자. 모듈 이름은 Draw라 하자. module Draw (...) where import Shape import SOEGraphics -- 윈도우의 사이즈: 600x500 xWin, yWin :: Int xWin = 600 yWin = 500 -- 인치를 픽셀로 바꾸는 함수 inchToPixel :: Float -> Int inchToPixel x = round (100 * x) -- 윈도우의 중심을 (0,0)으로 하는 것으로 쓰는 것이 편하므로 이것을 실제 윈도우의 좌표(좌상단을 (0,0)으로 하는 것)로 바꾸어 주는 함수를 만든다. trans :: Vertex -> Point trans (x, y) = (xWin2 + inchToPixel x, yWin2 - inchToPixel y) xWin2, yWin2 :: Int xWin2 = xWin 'div' 2 yWin2 = yWin 'div' 2 transList :: [Vertex] -> [Point] transList [] = [] transList (p:ps) = trans p : transList ps --Shape을 Graphic으로 바꾸어 주는 함수 shapeToGraphic :: Shape -> Graphic shapeToGraphic (Rectangle s1 s2) = let s12 = s1 / 2 s22 = s2 / 2 in polygon (transList [(-s12, -s22), (-s12, s22), (s12, s22), (s12, -s22)]) shapetoGraphic (Ellipse r1 r2) = ellipse (trans (-r1, -r2)) (trans (r1, r2)) shapeToGraphic (RtTriangle s1 s2) = polygon (transList [(0,0), (s1,0), (0,s2)]) shapeToGraphic (Polygon vts) = polygon (transList vts)
Just For Fun!
Big brother is watching you.
Thursday, August 25, 2011
Chapter 4. Shapes II: Drawing Shapes
Chapter 3. Simple Graphics
Hello World를 화면에 출력해보자.
Main.hs라는 파일에 아래의 내용을 입력한다.
-- module의 이름과 파일의 확장자를 뺀 이름이 같아야 한다.
module Main where
main = putStr "Hello World\n"
위 프로그램을 컴파일 후 실행하면 화면에 Hello World가 출력될 것이다. putStr의 type은 'String -> IO ()'이다. I/O action을 취하는 것은 IO T 로 표시한다. 여기서 T는 type을 의미한다. IO ()는 I/O를 하고 아무것도 리턴하지 않는 것을 의미한다. IO String은 I/O를 하고 String을 리턴하는 것을 의미한다.
파일에 텍스트를 쓰고 화면에 텍스트를 표시하는 것을 둘 다 하고 싶을 수 있다. 이럴 경우에는 do를 사용한다.
do writeFile "testFile.txt" "Hello File System"
putStr "Hello World\n"
testFile.txt라는 파일에 Hello File System을 쓰고 화면에 Hello World를 표시할 것이다. writeFile은 아래의 타입을 가진다.
writeFile :: FilePath -> String -> IO ()
type FilePath = String
사용자로부터 한 라인의 입력을 얻기 위해서는 getLine을 사용하면 된다.
getLine :: IO String
사용자로부터 입력을 받고 그것을 testFile.txt에 쓰는 코드는 아래와 같다.
do s <- getLine
writeFile "testFile.txt" s
파일의 전체 내용을 읽어서 화면에 표시하는 코드는 아래와 같다.
do s <- readFile "testFile.txt"
putStr s
그래픽 명령어를 살펴보자.
윈도우를 만드는 함수는 openWindow이다.
openWindow :: Title -> Size -> IO Window
type Title = String
type Size = (Int, Int)
새 윈도우를 만드는 코드를 짜보자.
main = runGraphics (
-- 타이틀이 First Window이고 크기가 300x300인 위도우를 만든다.
do w <- openWindow "First Window" (300, 300)
-- 지정한 윈도우에 그림을 그린다.
drawInWindow w (text (100, 200) "HelloWorld")
-- 키보드로부터 키 입력을 받아서 이를 k에 넣는다.
k <- getKey w
-- 지정한 윈도우를 닫는다.
closeWindow w
)
위에 사용된 함수들의 타입을 보자. runGraphics가 그래픽 action들을 실행한다.
runGraphics :: IO ()
drawInWindow :: Window -> Graphic -> IO ()
test :: Point -> String -> Graphic
type Point = (Int, Int)
getKey :: Window -> IO ()
closeWindow :: Window -> IO ()
위 코드는 아무 키나 입력되면 위도우를 닫는다. 이것을 ' '(space)가 입력되면 윈도우를 닫는 코드로 바꾸어 보자.
main = runGraphics (
do w <- openWindow "First Program" (300, 300)
drawInWindow w (text (100, 200) "Hello World")
spaceClose w
)
spaceClose :: Window -> IO ()
spaceClose w = do k <- getKey w
if k == ' '
then closeWindow w
else spaceClose w
Chapter 2. A Module of Shapes: Part 1
우리가 만든 함수나 타입들을 다른 곳에서 사용할 수 있게 하려면 module을 만든다. module Shape (...) where -- ...에 외부에서 사용할 수 있게 하려는 함수나 타입을 적어준다. ...body-of-module... 이때 module Shape () where로 하면 아무것도 외부에서 사용할 수 없게 하는 것이고 module Shape where로 하면 모든 함수를 외부에서 사용할 수 있게 하는 것이다. 다른 곳에서 이 module을 사용하려면 import를 한다. import Shape 새로운 data type을 만드려면 data를 사용하고 기존의 type을 이름만 바꾸려면 type을 사용한다. 새로운 data type 만들기. data Shape = Circle Float | Square Float 위 새로운 data type의 의미는 다음과 같다: Shapre에는 두 종류가 있는데 Circle Float의 형태를 가지는 것과 Square Float의 형태를 가지는 것이 있다. 기존의 type 이름만 바꾸어서 사용하기. type Radius = Float 지금까지 공부한 내용으로 Shape의 면적을 계산하는 module을 만들어 보자 -- Shape data type의 경우 아래처럼 외부에서 사용이 필요한 이름만 넣어줄 수 있다. -- Shape의 모든 것을 외부에서 쓸 수 있게 하려면 Shape(...)으로 하면 된다. module Shape (Shape (Rectangle, Ellipse, RtTriangle, Polygon), Radius, Side, Vertex, square, circle, distBetween, area ) where data Shape = Rectangle Side Side | Ellipse Radius Radius | RtTriangle Side Side | Polygon [Vertex] deriving Show -- Show라는 type class를 상속받음을 의미한다. 뒤에서 설명할 것이다. -- Float을 Radius나 Side로 이름은 바꿈으로서 Shape이라는 새 data type에서의 의미를 더 잘 나타낼 수 있다. type Radius = Float type Side = Float type Vertex = (Float, Float) -- Shape의 면적을 계산하는 함수 area :: Shape -> Float area (Rectangle s1 s2) = s1 * s2 area (RtTriangle s1 s2) = s1 * s2 / 2 area (Ellipse r1 r2) = pi * r1 * r2 area (Polygon (v1:vs)) = polyArea vs where polyArea :: [Vertex] -> Float polyArea (v2:v3:vs') = triArea v1 v2 v3 + polyArea (v3:vs') polyArea _ = 0 -- 삼각형의 면적을 계산하는 함수 triArea :: Vertex -> Vertex -> Vertex -> Float triArea v1 v2 v3 = let a = distBetween v1 v2 b = distBetween v2 v3 c = distBetween v3 v1 s = 0.5 * (a+b+c) in sqrt (s*(s-a)*(s-b)*(s-c)) 앞의 세개는 간단하지만 Polygon은 조금 복잡하다. Polygon의 면적 계산 방식은 첫번째 3개의 vertex의 삼각형의 면적을 구하고(v1, v2, v3), 두번째 vertex를 뺀 새 Polygon을 만들고(v1:v3:vs), 다시 첫번째 3개의 vertex의 삼각형의 면적을 구하는 것을 반복하는 것이다. triArea는 삼각형의 면적의 계산인데 Heron's formula이다.
Chapter 2. Getting Started
함수 사용하는 방법은 (procedure arg1 arg2 ...)의 형태가 된다. (+ 1/2 1/2) => 1 여러 값을 포함하는 데이터 구조로 list가 있다. list는 괄호 안에 여러 값들을 적는다. (1 2 3 4 5) list에 다양한 type이 올 수 있다.(참고: haskell의 경우는 같은 type만이 올 수 있다.) (4.2 "hi") procedure와 단순 list를 구별하는 방법은 어떻게 할까? 이를 위해 quote가 있다. quote를 사용하면 quote 뒤에 오는 것은 procedure가 아니라 단순 list의 data라는 것을 알린다. 예: (quote (+ 3 4)) => (+ 3 4) quote에 대한 축약으로 '(single quotation mark)를 사용할 수 있다. '(/ (+ 2 -1) 3) => (/ (+ 2 -1) 3) list에 관련된 함수로 cons와 car과 cdr(could-er라 읽는다)가 있다. cons는 list를 만들고, car은 list의 첫번째 element를 리턴하고, cdr은 list의 첫번째를 제외한 나머지를 리턴한다. (cons 'a '()) => (a) (car '(a b c)) => a (cdr '(a b c)) => (b c) cons는 pair를 가지고 list를 만든다. 이때 꼭 두번째 파라메터가 list일 필요는 없다. 이렇게 두번째 파라메터가 list가 아닐 경우를 improper list라 부르고 dotted-pair notation으로 표시한다. (cons 'a 'b) => (a . b) (cdr '(a . b)) => b 변수에 값을 주고 싶은 경우 let를 사용한다. 형태는 (let ((var expr) ...) body1 body2 ...) 이다. var은 body에서만 유효하다. var을 정의할 때 ()대신 [] 사용할 수 있다. (let ((x 2)) (+ x 3)) => 5 (let ([x 2]) (+ x 3)) => 5 procedure의 선언을 위해 lanbda를 사용한다. 형식은 (lambda (var ...) body1 body2 ...)이다. (lambda (x) (+ x x)) => #((lambda (x) (+ x x)) (* 3 4)) => 24 아래와 같이 procedure를 정의할 때 lambda에 관계되지 않는 variable을 쓰면 어떻게 될까? (let ([f (let ([x 'sam]) (lambda (y z) (list x y z)))]) (f 'i 'am)) => (sam i am) 결과를 보면 알 수 있듯이 lambda를 정의할 때의 x값인 sam이 proedure에 들어가 있게 된다.(이것을 static scoping이라 한다.) 따라서 아래처럼 해도 결과는 똑같게 나온다. (let ([f (let ([x 'sam]) (lambda (y z) (list x y z)))]) (let ([x 'not-sam]) (f 'i 'am)) => (sam i am) 실제로 lambda의 형식은 세 종류로 이야기할 수 있다. 1. parameter의 개수가 정해져 있는 경우(앞에서 이야기한) 2. 하나의 variable만 오는 경우(괄호가 없는것에 주의) 이 variable에 argument의 list가 들어간다. ;; x에 1 2 3 4가 list인 (1 2 3 4)를 가지게 된다. (let ([f (lambda x x)]) (f 1 2 3 4)) => (1 2 3 4) 3. 정해진 variable이후 하나의 variable이 오고 이 variable에 나머지 argument가 list로 들어가는 경우 ;; x에는 a가, y에는 b가, z에는 c와d의 list인 (c d)가 들어가게 된다. (let ([h (lambda (x y . z) (list x y z))]) (h 'a 'b 'c 'd)) => (a b (c d)) let이나 lambda에 의한 expression은 특정 영역에서만 유효하게 된다. primitive procedure처럼 쓰는 경우를 원한다면 define를 쓴다. (define double-any (lambda (f x) (f x x))) ;; 한번 double-any를 위와 같이 정의하고 나면 어디서든 사용할 수 있다. (double-any + 10) => 20 lambda를 이용한 define의 경우 (define (var0 var1 ... varn) e1 e2 ...)의 형태로 간단하게 쓸 수 있다. (define (cadr x) (car (cdr x))) 조건문의 경우 (if test consequent alternative)로 쓴다. (define abs (lambda (n) (if (< n 0) (- 0 n) n))) (abs 77) => 77 (abs -77) => 77 or은 (or expr ...)이다. expr을 순서대로 계산한 후 true가 있으면 true이고 없으면 false이다. and는 (and expr ...)이다. expr을 순서대로 계산하다가 false가 있으면 false이고 모두가 true이면 true이다. and와 or 둘다 마지막으로 계산한 expression의 값을 리턴한다. predicate의 경우 ?(question mark)를 주로 끝에 붙여준다. null?은 null을 체크한다. (null? '()) => #t (null? 'abc) => #f eqv?는 두 argument가 같은지를 비교한다. (eqv? 'a 'a) => #t (eqv? 'a 'b) => #f (eqv? (cons 'a 'b) (cons 'a 'b)) => #f ;; 다른 cons에 의해 생성되었기 때문에 false이다. object의 type을 비교하는 함수로 pair?, symbol?, number?, string?등이 있다. 조건문이 중첩되거난 하는 경우는 cond를 사용하는 것이 편할 수 있다. cond의 형태는 (cond (test expr) ... (else expr))이다. else는 적용되는 값이 없을 경우 실행된다. else는 생략할 수 있다. (define sign (lambda (n) (cond [(< n 0) -1] [(> n 0) +1] [(= n 0) 0]))) list의 길이를 계산하는 함수는 아래와 같이 만들 수 있다. (define length (lambda (ls) (if (null? ls) 0 (+ (length (cdr ls)) 1)))) 위처럼 base case와 recursion step을 갖는 것을 recursion이라 부른다. assignment는 set!으로 할 수 있다. (define abcde '(a b c d e)) abcde => (a b c d e) (set! abcde (cdr abcde)) abcde => (b c d e)
Monday, August 1, 2011
Chapter 1: Problem Solving, Programming and Calculation
프로그래밍은 문제를 해결하는 것이다. 문제 해결은 computation by calculation으로 볼 수 있다.
예)
3 * ( 9 + 5 ) => 3 * 14 => 42
위의 예를 함수 형태로 바꾸어 볼 수 있다.
simple x y z = x * ( y + z )
함수란 input이 같으면 output이 항상 같게 나오는 것을 의미한다.
계산의 과정중에 있는 것을 expression이라 하고 이의 최종 결과를 value라 한다. 모든 expression은 type을 갖는다. type은 같은 집합으로 묶을 수 있는 expression의 집합이다. 정수는 Integer, 문자는 Char등으로 표시한다. type은 항상 대문자로 시작해야 하고 value는 소문자로 시작해야 한다.
함수를 적을 때 type도 같이 적어준다.
simple :: Integer -> Integer -> Integer -> Integer
simple x y z = x * ( y + z )
실제로는 type을 적어주지 않아도 하스켈이 알아서 type을 추론해준다. 하지만, 가능하다면 항상 적어주는 것이 좋다. 그렇게 하는 것이 에러를 방지할 수도 있고 문서화에도 도움이 된다.
프로그래밍에 있어서 추상화(abstraction)는 매우 중요하다. 값에 이름을 다음과 같이 주어서 쓰거나,
pi = 3.14159
함수를 다음과 같이 쓰는 것이 추상화의 예가 될 것이다.
circleArea :: Float -> Float
circleArea r = pi * r^2
totalArea = circleArea r1 + circleArea r2 + circleArea r3
위의 함수 선언은 circleArea가 외부에서 쓰일 일이 없다면 다음과 같이 쓰는 것이 좋다.
totalArea = let circleArea r = pi * r^2
in circleArea r1 + circleArea r2 + circleArea r3
let 안에서 사용된 선언은 in 안에서만 유효하다.
임의의 길이의 데이터를 쓸 일이 있으면 list를 사용할 수 있다. 예를 들어, 1,2,3에 대한 list는 [1,2,3]으로 표시한다. element가 없는 list는 []로 표시하고 nil이라 읽는다. []에 x라는 element를 앞에 더하면 x : []로 표시할 수 있다. 따라서, [1,2,3]은 1:2:3:[]으로 표시할 수 있다.
list의 element의 합을 더하는 함수는 아래와 같이 만들 수 있다.
listSum :: [Float] -> Float
listSum [] = 0
listSum (x:xs) = x + listSum xs
위의 함수에 대한 적용은 pattern matching에 의해 이루어진다. pattern matching은 위로부터 아래로 matching이 될때까지 순서대로 이루어지며 matching이 되면 오른쪽의 내용이 계산된다.
값에 이름을 부여하거나 중복되는 expression을 함수로 만들어서 사용하는 것은 modularity의 측면에서도 좋다. 이렇게 함으로서 다른 곳에서도 똑같은 것을 다시 만들 필요없이 있는 것을 재활용하면 되기 때문이다.
하스켈에서 정수 표현의 경우 Integer와 Int를 사용할 수 있다. Int의 경우 CPU의 word 크기에 따른 최대/최소값을 가진다. Integer의 경우는 제한이 없이 사용할 수 있지만 효율성이 떨어진다.
Subscribe to:
Posts (Atom)