비트코인 머클 트리 알고리즘의 취약점과 우회방안

비트코인 마이너는 블록에 포함될 트랜잭션들의 머클 루트를 계산하여 블록 헤더에 넣고 블록을 생성합니다. 다른 비트코인 풀 노드들은 블록 헤더의 머클 루트가 스스로 계산한 머클 루트와 같은지 비교하여 해당 블록의 유효성을 검사합니다.

머클 루트는 세부적인 구현에서 약간씩 차이가 나는데, 비트코인은 각 라운드의 해시가 홀수 개이면 마지막 해시를 한 번 중복해서 넣는 방식으로 짝수로 만들고 계산을 합니다. 일례로 1 2 3 해시의 머클 루트를 계산하기 위해서는 1 2 해시를 합쳐서 A를 만들고 남은 해시가 홀수 개니 3을 하나 더 추가해서 3 3 해시를 합쳐서 B를 만드는 방식입니다. 그런 다음 A B를 합쳐서 해시를 계산하면 머클 루트가 나옵니다.

그런데 이런 방식으로 머클 루트를 만들면 트랜잭션을 중복해서 넣는 방식으로 머클 루트가 같은 블록을 쉽게 만들어 낼 수 있는 문제가 있습니다. 예를 들어, 아래 그림처럼 1 2 3 4 5 6 해시의 머클 루트가 A라고 하면 5 6해시를 중복해서 한 번 더 넣은 1 2 3 4 5 6 5 6의 머클 루트도 A가 됩니다. 다음 라운드에서 F가 하나인 것과 F가 두 개인 것이 해시 값이 C로 동일하기 때문입니다.

누군가가 고의로 머클 루트는 같지만 validation이 실패하는 블록을 고의로 보내게 되면, 이 블록을 받은 노드는 이후 정상적인 블록을을 invalid하다고 판단하게 되므로 심각한 DoS 공격이 가능해집니다.

비트코인 이 문제를 인지하고 해시 목록의 마지막에 두 개의 동일한 해시가 나오는 경우를 머클 루트가 invalid한 것과 동일하게 취급하는 방법으로 우회하고 있습니다. (호환성 때문에 머클 트리 알고리즘을 바꿀 수는 없었습니다.) 1 2 3 4 5 6 5 6의 경우 해시를 계산하고 나면 D E F F가 나오는데 마지막에 F가 2번 나오므로 invalid하다고 판단하는 방식입니다.

이는 머클 트리 구현 편의를 위해 선택한 간단한 구현 세부사항이 어떤 식으로 보안 문제를 일으킬 수 있는지를 보여주는 아주 좋은 예입니다. 참고로 이 문제는 CVE-2012-2459로 리포트되어 있습니다.

비트코인 코어 지갑의 UTXO 사용법

Bitcoin Core 지갑이 UTXO를 어떤 로직에 따라 사용하는지 궁금해서 찾아봤습니다.

  1. 하나의 UTXO가 사용할 금액(타겟)을 정확히 만족시키면 해당 UTXO를 사용.

  2. 타겟보다 작은 여러 개의 UTXO의 총합이 타겟을 정확히 만족시키면 해당 UTXO들을 사용.

  3. 타겟보다 작은 여러 개의 UTXO의 총합이 타겟보다 작으면 타겟보다 큰 UTXO 중에 가장 작은 UTXO를 사용.

  4. 랜덤하게 선택한 UTXO들 조합의 총합이 타겟과 같거나 클때까지 1000번 시도. 총합이 타겟을 정확히 만족시키면 즉시 종료.

  5. (1) 타겟보다 큰 UTXO 중 가장 작은 UTXO
    (2) 4번에서 찾은 UTXO 조합 중 총합이 가장 작은 조합.
    (1), (2)를 비교해서 작은 쪽을 선택

Bitcoin Core 지갑은 아웃풋 크기를 최소화하는 방법으로 최적화되어 있는 걸 알 수 있습니다.

물론 이 방법이 항상 정답은 아닙니다. 지갑을 만드는 엔지니어가 UTXO 아웃풋 크기, 성능, 보안 등 어떤 면을 강조하느냐에 따라 다양한 알고리즘이 가능한 영역입니다.

