타입 시스템

한 줄 광고: 보다 강력한 타입 시스템을 가진 언어를 찾으신다면, 하스켈 학교에서 하스켈을 공부해보시기 바랍니다!

타입 시스템(type system)은 프로그래밍 언어를 연구하는 학자들에게는 아주 중요한 연구 분야입니다. Curry–Howard Isomorphism에 따라 컴퓨터 프로그램과 수학적인 증명 사이에 직접적인 연관 관계가 있음에 밝혀졌기 때문입니다. 극단적인 예로, Coq의 경우, 프로그램을 작성하는 행위 그 자체가 해당 프로그램이 나타내는 정리를 증명하는 일이 됩니다.

하지만 일반적인 개발자 입장에서 타임 시스템은 애증의 대상입니다. 타입 시스템은 개발자가 올바른 프로그램을 작성하기 위한 도우미 역할을 해야 하지만, 반대로 개발자가 컴파일러를 위해 열심히 타임을 적어줘야 하는 경우가 더 많았기 때문입니다. 일례로, C++의 템플릿(template)이나 Java의 제네릭(Generic)을 사용해 보신 분들은 코드보다 타입 어노테이션이 더 길어지는 상황을 경험해 보셨을 거라 생각합니다.

많은 개발자들이 Java를 버리고, Python, Ruby 같은 동적 타이핑(dynamic typing)하는 스크립트 언어로 갈아탄 것도 이런 타입 시스템의 불편함에 기인한 바가 큽니다. 안타까운 점은 이런 불편함이 타입 시스템 자체의 한계에서 나온 것이 아니라, C++이나 Java 타입 시스템 구현의 문제임에도 불구하고 타입 시스템에 대해 부정적인 인식이 널리 퍼졌다는 사실입니다.

정적 타이핑과 동적 타이핑 논쟁은 그 자체로 사실 큰 의미가 없습니다. 흔히 정적 타이핑과 동적 타이핑으로 여러 프로그래밍 언어를 분류하지만, 실제로는 다양한 스펙트럼이 존재하기 때문입니다. 예를 들어, 정적 타이핑하는 Java 같은 언어에도 컴파일 타임이 아닌 런타임에만 확인 가능한 속성들이 존재하고, 이는 타임 시스템 설계 시 여러 트레이드오프를 고려해서 선택하는 사항이기 때문입니다. 또한 이론적으로 동적 타이핑이란 컴파일 타임에 타입이 하나뿐인 정적 타이핑 시스템으로 이해할 수 있습니다.

C# 같은 언어는 이런 경계를 더 흐리게 만듭니다. C# 4.0에 포함된 dynamic은 정적 타이핑하는 언어에 동적 타이핑을 포함시켰기 때문입니다. 예를 들어, 아래 코드는 스크립트 언어들과 마찬가지라 ExampleClassexampleMethod1가 실제로 존재하는지 컴파일 타임이 아닌 런타임 확인하게 됩니다.

    dynamic dynamic_ec = new ExampleClass();
    // The following line is not identified as an error by the
    // compiler, but it causes a run-time exception.
    dynamic_ec.exampleMethod1(10, 4);

정적 타이핑하는 언어를 사용할 때 가장 불편한 점 중 하나가 타입 어노테이션을 일일이 적어줘야 한다는 점인데, 현대적인 언어들은 타입 추론(type inference)를 통해 이런 불편함을 줄이기 위해 노력하고 있습니다. C#도 C# 3.0에 Implicitly typed local variables을 도입해서 아래와 같이 컴파일러가 타입을 바로 알 수 있으면 개발자가 굳이 타입을 적어주지 않아도 되게 바뀌었습니다.

// i is compiled as an int
var i = 5;

// s is compiled as a string
var s = "Hello";

// a is compiled as int[]
var a = new[] { 0, 1, 2 };

// expr is compiled as IEnumerable<Customer>
// or perhaps IQueryable<Customer>
var expr =
    from c in customers
    where c.City == "London"
    select c;

