C#의 LINQ는 버전 3.0에 처음 도입된 기능으로, 그 이전까지 단지 잘 베낀 자바에 불과했던 C#을 일거에 함수형 프로그래밍 언어로 도약하게 만든 가히 혁명적인 시도라 부를만 하다. LINQ 이후의 C#은 이 언어가 C/C++에 기반을 두고 있는 게 맞나 싶을 정도로 코딩 스타일이 많이 달라졌다. LINQ를 이용하면 기존 명령형 프로그래밍으로 복잡하고 지저분하게 짜야만 했던 코드가 작성하기 쉽고 이해하기 쉬운 함수형 프로그래밍 스타일로 바뀐다.

이 글에서는 LINQ의 기본 아이디어를 알아보고 이것이 F#의 함수형 스타일과는 어떤 차이점과 유사점이 있는지 비교해 보도록 하겠다.

확장 메쏘드

LINQ의 문법은 확장 메쏘드라는 아주 간단하고도 기발한 아이디어에서 출발하는데, 이 확장 메쏘드는 더 이상 메쏘드를 추가할 수 없는 기존 타입에 메쏘드가 추가된 것처럼 보이게 만드는 기능이다. 방법은 정적 클래스 안에 정적 메쏘드를 정의하면서 첫번째 파라미터 앞에 this 키워드를 붙여주면 된다. 즉, 아래처럼 하면:

static class Extension {
    public static int KiloBytes(this int number) {
        return number * 1024;
    }
}

12.KiloBytes() == 12288 // true

마치 int 타입에 KiloBytes()란 인스턴스 메쏘드가 있는 것처럼 코드를 짤 수 있게 된다.

확장 메쏘드는 기본적으로 정적 메쏘드이기 때문에 아래처럼 기존 문법대로 써도 된다:

Console.WriteLine(Extension.KiloBytes(12));

C# 함수형 스타일 로또 생성기

지난번에 만들었던 함수형 스타일 로또 생성기를 다시 가져왔다:

IEnumerable<TResult> RunInfinite<TResult>(Func<TResult> func) {
    while (true)
        yield return func();
}

IEnumerable<int> LottoNumbers_Functional(int minValue, int maxValue, int count) {
    var rand = new Random((int)DateTime.Now.Ticks);
    return RunInfinite(() => rand.Next(minValue, maxValue + 1))
           .Distinct()
           .Take(count);
}

여기서 Distinct()Take()System.Linq.Enumerable 정적 클래스에 들어 있는 확장 메쏘드들이다. 기존 문법대로 코드를 다시 써보면

RunInfinite(() => rand.Next(minValue, maxValue + 1))
.Distinct()

Enumerable.Distinct(
    RunInfinite(() => rand.Next(minValue, maxValue + 1))
)

로 바뀌게 되고, 뒤의 .Take(count)까지 바꾸면

Enumerable.Take(
    Enumerable.Distinct(
        RunInfinite(() => rand.Next(minValue, maxValue + 1))
    ),
    count
)

가 된다. LINQ 스타일로 썼을 때는 가장 마지막에 실행되는 것처럼 보였던 Take()가 사실은 가장 먼저 실행됨을 알 수 있다. RunInfinite() 메쏘드가 무한대로 실행되지 않는 이유도 그 때문이다. Take()가 내부 루프를 통해 Distinct()를 반복 호출하고, Distinct()는 다시 내부 루프를 통해 RunInfinite()를 반복 호출하기 때문에 RunInfinite()는 실제로 6번 + 중복제거된 회수만큼만 실행되는 것이다.

F# 함수형 스타일 로또 생성기

F# 버전의 로또 생성기는 이미 지난번에 다룬 바 있다. 파이프라인 연산자 |>를 이용해서 아래처럼 만들었던 코드를

open System

let lottoNumbers minValue maxValue count =
    let rand = Random(int DateTime.Now.Ticks)
    Seq.initInfinite (fun _ -> rand.Next(minValue, maxValue + 1))
    |> Seq.distinct
    |> Seq.take count

|>를 이용하지 않은 형태로 고치면

open System

let lottoNumbers minValue maxValue count =
    let rand = Random(int DateTime.Now.Ticks)
    Seq.take count (
        Seq.distinct (
            Seq.initInfinite (fun _ -> rand.Next(minValue, maxValue + 1))
        )
    )

처럼 된다. C# LINQ 스타일과 개념적으로 동일하다는 것을 알 수 있다.

C# 확장 메쏘드와 F# |>의 차이

F#은 단순히 연산자를 정의하는 것만으로 새로운 기능을 만들어낸 반면, C#에서는 언어 자체를 수정해서 확장 메쏘드라는 문법을 새로 만들어야 했다. 그리고 F#은 다른 파라미터들은 그대로 두고 맨 뒤에 있는 파라미터만 |> 앞으로 이동하는데 비해 C#은 첫번째 파라미터부터 한칸씩 앞으로 당겨 쓰는 방식이다.

C#에 비해 F#의 구현이 간단한 이유는 기본적으로 커링과 부분 함수 적용이 언어 차원에서 지원되기 때문이다. F#은 함수 파라미터 리스트를 임의의 위치에서 잘라 새로운 함수를 만드는 것이 아주 쉽기 때문에 언어의 유연성과 표현력이 그만큼 높다고 하겠다.