관련 코드는 Bitcoin Core 코드에서 확인 가능합니다.

스마트 컨트랙트 개발자의 특성과 커리어

스마트 컨트랙트 개발자의 특성과 커리어에 대해 잘 정리한 글이 있어서 공유합니다.

  • 1년 이하의 개발자의 연봉도 무척 높다. 스마트 컨트랙트를 2년 이상한 개발자는 이미 너무 부자라서 고용 시장에는 없다. 다만, 현재 투자 열풍과 ICO 붐이 꺼진 후에는 거품이 꺼질 수 있다.
  • 스마트 컨트랙트 개발자가 되기 위해 암호학, 게임 이론, 프로토콜 설계, 분산 시스템 등을 다 이해해야 하는 것이 아니다. 웹 개발자가 HTTPS나 TCP/IP가 어떻게 동작하는지 알 필요가 없는 것과 마찬가지다.
  • 기술 자체는 어렵지 않지만, 작은 문제를 하나 해결하기 위해 많은 수고를 할 각오를 해야 한다. 문서도 없고 스택오버플로에 제대로 답변도 없고, 언어나 개발 도구 등도 모두 아직 성숙하지 않은 상태이기 때문이다.
  • 출시하기 전에 검증 및 테스트를 엄격하게 해야 한다는 측면에서 소프트웨어 개발보다는 하드웨어 개발과 유사한 점이 있다.

 

DAML

Hypledger Fabric이나 R3 Corda 같은 permissioned blockchain은 public blochcain과 달리 스마트 컨트랙트 작성에 Go, Java, Kotlin 같은 범용 언어를 사용합니다.

인증 서버를 통해 인증된 노드만 네트워크에 접속할 수 있기 때문에 이더리움처럼 Gas를 사용해 프로그램의 종료를 보장하거나 하는 방법은 사용하지 않는 게 일반적입니다. 일례로, Fabric의 스마트 콘트랙트를 chaincode라고 부르는데, 일반적인 Go 바이너리를 가공 없이 그대로 사용합니다.

하지만, 꼭 악의적인 의도로 작성된 스마트 콘트랙트가 아니더라도 버그 때문에 이상 동작을 할 수 있고 이를 제대로 검증하지 못하면 심각한 문제가 발생한다는 사실은 public blockchain이나 permissioned blockchain가 차이가 없습니다.

이 문제를 해결하기 위해 permissioned blockchain을 만드는 일부 업체는 스마트 컨트랙트 작성을 위한 새로운 언어를 제공하는 경우도 있습니다. 대표적인 예로, permissioned blockchain을 만드는 디지털 애셋(Digital Asset)이 있는데, 작년 금융사를 위한 DSL을 만들던 스위스 기반의 Elevence 사를 인수하였습니다.

Elevence 사가 만들던 기술은 DAML이라는 디지털 자산 모델링 언어인데, Solidity와는 달리 Turing-complete 하지 않은 언어입니다. 오픈소스 계획을 밝혔지만 아직 소스 코드가 공개되지 않아 자세한 내용은 알 수 없지만 하스켈 스타일의 total functional programming 언어로 알려져 있습니다.

public blockchain에서는 Solidity가 디팩토 표준이 되어가고 있지만, 기술적으로는 훨씬 나은 방법이 존재하고 실제로 사용되는 예가 있다는 말씀을 드리고 싶었습니다. 물론 하스켈 계열의 언어들은 진입 장벽이 높긴 하지만, Solidity로 버그 없는 스마트 컨트랙트를 작성하기 위해 들여야 하는 노력을 생각하면 어느 쪽이 실제로 더 비용이 높은지 알기 어렵습니다.

이더리움 스마트 컨트랙 언어

이더리움은 제한된 일밖에 할 수 없는 비트코인 스크립트 언어와 달리 Turing-complete한 VM을 제공하는 것이 큰 특징입니다. 전산 전공이 아닌 분들은 complete라는 단어의 어감 때문에 막연히 어떤 계산이든 할 수 있는 강력한 언어 혹은 VM이라고 생각할 가능성이 큰데, Turing-complete한 언어는 강력한만큼 보장되는 것이 없는 언어이기도 합니다.

