E D R , A S I H C RSS

하스켈

프로그래밍 언어는 결국 컴퓨터 상에서 실행되는 프로그램을 생성하기 위한 도구다. 가장 근본적이자 원시적인 언어는 기계어이고, 기계어의 거의 일대일 대응 격인 어셈블리어가 그 다음이다. 2학년 수업 중에 <컴퓨터 구조>에서 배우듯이 프로그램의 실행은 일련의 명령(instruction)을 클럭이 박동함에 따라 인출, 판독, 실행하는 과정이다. 즉 태생이 절차적(procedural)일 수 밖에 없다.

어셈블리어에서는 프로그램에서 쓰이는 모든 데이터를 단순히 데이터 영역에 주루룩 나열한다면, 이제 C에서는 여러 데이터를 하나로 묶어 구조체로 만드는 구조적(structural) 프로그래밍을 지원한다. 그 구조체들 사이의 상하, 대응, 포함 관계나 상호작용하는 방법을 다루는 객체지향(object-oriented)이라는 개념이 등장하였고 C++, 자바 같은 주류 언어에서 문법적으로 지원하고 있다. 하지만 이들 언어로 작성한 코드는 다음과 같은 일반적인 개념이 성립한다.

1. 코드의 각각의 줄이 하나의 명령문(statement)이 된다.
2. 한 번에 한 명령문을 실행하고 그 다음에 어떤 명령문을 실행할 지가 결정된다.

즉 프로그램의 원시적인 작동 방식이 여전히 코드 상에 반영되어 있는 것이다. 한 명령을 실행하고, 프로그램 카운터는 다음에 실행할 명령을 찾고, 그 명령문은 다음 클럭에 실행하고... 이걸 추상적인 수준으로 끌어올렸을 뿐 코드에 그 절차가 고스란히 묻어있는 것이다.

함수형(functional) 언어인 하스켈은 완전히 다른 방식으로 프로그램에 접근한다. 명령 인출-판독-실행은 잊어라. 흐름 제어도 잊어라. 문제를 푸는 데 필요한 정의들을 적어라. 그리고 그 정의에서 필요한 부분만 실행하라. 명령형(imperative) 언어에서는 추상적인 논리를 코드로 실행 가능한 절차로 표현한다면 하스켈에서는 코드로 논리를 그대로 표현하고 컴파일러가 그걸 알아서 실현 가능한 프로그램으로 만들어낸다. 하스켈 코드 한 줄은 기계에 종속적인 절차와 일대일 대응이 되지 않는다.

C 같은 언어로 프로그래밍을 처음 배울 때면 x = x+1 은 "x와 x+1가 같다"가 아니라 x에 x+1을 할당하는(assign) 거라고 배운다. 하스켈에서는 정말로 x = x+1 이라고 정의해버린다. (하지만 x를 실제로 쓰기 전에는 프로그램이 무한 루프에 빠지지는 않는데 이는 하스켈의 지연 평가(lazy evaluation)라는 특성과 관련이 있다. 자세한 건 검색)

또한 하스켈의 주요 특징 중 하나는 순수성인데, 함수 내에서 부수효과(side-effect)를 일으킬 수 없다는 것이다. 함수의 출력이 달라지는 요인은 오직 그 함수에 입력되는 인자 뿐이다. 인자가 같으면 무조건 출력도 같다. 어떤 함수 내에서 데이터에 가하는 어떤 수정도 다른 함수의 실행에 영향을 끼칠 수 없다. (그러기 위해서는 데이터를 항상 복사해야 하는데 자연스레 수행능력 면에서는 C++ 같은 언어보다 뒤쳐질 수 밖에 없다)

하스켈 코드가 읽기 힘든 것은 특유의 압축성 때문이다. 사실 풀어쓰면 그다지 어려울 것 없는 코드를, 단순한 연산들을 정체불명의 함수들(id, const, map, fold, zip, ($), (.), (>>=) 등)로 감싸고 한 줄에 모조리 때려박으면 이제 타 언어 사용자가 알아볼 수 없는 괴이한 코드가 된다. C나 자바가 구구단이라면 하스켈은 19단은 외워야 걸음마를 뗄 수 있는 것 같다.

예시 1
import Data.Bits
import Numeric

-- 32비트 정수를 8비트씩 잘라낸다
split :: Int -> [Int]
split n = [b1, b2, b3, b4]
  where
    b1 = n .&. 0xFF
    b2 = (n `shiftR` 8) .&. 0xFF
    b3 = (n `shiftR` 16) .&. 0xFF
    b4 = (n `shiftR` 24) .&. 0xFF

main = print $ map showHex (split 0x12345678) -- ["12","34","56","78"] 출력
하스켈 코드를 본 적이 없어도 split 함수를 대충은 알아볼 수 있다. 이제 한 줄로 압축해보자.
split n = map (.&. 0xFF) (zipWith shiftR [n,n,n,n] [24,16..])
???????????????????????????????????????????????????????

예시 2 - 리스트의 짝수 번째 원소들에만 2 곱하기
xs = [1, 10, 2, 20, 3, 30, 4, 40]
xs' = zipWith ($) (cycle [(*2), id]) xs -- 뭐라고요
main = print xs >> print xs'

-- 출력
[1,10,2,20,3,30,4,40]
[2,10,4,20,6,30,8,40]

예시 3 - https://algospot.com/judge/problem/read/LECTURE 의 해답
import System.IO
import Data.List
import Control.Monad

-- 알아볼 수 없는 모나드의 향연
main = getLine >>= (\ n -> replicateM_ (read n :: Int) solve)
solve = getLine >>= (\ str -> putStrLn $ (concat . sort . pairup) str)
pairup str = sub [] str where
    sub xs [] = xs
    sub xs ys = sub ((2 `take` ys) : xs) (2 `drop` ys)





Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:31:30
Processing time 0.0223 sec