하스켈 가볍게 훑어보기

이현재·2024년 1월 7일
3

하스켈의 로고는 람다 대수에서의 λ와 모나드에 사용하는 연산자인 >>=를 겹쳐놓은 이미지입니다.

하스켈 소개

하스켈의 간략한 역사

논리학자 하스켈 커리의 이름을 딴 이 언어는 강력한 정적 유형 시스템, 지연 평가, 함수형 프로그래밍 패러다임을 추구하는 등 당시 다른 언어에는 없던 여러 가지 기능들을 담는 것을 목표로 삼았습니다.

논리학자 하스켈 브룩스 커리의 이름을 딴 하스켈은 함수형 언어와 컴퓨터 아키텍처 연구 학술회에서 1987년에 처음 고안되어서 이후 여러 번의 반복을 통해 발전해 왔으며, 하스켈 98과 하스켈 2010이 현재 언어의 표준에 가까운 버전입니다.

프로그래밍 세계에서 하스켈의 위치

다양한 프로그래밍 언어 생태계에서 하스켈은 독특한 틈새 시장을 차지하고 있습니다. 대부분의 소프트웨어 개발을 지배하는 명령형 스타일과는 상당히 다른 기능과 불변성을 강조하는 함수형 언어로서의 순수성을 인정받고 있습니다. 하스켈은 학계에서 함수형 프로그래밍 개념을 가르치는 데 자주 사용되지만 산업계, 특히 금융 및 데이터 분석과 같이 신뢰성과 수학적 정확성이 가장 중요한 분야에서도 두드러지게 사용됩니다.

하스켈의 주요 기능 및 장점

하스켈은 다른 언어와 차별화되는 몇 가지 특징이 있습니다

순수 함수형 언어 (Purely Functional): 하스켈은 순수 함수형 언어이므로 함수에 부작용이 없습니다. 따라서 특히 동시 실행 애플리케이션에서 예측 가능한 코드와 버그가 줄어듭니다.

게으름 (Laziness): 하스켈은 기본적으로 지연 평가를 사용합니다. 즉, 결과가 필요할 때까지 표현식을 평가하지 않으므로 성능이 향상되고 무한한 데이터 구조를 구성할 수 있습니다.

강력하고 정적인 타입 시스템: 하스켈의 타입 시스템은 강력하며 컴파일 시 오류를 포착하는 데 도움이 됩니다. 또한 유형 추론과 같은 고급 기능을 지원하므로 프로그래머가 작성해야 하는 유형 관련 코드의 양을 줄일 수 있습니다.

타입 클래스: 일종의 임시 다형성을 허용하는 강력한 기능인 타입 클래스는 다른 언어에서는 쉽게 달성할 수 없는 수준의 추상화와 코드 재사용을 가능하게 합니다.

하스켈 구문 이해하기

변수, 함수 및 연산자

하스켈의 구문은 우아하고 간결하며 수학적 표기법과 비슷합니다. 변수는 간단한 할당을 사용하여 정의되지만, 명령형 언어와 달리 이러한 할당은 항상 불변이므로 변수에 값이 할당되면 변경할 수 없습니다.

함수는 하스켈의 핵심이며 또한 간단한 방식으로 정의됩니다. 함수 정의에는 함수 이름, 매개변수, 수행될 계산을 지정하는 함수 본문이 포함됩니다. 하스켈의 연산자는 대부분 수학적으로 타당한 것이며, 가독성을 높이기 위해 직접 정의하거나 접두사 또는 접미사 형태로 사용할 수도 있습니다.

-- 변수 및 함수 정의의 예시
x = 10  -- 이 문맥에서 x는 이제 영원히 10입니다.
square y = y * y

하스켈의 강력한 타입과 정적 타입 시스템

하스켈의 타입 시스템은 하스켈의 가장 강력한 기능 중 하나입니다. 강타입이므로 컴파일 시 모든 표현식의 타입을 알 수 있고, 런타임 오류가 크게 줄어듭니다. 또한 하스켈은 타입 추론 기능도 갖추고 있어 컴파일러가 명시적인 타입 주석 없이도 표현식의 타입을 추측할 수 있는 경우가 많습니다.