대표적인 예로 halting problem이 있습니다. computability theory에서 halting problem은 어떤 프로그램의 종료 여부를 실행 전에 알 수 있는지를 묻는 것인데, Turing complete한 언어는 종료 여부가 undecidable합니다.

이 문제는 생각보다 심각합니다. 어떤 스마트 컨트랙트를 종료되지 않고 무한히 실행되는 상태로 만들 수 있으면 이더리움 네트워크에 대한 DoS(Denial of Service) 공격이 가능하게 되고, 이더리움 네트워크의 가용성을 해칠 수 있기 때문입니다.

그래서 이더리움은 가스라는 개념을 만들고 각 EVM 바이트코드에 가스비를 부과하여 트랜잭션에 지정된 가스가 다 소진되면 Out of Gas 에러를 던지는 cost semantic을 만들었습니다.

바꿔 말해, 이더리움이 실제로 원했던 것은 종료 여부를 알 수 없는 Turing-complete한 언어가 아니라 종료를 보장 혹은 증명할 수 있는 total program이었던 셈입니다. 다만, 이미 탄탄한 이론적 배경이 있는 total functional programming이 아닌 Gas라는 adhoc한 방법을 사용한 것이 차이점입니다.

하지만 단순 종료 보장 정도로는 충분치 않습니다. correctness가 생명인 스마트 컨트랙트의 특성상 “specification”과 “implementation”이 같다는 걸 증명할 방법이 필요하기 때문입니다. 하지만 자바스크립트를 차용한 스마크 컨트랙트 언어 Solidity에서는 이것도 쉽지 않습니다. 여기에 Gas를 소모하는 EVM의 cost semantic이 결합되면서 온갖 종류의 버그가 나올 수 있는 최악의 프로그래밍 환경이 되었습니다.

이 문제를 해결하는 방법으로 많은 스마트 컨트랙트 프로젝트가 코드 리뷰를 크라우드-소싱하는 방법을 택했습니다. GitHub에 소스 코드를 올리고 스마트 컨트랙트 코드에 대한 동료 리뷰, 감사를 요청하는 방식입니다. 이미 코드 감사만 전문으로 수행하는 컨설턴시가 생기고 있고, 코드 감사를 크라우드-소싱하는 코드 감사 플랫폼에 개발이 진행되고 있습니다. 많은 돈이 걸려있는 만큼 관련 산업도 빠르게 발전하고 있습니다.

지난 수십 년간 연구된 프로그래밍 언어 이론, 검증 기술 등이 이미 존재하지만, 안타깝게도 이더리움의 인기를 등에 업은 EVM 기반의 언어가 당분간 스마트 컨트랙트의 주류 언어가 될 가능성이 매우 높습니다. 이미 Solidity는 다른 언어에 비해 우월한 지위를 누리고 있습니다. 분산 어플리케이션을 만들고 ICO을 통해 수백, 수천억원을 조달할 수 있는 상황에서 아무도 주목하지 않은 인프라 기술에 투자할 회사는 드물기 때문에 이런 상황은 쉽게 바뀌지 않을 겁니다.

앞으로 많은 개발자들이 애증을 가질 또 하나의 언어가 생겼습니다.

소프트웨어 엔지니어가 말하는 좋은 ICO 판별법

지난 몇 달간 블록체인 기술을 공부하면서 여러 블록체인 ICO 기술 백서를 읽고 몇 개의 ICO에 직접 참여해 보기도 하였습니다.

좋은 ICO와 나쁜 ICO를 구분하는 방법에 정답은 없지만, 소프트웨어 엔지니어 입장에서 최소한의 기준을 이야기해 보려고 합니다.

기술 백서

단순히 기술 백서가 존재하는 것만으로 충분하지 않습니다. 기술 백서의 내용이 명확하고 검증 가능해야 합니다. 많은 프로젝트가 합의 알고리즘의 성능 개선을 말하면서 구현 없이 아이디어와 성능 목표치만 이야기하는데, 성능은 실제로 구현하고 숫자로 증명하지 않는 이상 아이디어만 가지고는 아무 것도 안 한거나 마찬가지입니다.