// anon is compiled as an anonymous type
var anon = new { Name = "Terry", Age = 34 };

// list is compiled as List<int>;
var list = new List<int>();

하지만 이런 노력에도 불구하고 타입 시스템에 대한 부정적인 의견은 더 있습니다. 대표적인 예로, 타입 시스템이 개발자의 표현의 자유를 제한한다는 비판이 있습니다. 실제로는 전혀 문제 없는 코드임에도 불구하고, 타임 시스템의 한계 때문에 문제가 있는 코드로 인식하고 거절하는 경우를 말합니다. 예를 들어, 함수 f와 인자 u, v를 받아서 uv를 인자로 함수 f를 각각 호출한 다음 결과를 튜플로 리턴하는 코드를 JavaScript로 작성하면 다음과 같습니다.

function applyTuple2(f, u, v)
{
  var ru = f(u);
  var rv = f(v);
  return [ru, rv];
}

그리고 다음과 같이 f에는 id 함수를, u, v는 각각 0과 ‘a’를 인자로 해서 applyTuple2 함수를 호출하면 결과는 예상대로 [0, ‘a’]가 리턴되게 됩니다.

function id(x)
{
  return x;
}

var i = 0;
var a = 'a';

var r2 = applyTuple2(id, i, a);

재미있는 사실은 비교적 간단해 보이는 이 코드를 C#으로 표현할 방법이 없다는 점입니다. 위 코드를 C#으로 그대로 옮기면 다음과 같습니다.

class MainClass
{
    public static Tuple<U, V> ApplyTuple2<T, U, V>(Func<T, T> f, U u, V v)
    {
        U ru = f(u);
        V rv = f(v);
        return new Tuple<U, V>(ru, rv);
    }

    public static T Id<T>(T x)
    {
        return x;
    }

    public static void Main(string[] args)
    {
        int i = 0;
        char a = 'a';

        var r2 = ApplyTuple2(Id, i, a);
    }
}

안타깝게도 이 코드는 컴파일이 안 됩니다. C# 타입 시스템의 한계 때문에 f 함수는 Func[int, int] 혹은 Func[char, char] 둘 중 하나만 될 수 있기 때문입니다. 이 경우 f 함수를 polymorphic하게 각각 u, v 인자에 대해 호출하고 싶은 게 개발자의 의도이지만, C# 타입 시스템에서 이 의도를 표현할 방법이 존재하지 않습니다.

그런데 이게 이론적으로 불가능한 건 아닙니다. 타임 시스템이 이런 요구사항을 표현하기 위해서는 Rank-2 Polymorphism을 지원해야 합니다. 안타깝지만, 대중에게 널리 알려진 프로그래밍 언어 중에 Rank-2 Polymorphism을 지원하는 언어는 Haskell밖에 없습니다.

소프트웨어 엔지니어링이라는 게 다 그렇지만, 결국 트레이드오프의 문제가 됩니다. 타입 시스템도 개발자의 편의, 컴파일 타임에 검증할 수 있는 속성의 범위, 구현의 복잡도, 성능 등 여러 요소를 고려해서 설계할 수밖에 없고 어떤 쪽에 무게를 두느냐에 따라서 Python, Ruby 같은 동적 타이핑 언어부터, C#, Java 같은 대중적인 정적 타이핑 언어, Scala, F#, Haskell 같은 강력한 타입 시스템을 가진 언어까지 여러 스펙트럼이 나오게 됩니다.

많은 개발자들이 Scala, F#, Haskell과 같은 강력한 타입 시스템을 가진 언어들을 경험해보지 않고 C++, C#, Java의 타입 시스템의 정적 타입 시스템의 한계라고 생각하는 것 같습니다. “정적 타이핑 vs 동적 타이핑”이라는 틀에서 벗어나서 좀 더 종합적인 관점에서 여러 타입 시스템을 비교해 볼 것을 권합니다.

Advertisements