그러나 하스켈 프로그래머는 꼭 필요하지 않은 경우에도 문서화 및 명확성을 위해 유형 선언을 사용하는 경우가 많습니다. 정사각형 함수에 대한 간단한 타입 선언을 살펴봅시다.

square :: Int -> Int
square y = y * y
-- 여기서 :: 는 "has type of"로 읽을 수 있으며, 화살표는 Int를 취하고 Int를 반환하는 함수를 나타냅니다.

패턴 매칭 및 가드

패턴 매칭을 사용하면 함수가 입력의 구조에 따라 다른 코드를 실행할 수 있습니다. 명령형 언어에서 흔히 볼 수 있는 if-else 문이나 스위치 케이스가 필요 없는 강력한 기능입니다.

가드는 조건부 논리를 더 읽기 쉽게 만드는 기능입니다. 패턴 매칭과 유사하지만 패턴이 아닌 bool 조건을 기반으로 합니다.

factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)

absVal :: Int -> Int
absVal x
  | x < 0     = -x
  | otherwise = x

코드 가독성 및 레이아웃 규칙

하스켈의 구문은 불필요한 토큰을 최소화하는 데 중점을 두고 깔끔하고 가독성이 좋도록 설계되었습니다. 예를 들어 괄호는 대부분 선택 사항이며 중괄호와 세미콜론은 일반적으로 공백과 들여쓰기로 대체됩니다.

구조를 전달하기 위해 레이아웃에 의존하는 이러한 방식은 "off-side rule"으로 알려져 있으며, 하스켈 구문의 특징으로, 하스켈 코드베이스 전반에서 깔끔하고 일관된 스타일을 장려합니다.

main = do
  print $ square 4
  print $ factorial 5
  print $ absVal (-10)

함수형 프로그래밍 패러다임

순수 함수와 불변성

하스켈은 함수형 프로그래밍 패러다임을 충실히 따르며, 그 핵심은 순수 함수를 사용하는 것입니다. 순수 함수는 동일한 입력에 대해 전역 상태를 수정하거나 변수 값을 변경하는 등의 부작용을 일으키지 않고 항상 동일한 출력을 생성하는 함수를 말합니다. 이러한 불변성은 시간이 지남에 따라 상태가 변하는 것이 아니라 함수를 통해 흐르기 때문에 하스켈 프로그램을 추론하고 디버깅하는 작업을 훨씬 더 간단하게 만듭니다.

-- 이 함수는 순수함수입니다.
addTwoNumbers :: Int -> Int -> Int
addTwoNumbers a b = a + b

하스켈에서 변수는 설계상 불변입니다. 값을 이름에 바인딩하면 불변이므로 상태 변경과 관련된 모든 종류의 버그를 만들지 않을 수 있습니다.

일급 및 고차 함수

하스켈의 함수는 일급시민으로, 다른 함수에 인수로 전달하거나 다른 함수에서 값으로 반환하거나 데이터 구조에 저장할 수 있습니다. 고차 함수는 다른 함수를 인수로 받거나 결과로 반환하는 함수입니다. 이를 통해 표현력이 풍부하고 간결한 코드를 작성할 수 있으며, 일반적인 패턴을 재사용 가능한 컴포넌트로 추상화할 수 있습니다.

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

재귀 및 무한한 데이터 구조체

하스켈에는 for 또는 while 루프와 같은 전통적인 반복 구조가 없습니다. 대신 재귀를 사용하여 동일한 결과를 얻습니다. 재귀 함수는 강력하고 표현력이 풍부한 반복 메커니즘을 제공합니다.

하스켈의 지연 평가 덕분에 무한한 데이터 구조도 처리할 수 있습니다. 목록은 무한대일 수 있으며, 하스켈은 필요한 것만 계산합니다.

-- 무한한 자연수 목록
naturals :: [Int]
naturals = [0..]

-- factorial을 계산하는 재귀 함수
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)

게으름과 게으른 평가

게으름은 하스켈의 근본적인 실행 모델입니다 . 게으른 언어에서는 값이 필요할 때까지 표현식이 평가되지 않습니다. 이를 통해 잠재적으로 무한한 데이터 구조를 정의하고, 제어 구조를 간단한 라이브러리 함수로 사용할 수 있으며, 필요한 만큼만 계산하는 우아하고 효율적인 알고리즘을 구성할 수 있습니다.