그럴 듯해 보이는 아이디어도 실제로 구현하면 예상치 못한 이유로 성능이 안 나오는 경우가 많고, 겨우 겨우 성능 목표를 달성해도 버그 투성이 코드가 되거나 유지 보수가 불가등 할 정도로 복잡한 코드가 나오는 경우도 비일비재합니다.

구현해서 검증하지 않은 성능 개선 방법을 대단한 기술처럼 포장해서 보여주는 백서는 그만큼 기술적으로 보여줄 게 없다는 반증입니다. 리눅스 토발즈의 명언을 다시 한 번 인용합니다. “Talk is cheap. Show me the code.”

소스 코드

GitHub에 소스 코드가 공유되어 있어야 합니다. 단순히 코드를 공개한 것만으로는 부족합니다. 소스 코드 공개 후에도 지속적인 활동이 있어야 합니다. 매일 새로운 코드, 이슈, PR이 올라오고 활발한 개발 활동이 일어나야 합니다. 그게 아니라면, 그저 소스 코드가 있다는 걸 보여주기 위한 요식 행위일 가능성이 큽니다.

다른 프로젝트를 포크해서 시작한 프로젝트의 경우, 포크 이전과 이후를 구분해서 포크 이후에 얼마나 수정이 이루어졌는지를 파악해야 합니다. 이미 ICO를 받은 국내의 한 프로젝트는 stellar-core를 포크하여 README만 조금 고친 후에 전혀 개발 활동이 이루어지지 않았습니다. 물론 내부적으로 개발이 이루어지고 있을 수도 있곘지만, 개발 활동을 숨겨야 한다면 뭔가 이유가 있는 거겠죠.

그리고 개발팀에 포함된 개발자들이 실제 개발 활동을 하는지도 확인해야 합니다. 개발팀 명단에 이름만 올려놓고 실제로 개발에 참여하지 않는 개발자들이 생각보다 많습니다. 반대로 상당한 양의 코드를 작성한 개발자가 개발팀에 포함되지 않은 경우도 있습니다. ICO용으로 PoC 코드만 외주를 줬을 가능성도 있고, 개발팀은 개발 역량이 없을 가능성도 의심해야 합니다.

많은 ICO 프로젝트가 ICO 자체를 수익 모델로 하고 있습니다. 그러다 보니 기술 백서, 소스 코드 공개도 요식 행위 수준에서 이루어지고 있습니다. 다른 사람은 몰라도 소프트웨어 엔지니어라면 기술 백서, 소스 코드 존재만 확인하지 말고, 그 내용까지 꼼꼼히 살펴보시고 기본이 튼튼한 프로젝트에 투자하시기 바랍니다.

How I learned Haskell

Happy lunar new year! Today I would like to share my experience of learning Haskell. It’s been a really long journey but a really wonderful one.

First Encounter

Back in 2000, I was an undergraduate student majoring in computer science. At that time, I was into system programming and enjoyed learning low-level implementation details of operating systems and system applications. My primary language was C and programmed all programming assignments in C. I was proud that I understood how my code was compiled and executed on the machine.

One day my friend told me about Haskell. I wasn’t interested in functional programming at that time, but I became curious because he enthusiastically persuaded me to learn Haskell. I ended up reading two books on Haskell.

Both books were really nice and I learned I could program in an entirely different way. However, I wasn’t sure if Haskell could solve the real-world problems I wanted to solve. So my interest in Haskell stopped there.

Dark age

My first job was mainly about porting and optimizing Java Virtual Machine for embedded systems. My company licensed Sun’s CDC JVM and I was responsible for maintaining it.

It was 2002 and Linux was still a luxury for embedded systems. RTOSes such as pSOS and VxWorks were popular on STBs and I ported JVM to these OSes. These RTOSes didn’t have distinctions between kernel and user space and an application was linked statically with the kernel and ran as a single (kernel) process application on the device.

The implication was profound. I had no safety guarantee provided by modern operating systems. A bug in an application could corrupt the kernel data and crash the entire system. Moreover, because there were dozens of threads competing for shared resources, race conditions and dead locks were a common place. It took hours or even days to find and fix a trivial bug.

The situation was much better when debugging an application written in Java. Thanks to the safety guarantee of Java, certain types of bugs were impossible. A Java program can’t corrupt memory and crash the system. Dead locks are reported systematically by the JVM. It was relatively fun to fix a bug in Java applications.

These experiences motivated me to find systematic ways of preventing bugs and led me to read Benjamin C. Pierce’s Types and Programming Languages. It was the best computer science text book I’ve ever read. I understood why types were important in statically typed functional languages like Haskell! If universities had used this book as a undergraduate PL textbook, many of the confusions and misunderstandings about dynamic vs static typing would have disappeared.

Stuck again

By fully understanding the merits of type systems, I started to learn Haskell in a different perspective. I read A Gentle Introduction To Haskell and many tutorials on monads. It wasn’t very hard to understand specific instances of monads such as ReaderWriterStateList and Maybe. But I couldn’t figure out how they were related. I managed to write simple applications in Haskell, but wasn’t confident that I could use Haskell productively because I couldn’t fully understand one of the core ideas of Haskell.

The Challenges of Multi-core Programming

In the meantime, I changed my career and founded a tech start-up in 2008. I built mobile web browsers for the embedded systems. I created a port of WebKit and hacked various components of WebKit to speed up the performance. The primary means for optimization was to leverage the multi-core CPU and GPU.

WebKit performs lots of tasks concurrently but it is mostly single-threaded. Loading a page does not benefit much from having a multi-core CPU. So I offloaded some tasks to separate threads but I only gained marginal performance benefits in exchange for largely increased complexity. I learned a lesson that I must pay very high costs of complexity to get small benefits of performance boost. Considering the already complex nature of WebKit, I ended up abandoning most of performance optimizations to keep the complexity under control.

While struggling to squeeze performance out of WebKit, I learned Haskell again to get some insights on parallel programming because Haskell was the only programming language which natively supported STM(Software Transaction Memory). Simon Marlow’s Parallel and Concurrent Programming in Haskell helped me understand how Haskell supported parallel and concurrent programming. Though I learned many valuable lessons from the book, I also felt that the lazy nature of Haskell didn’t go well with parallel programming.

Reunion

I have spent more than 10 years of my career on embedded systems and increasingly got frustrated with the tools available. So I decided to teach myself Haskell again and use it at work. This time I started to read classic papers on functional programming and Haskell.

Philip Wadler’s Monads for functional programming clicked my mind and I finally became enlightened. The paper is really well written, but I don’t think I could understand Monad just because I read the paper. Years of trials and errors were necessary to understand abstract concepts like monad. It was the most exciting moment of my long journey to Haskell.

Once I understood how I could learn abstractions, the rest was easy. Now I don’t get discouraged just because I don’t understand abstractions at first glance. It takes time and practice to understand abstract things. I also realized that monad was just the beginning. There exist many Haskell idioms that require other abstract concepts such as applicative functorarrowprofunctor and so on.

Here is the list of papers I found most enlightening when learning Haskell. I also recommend you read any paper with “Functional Pearl” attached to it.

Back to Real-World

I was confident that Haskell was a good programming language and I was looking for opportunities to use Haskell in production. Bryan O’Sullivan, Don Stewart, and John Goerzen’s Real World Haskell was a good reference in this direction. It showed how I could use Haskell to do my daily work such as networking, system programming, databases and web programming.

Finally, I started to read real-world Haskell code available on the hackage and realized that the Haskell I know was different from the Haskell that is actually used. Real world Haskell uses lots of GHC extensions which makes me feel it is an entirely different language. A typical Haskell module starts with:

{-# LANGUAGE CPP                        #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE ConstraintKinds            #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE FunctionalDependencies     #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE TupleSections              #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE DeriveDataTypeable         #-}

It seems sticking to Haskell 98 or 2000 does not have much practical sense because many Haskell packages already use many GHC extensions. So I learned them too. 24 Days of GHC Extensions was a really great way of learning this topic.

I like the approach of Yesod Web Framework Book which explains the GHC extensions used in the library before explaining how to use the library. This is often the first step to learn a new library for many Haskell programmers. For example, you can’t use Servant unless you understand DataKinds and TypeOperators. So I encourage Haskell library authors to write more about the GHC extensions they use.

I also found that some packages are essential to use Haskell in practice.

  • String type has problems. You need either text or bytestring for efficient string data processing.
  • Lazy IO looks nice, but does not work well in practice. To process streaming data properly you need either pipes or conduit.
  • You will need a custom monad or monad transformer for your application sooner or later. Either mtl or transformers is required.
  • JSON is a really universal data exchange format these days. aeson will help you here.
  • QuickCheck is a bonus you get from using Haskell!

Haskell in production

I founded a small Haskell shop this year and started to use Haskell in production. I realized that using Haskell in production was, surprisingly, easier than learning Haskell. It took me more than 10 years to learn Haskell, but I felt confident that I could use Haskell in production only after a few months of experiments.

There is one thing I would like to emphasize. Using Haskell does not mean that you must understand all the dependencies you use. Haskell programmers tend to care much about the implementation details of their dependencies because Haskell makes it so easy to understand the meaning of programs with types and equational reasoning. But in my opinion, this is a blessed curse.

That’s not how civilization works. You can drive a car without understanding how engines work. Python or JavaScript programmers do not care about the implementation details of their dependencies because it is simply impossible. Haskell is no exception. Time and money are limited. Please don’t spend too much time on understanding things. Spend more time on building things. Be practical.

Fortunately, some library authors provide a high-level overview of their library. Type-level Web APIs with Servant is a great example. It explains the core concepts and the implementation techniques of the library without involving accidental complexities of implementation details. I would love to see more papers like this.

Tools and Libraries

Stackage and the Stack are indispensable tools for using Haskell in production. All the hard work of FP Complete gave me confidence that Haskell was production-ready. The Haskell ecosystem is not small anymore. There are multiple competing web frameworks such as YesodScottySnapHappstack and Servant. Qualities of these packages are all good.

If you write web servers in Haskell, all the packages you need such as web servers, web frameworks, logging packages, database drivers are already available. I use Servantpersistent and esqueleto for my server. So far, everything works fine.

Haskell Community

Haskell community is relatively small compared to other major languages, but I am often surprised by the quality of feedback I get from the community. Haskell is a great language, but the community is even greater. That’s the biggest reason why I love programming in Haskell.

My journey to Haskell is still going on.

하스켈 학교

1990년대 후반부터 많은 대학들이 프로그래밍 입문에 코스에 자바를 채택하기 시작하였습니다. 2001년 텍사스 오스틴 대학도 프로그래밍 입문 코스에 기존에 사용하던 언어인 Haskell을 자바로 대체하는 방안을 검토 중이었습니다. 당시 이 대학에 재직 중이던 저명한 컴퓨터 과학자 다익스트라(Edsger W. Dijkstra)는 공개 서한을 통해 이런 움직임을 비판하며 Haskell의 장점을 강조하였습니다.

하지만 텍사스 오스틴 대학은 결국 상업적으로 성공한 자바를 채택하였고, 이후 MIT를 비록한 많은 대학들이 Scheme, ML, Haskell 같은 함수 언어 대신 Java나 Python을 가르치기 시작하여 그 추세는 지금까지도 이어지고 있습니다.

저도 대학에서 C, C++, Java를 배웠고 졸업 후에도 10년 이상을 C, C++, JavaScript를 사용해 코드를 작성하였습니다. 자바가상머신(JVM), 웹킷(WebKit) 등의 프로젝트에 참여하며 복잡한 소프트웨어를 작성하기 위한 객체지향프로그래밍 방법론, 디자인 패턴, 테스트 케이스 작성 방법 등도 배웠습니다.

하지만 동시에 한계도 느꼈습니다. 어제 작성한 코드가 오늘 작성한 코드와 크게 다르지 않고 오늘 발생한 버그가 지난 번에 해결한 버그와 크게 다르지 않았습니다. 프로그래밍은 점점 기계적인 활동이 되었고 흥미도 잃게 되었습니다. 코드 재활용이나 쉬운 유지보수는 영원히 달성할 수 없는 이상향으로만 느껴지기도 했습니다.

그러던 와중에 지푸라기라도 잡는 심정으로 학부 때 잠깐 공부했던 Haskell을 다시 꺼내서 공부하기 시작하였습니다. 그리고 프로그래밍에 대해 다시 생각하게 되었습니다. 함수 합성(function composition)을 통한 코드 재활용, 지연 연산(lazy evaluation)을 통한 모듈화, 펑터(functor)와 모나드(monad) 등 카테고리 이론을 이용한 추상화 등은 그간 프로그래밍에 대해 가졌던 여러 고정관념들을 허물고 익숙한 코드를 낯선 관점에서 새롭게 바라볼 수 있는 시각을 제공해 주었습니다.

흔히 Haskell은 배우기 어려운 언어라고 합니다. 저도 꽤 오랜 시간 많은 시행착오를 하며 람다 대수, 타입 이론, 카테고리 이론 등 Haskel 이해에 필요한 이론들을 공부하였습니다. 하지만 돌이켜 생각해보면 Haskell은 단순히 어려운 언어가 아니라 “다른” 언어였다는 생각이 듭니다. 특히, 이미 C++이나 Java 같은 언어로 오랜 시간 프로그래밍을 해왔다면 Haskell이 더욱 다르고 낯설게 느껴질 수 있습니다.

국내에는 Haskell 관련 자료가 별로 없습니다. Haskell을 주된 프로그래밍 언어로 사용하는 회사도 없습니다. 그래서 Haskell은 배우기 어려운 언어라는 악순환이 반복되고 있습니다. 하지만 많은 분들이 Haskell을 배우고 싶어합니다. 대체 무엇이 있는지 알 수 없지만, 뭔가 흥미로운 게 있을 것 같다는 기대 때문입니다. 이 사이트가 Haskell 세상으로 탐험을 나서는 분들에게 미약하나마 작은 도움이 되어드릴 수 있으면 좋겠습니다.

하스켈 “Hello World” 프로그램

프로그래밍 언어 입문서는 화면에 “Hello World”를 출력하는 프로그램을 보여주는 것으로 설명을 시작하는 전통이 있습니다. 이 전통에 따라 하스켈 “Hello World” 프로그램을 소개하겠습니다.

main = putStrLn "Hello World!"

이 프로그램을 실행하려면 먼저 코드를 복사해서 HelloWorld.hs 파일을 만듭니다. 그런 다음에 runhaskell 명령을 이용해 프로그램을 실행합니다. runhaskell은 GHC를 설치하면 함께 설치되는 프로그램으로 인자로 주어진 하스켈 프로그램을 곧바로 실행시켜 주기 때문에 하스켈을 스크립트 언어로 쓸 수 있게 해줍니다.

$ runhaskell HelloWorld.hs
Hello World!

ghc 컴파일러를 이용해서 실행 파일을 생성하는 것도 가능합니다.

$ ghc --make HelloWorld.hs

컴파일 후에 HelloWorld(윈도에서는 HelloWorld.exe)라는 실행 파일이 생성된 것을 확인할 수 있습니다. 실행하면 runhaskell HelloWorld.hs를 했을 때와 마찬가지로 화면에 “Hello World!”를 출력하는 것을 보실 수 있습니다.

$ ./HelloWorld
Hello World!

추가로 같은 디렉토리에 HelloWorld.hiHelloWorld.o 파일도 생성된 것을 볼 수 있는데, .hs 파일은 각 모듈을 따로 컴파일(separate compilation)을 하기 위한 인터페이스 파일이고, .o 파일은 해당 모듈을 컴파일한 오브젝트 파일입니다. 이에 대해서는 하스켈 모듈 시스템을 설명할 때 좀 더 자세히 설명하겠습니다.

이제 코드 내용을 살펴보겠습니다. 우선 main은 하스켈 코드가 실행되는 시작점(entry point)입니다. 모든 하스켈 프로그램은 main에서 시작해서 main으로 끝나게 됩니다.

ghci을 이용해서 main을 타입을 확인하면 다음과 같습니다. 참고로 :l 명령을 이용하면 외부 파일을 ghci로 로드할 수 있습니다. HelloWorld를 인자로 주면 HelloWorld.hs파일을 컴파일하여 ghci로 로드합니다.

$ ghci
> :l HelloWorld
[1 of 1] Compiling Main             ( HelloWorld.hs, interpreted )
Ok, modules loaded: Main.
> :t main
main :: IO ()

main의 타입은 IO ()라는 것을 알 수 있습니다. 여기서 () 타입은 유닛(unit) 타입으로()가 유일한 값입니다. 주로 유용한 리턴 값이 없을 때 사용하는 타입인데, C++이나 Java의 void와 유사한 역할을 합니다. 다만, 하스켈의 () 타입은 void와 달리 리턴 값이 없는 것이 아니라 () 타입의 유일한 값인 ()을 리턴합니다. 타입과 값 모두 ()이라는 심볼을 사용하는 것에 유의하시기 바랍니다.

> () -- 여기서 ()는 값입니다.
()
> :t () -- 여기서 ()는 타입입니다.
() :: ()

main의 리턴 값은 ()이 아니라 IO ()인데, 여기서 IO는 사이드 이펙트가 있는 함수라는 뜻입니다. 쉽게 말해, 화면에 입출력을 할 수 있는 함수는 ()가 아닌 IO () 타입으로 표시합니다. IO 타입에 대한 자세한 설명은 입출력(IO)에 대한 설명을 할 때 자세히 이야기하겠습니다.

putStrLn의 타입은 String -> IO ()입니다. 타입에 ->가 포함되어 있으므로 함수임을 알 수 있습니다. 인자 타입은 String이고 리턴 타입은 IO ()입니다. putStrLn은 문자열을 하나 받아서 화면에 출력하는 함수입니다. 화면에 문자열을 출력하는 것도 사이드 이펙트이기 때문에 리턴 타입이 ()가 아닌 IO ()임을 알 수 있습니다.

> :t putStrLn
putStrLn :: String -> IO ()

putStrLn "Hello World!"의 타입은 String -> IO () 타입 함수에 String 인자를 호출하였기 때문에 putStrLn의 리턴 타입인 IO ()가 됩니다. main의 타입인 IO ()putStrLn "Hello World!"의 타입이 같기 때문에 이 프로그램을 컴파일 에러 없이 정상적으로 컴파일이 됩니다.

하스켈 시작하기

설치

하스켈을 설치하는 가장 쉬운 방법은 하스켈 플랫폼(Haskell Platform)을 이용하는 것입니다. 하스켓 플랫폼은 하스켈 컴파일러 GHC, 빌드 시스템 Cabal, 자주 사용되는 35개의 하스켈 패키지를 한 번에 설치할 수 있게 도와주는 하스켈 배포판이라고 생각하시면 됩니다. Mac OS X, Windows, Linux를 모두 지원합니다.

GHC와 GHCi

ghc는 하스켈 컴파일러입니다. 다음과 같이 --version 옵션을 주고 실행했을 때 버전 정보가 출력되면 정상적으로 설치된 것입니다.

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.10.3

추가로 ghci라는 REPL도 함께 설치되었을 겁니다. 여기서 REPL은 Read–eval–print loop의 약자로 프로그램 코드를 입력하면 수행 결과를 화면에 보여주는 프로그램을 통칭하는 말입니다.

쉘에서 ghci 명령을 입력하면 다음과 같이 Prelude>라는 프롬프트가 뜨는 것을 확인하실 수 있습니다.

$ ghci
GHCi, version 7.10.3: http://www.haskell.org/ghc/  😕 for help
Prelude>

프롬프트에 간단한 하스켈 프로그램 1+2를 입력하면 계산 결과인 3이 화면에 표시됩니다.

Prelude> 1 + 2
3
Prelude>

이제 하스켈 프로그래밍을 시작할 준비가 완료되었습니다.