-- 게으름을 이용해 처음 10개의 자연수 취하기
takeTenNaturals :: [Int]
takeTenNaturals = take 10 naturals

순수 함수와 불변성은 보다 예측 가능한 코드를 만들고, 일차 및 고차 함수는 강력한 추상화를 가능하게 하며, 재귀와 게으름은 반복과 데이터 구조 조작에 대한 기존의 절차형 언어와 다른 사고방식을 보여줍니다.

하스켈의 언어 철학

수학적 개념의 표현

하스켈의 언어 설계는 수학적 논리와 카테고리 이론의 영향을 많이 받았습니다. 이는 수학적 표현을 매우 유사하게 모방한 구문과 복잡한 수학적 속성을 표현할 수 있는 문법에서 드러납니다. 이 언어가 함수와 불변성을 강조하는 것은 동일한 입력이 주어졌을 때 부작용 없이 일관된 출력을 생성하는 함수의 수학적 개념을 반영한 것입니다. 하스켈의 목표는 수학적 추론을 실행 가능한 코드로 직접 변환하여 높은 수준의 정확성과 예측 가능성을 제공하는 것입니다.

-- 수학적 개념 피보나치를 표현하는 하스켈 함수
fibonacci :: Int -> Int
fibonacci n
  | n == 0    = 0
  | n == 1    = 1
  | otherwise = fibonacci (n - 1) + fibonacci (n - 2)b

가독성 및 간결성

하스켈의 구문은 읽기 쉽고 간결하도록 설계되었습니다. 다른 언어에서 흔히 볼 수 있는 상용구와 장황한 구문을 피하여 간결하고 핵심을 찌르는 코드를 작성할 수 있습니다. 상당한 공백과 최소한의 구문을 사용하여 가독성을 높이고, 언어의 강력한 추상화를 통해 코드를 반복할 필요성을 줄입니다. 하스켈 코드는 종종 수도코드처럼 읽히기 때문에 개발자가 프로그램의 논리와 구조에 더 쉽게 집중할 수 있습니다.

-- haskell의 문법인 list comprehension을 사용한 간결한 정의
primes :: [Int]
primes = [x | x <- [2..], all ((/= 0) . (mod x)) [2..(x-1)]]

설계를 통한 유형 안전성과 정확성

하스켈은 수많은 런타임 오류를 제거하는 데 도움이 되는 엄격한 타입 시스템을 적용합니다. 유형은 코드의 의도를 전달할 수 있는 문서형식이며, 타입 클래스 및 대수 데이터 유형과 같은 Haskell의 기능은 프로그램이 의도한 대로 작동하도록 보장합니다. 이 언어적인 디자인을 통해 프로그램이 "컴파일되면 작동"하게 합니다.

-- Maybe 타입을 사용한 안전한 나누기 함수의 예시입니다.
safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide x y = Just (x `div` y)

추상화 및 재사용성

추상화 능력은 하스켈의 가장 강력한 측면 중 하나입니다. 하스켈의 타입 시스템과 고차 함수를 통해 개발자는 다양한 상황에 적용할 수 있는 일반적이고 재사용 가능한 코드를 작성할 수 있습니다. 이는 중복을 줄이고 언어의 표현력을 향상시킵니다. 일반적인 패턴을 함수로 추상화하고 다형성을 위해 타입 클래스를 활용하는 것은 Haskell이 코드 재사용을 가능하게 하는 방법 중 일부입니다.

-- map을 위한 재사용 가능한 고차 함수
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe f Nothing  = Nothing
mapMaybe f (Just x) = Just (f x)

하스켈은 함수형 프로그래밍의 개념과 수학적인 여러 논리들을 배울 수 있는 재미있는 언어입니다. 하스켈의 기능들은 현대의 여러 언어들에도 영향을 미치고있으며 더 나은 프로그램을 작성하는데 도움을 줍니다.

-- 하스켈 시작을 위한 초대장
main :: IO ()
main = putStrLn "Haskell에 오신 것을 환영합니다!"

참고

profile
코드 보는걸 좋아합니다. 궁금한게 많습니다.

0개의 댓글