Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

clojure와 C#을 통해 이해하는 Sequence와 Vector 형식의 차이점

아래와 같은 질문이 있군요. ^^

clojure에서 list와 vector의 차이점은?
; http://www.slipp.net/questions/194

어찌 보면, 아래의 글도 역시 그 차이점을 이해하지 못하는 것과 관련이 있습니다.

Java의 함수형 프로그래밍이 생각보다 위험하지 않은 이유
; http://justhackem.wordpress.com/2014/06/19/why-functional-programming-in-java-is-not-dangerous/

그래도 C# 언어를 공부한 분이라면 이 차이를 쉽게 이해할 수 있습니다. 즉, 아래와 같이 쉽게 설명할 수 있기 때문입니다. ^^

C# 언어상으로 보면 Sequence는 IEnumerable이고, Vector는 IList 자료형

사실 제가 쓴 책에서도 580 페이지의 "7.4 yield return/break" 구문을 설명하면서 이에 대한 예를 들고 있습니다. 이것 때문에 여러분들이 책을 구매하시기에는 좀 그러하니... 이 글에서 다시 설명해 보겠습니다. ^^

제가 책에서 든 예제는 '자연수 집합'을 프로그램에서 표현하는 것입니다.

집합 N = { 1, 2, 3, 4, ... }

이는 무한히 반복되기 때문에 코드로 표현하기가 애매합니다. 가령 다음과 같이 만들면,

static void Main(string[] args)
{
    uint [] integers = GetIntegers(10);
    for (int i = 0; i < integers.Length; i ++)
    {
        Console.WriteLine(integers[i]);
    }
}

static uint[] GetIntegers(uint end)
{
    List<uint> list = new List<uint>();
    for (uint i = 1; i < end; i++)
    {
        list.Add(i);
    }

    return list.ToArray();
}

이 함수가 자연수를 표현한다고 보기에는 무리가 있습니다. 시작 지점부터 끝 지점까지를 명시해야 하고 심지어 이렇게 반환받은 자연수 배열을 호출 측에서 모두 꺼내쓴다고 장담할 수도 없습니다. 다시 말하면, 어떤 경우에는 쓸데없이 메모리만 할당해서 반환한 요소도 존재할 수 있는 것입니다.

바로 여기에 IEnumerable을 도입하면 자연스럽게 자연수 표현이 가능해 집니다.

static void Main(string[] args)
{
    foreach (int elem in Integers())
    {
        Console.WriteLine(elem);
    }
}

static IEnumerable<uint> Integers()
{
    uint start = 1;
    while (true)
    {
        yield return start++;
    }
}

위의 프로그램을 돌리면 여러분이 컴퓨터를 끄는 순간까지 무한한 자연수의 집합을 출력합니다. 물론 uint의 한계값이 0xffffffff이므로 진정한 한계를 없애려면 BigInteger를 사용하시면 됩니다. (그 이후에는 시간과 메모리가 장벽이고!)

한 가지 알아두어야 할 것은, Vector형은 IEnumerable로 쉽게 변환이 가능합니다. 가령 C#의 경우 List 자료 구조가 IEnumerable도 함께 구현하고 있습니다. 왜냐하면 순차적(sequential)으로 열람할 수 있다면 IEnumerable 구현이 가능하기 때문입니다.

public class List<T> : IList<T>, ICollection<T>, 
    IEnumerable<T>, IEnumerable,  IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>

clojure의 경우에도 vector 자료형을 seq 함수를 통해 sequence로 쉽게 변환할 수 있습니다.

user=> (def x [1 2 3])
#'user/x

user=> (seq x)
(1 2 3)

표현을 달리해서 말하자면, 특정 요소가 있을 때 vector는 random access가 가능한 자료형이고, sequence는 순차적으로 그 요소가 나올 때까지 처음부터 열거해야 하는 자료형입니다.




그럼 이해가 되셨나요? ^^ 좀 더 이해를 돕기 위해 "Java의 함수형 프로그래밍이 생각보다 위험하지 않은 이유" 글에서 나온 코드를 C#과 clojure 버전으로 구현해 보겠습니다.

(take 25 (squares-of (integers)))

기본 clojure에는 integers와 squares-of 함수가 없기 때문에 이를 만들어 보면 됩니다. 우선 integers를 정의해 볼까요?

(defn integers [] (iterate inc 1))

오... 멋지군요. ^^ C#의 yield return을 이용한 구문보다 훨씬 간단합니다. 이 함수를 clojure REPL에서 실행해 보면 무한히 반복되는 자연수 출력을 확인할 수 있습니다.

user=> (integers)
(1 2 3 4 5 ...[REPL에서는 integers 함수의 평가 결과가 출력되므로, 결과적으로 무한히 자연수 출력]...)

clojure의 integers 함수를 C#으로 구현하는 방법은 위에서 이미 살펴봤으므로 생략합니다.

그다음 squares-of 정의를 구현해 볼텐데요.

user=> (defn squares-of [x] (* x x))
#'user/squares-of

위의 구현이 올바를까요? clojure는 타입 추론 기능이 있어서 (* x x)의 코드로 인해 x 인자의 형식을 단일 값으로 판단합니다. 실제로 squares-of의 반환값 형식을 통해 이것이 sequence가 아님을 알 수 있습니다.

user=> (class (squares-of 5))
System.Int64

따라서 우리가 원래 의도했던 "(squares-of (integers))" 코드를 수행하면 다음과 같이 예외가 발생합니다.

user=> (squares-of (integers))
InvalidCastException Unable to cast object of type 'clojure.lang.Cons' to type 'System.IConvertible'.  System.Convert.ToInt64 (:0)

왜냐하면, integers 함수는 Sequence(clojure.lang.Cons)를 반환하는 반면, squares-of 함수는 입력 인자로 정수값 하나를 기대하기 때문에 그로 인한 불일치가 발생하는 것입니다. 이 오류를 해결하려면 squares-of 함수가 Sequence를 입력받아 처리하는 구조로 바뀌어야 합니다. 그럼 이렇게 구현하면 되겠지요. ^^

(defn squares-of [x] (map #(* % %) x))

또는

(defn squares-of [x] (map (fn[w] (* w w)) x))

또는

(defn square [x] (* x x))
(defn squares-of [x] (map square x))

이제 integers와 squares-of를 결합하면 무한한 자연수를 열거하면서 그것의 각각을 제곱해주는 결과가 나옵니다.

user=> (defn integers [] (iterate inc 1))
#'user/integers

user=> (defn squares-of [x] (map #(* % %) x))
#'user/squares-of

user=> (squares-of (integers))    ; 이 함수 실행 이후 식이 평가되면서 무한 자연수 제곱 결과 출력
(1 4 9 16 25 ...[생략]...
...) 

이것을 C#으로 구현하면서 비교해 볼까요? 일단 integers는 구현했으니 squares-of 함수를 구현해 보면 이렇습니다.

static void Main(string[] args)
{
    foreach (int elem in SquaresOf(Integers()))
    {
        Console.WriteLine(elem);
    }
}

static IEnumerable<uint> SquaresOf(IEnumerable<uint> elems)
{
    foreach (uint elem in elems)
    {
        yield return (elem * elem);
    }
}

C# 확장 메서드 기능을 이용하면 이런 식의 호출도 가능합니다.

static void Main(string[] args)
{
    foreach (int elem in Integers().SquaresOf())
    {
        Console.WriteLine(elem);
    }
}

static IEnumerable<uint> SquaresOf(this IEnumerable<uint> elems)
{
    foreach (uint elem in elems)
    {
        yield return (elem * elem);
    }
}

사실, 일반적인 프로그램 세계에서 무한을 반환하는 함수는 버그에 가까운 기능입니다. 그래서 대개는 무한을 표현한다 해도 다음과 같이 일정 수의 요소만 취하는 기능이 꼭 필요하게 됩니다.

user=> (take 25 (squares-of (integers)))
(1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 400 441 484 529 576 625)

즉, 자연수를 제곱한 sequence 중에서 처음부터 25개의 요소만 가져오는 것입니다. C#도 Take 메서드를 제공하기 때문에 별다른 코드없이 유사하게 구현할 수 있습니다.

foreach (int elem in Integers().SquaresOf().Take(25))
{
    Console.WriteLine(elem);
}

sequece, vector 형식은 clojure뿐만 아니라 F#과 같은 함수형 언어에도 (이름만 다를 뿐) 동일하게 나오므로 한번 익혀두시면 다른 함수형 언어의 자료 구조를 이해하는 데 도움이 되실 것입니다. ^^




참고로 clojure의 경우 integers를 함수가 아닌 var에 바인딩하는 것도 가능합니다. 그런 경우에는 함수 평가를 하는 것이 아니기 때문에 integers 호출에 괄호를 사용할 필요가 없어 다음과 같이 바뀝니다.

(def integers (iterate inc 1))
(defn squares-of [x] (map #(* % %) x))
(take 5 (squares-of integers))

또는, squares-of를 sequence를 받지 않고 단일 요소로 받도록 하는 경우 map 함수를 이용해 우회해서 동일한 효과를 얻을 수 있습니다.

(def integers (iterate inc 1))
(defn squares-of [x] (* x x))
(map squares-of (take 5 integers))

이런 건 그냥... 표현의 방식일 뿐이니.

사실 절차형 프로그래밍에서 vector와 sequence의 구분이 크게 중요하지 않은 것은 그런 거 몰라도 어렵지 않게 프로그램할 수 있기 때문입니다. 가령, IEnumerable을 쓰지 않고 그냥 다음과 같이 무한 자연수를 표현해도 무방하지요.

static void Main(string[] args)
{
    Integers int32 = new Integers();
    while (true)
    {
        Console.WriteLine(int32.GetNext());
    }
}
        
class Integers
{
    uint i = 1;

    public uint GetNext()
    {
        return i ++;
    }
}

결과적으로 봤을 때 다를 것이 없습니다! 괜히 어려운 거 좋아하는 사람들이 IEnumerable을 들이대는 거라고 생각해도 좋습니다. ^^

(마지막으로... 이런 글 쓰면 제가 clojure 좀 할 줄 안다고 생각하실 수도 있는데, 완전 초보입니다!)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 9/3/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2021-09-03 09시53분
Stream large content from ASP.NET Core to avoid OutOfMemoryException
; https://blog.elmah.io/stream-large-content-from-asp-net-core-to-avoid-outofmemoryexception/
정성태

... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...
NoWriterDateCnt.TitleFile(s)
11299정성태9/9/201719715개발 환경 구성: 330. Hyper-V VM의 Internal Network를 Private 유형으로 만드는 방법
11298정성태9/8/201723053VC++: 119. EnumProcesses / EnumProcessModules API 사용 시 주의점 [1]
11297정성태9/8/201719684디버깅 기술: 96. windbg - 풀 덤프에 포함된 모든 닷넷 모듈을 파일로 저장하는 방법
11296정성태9/8/201722889웹: 36. Edge - "이 웹 사이트는 이전 기술에서 실행되며 Internet Explorer에서만 작동합니다." 끄는 방법
11295정성태9/7/201720291디버깅 기술: 95. Windbg - .foreach 사용법
11294정성태9/4/201720036개발 환경 구성: 329. 마이크로소프트의 CoreCLR 프로파일러 예제 빌드 방법 [1]
11293정성태9/4/201720579개발 환경 구성: 328. Visual Studio(devenv.exe)를 배치 파일(.bat)을 통해 실행하는 방법
11292정성태9/4/201718850오류 유형: 419. Cannot connect to WMI provider - Invalid class [0x80041010]
11291정성태9/3/201720703개발 환경 구성: 327. 아파치 서버 2.4를 위한 mod_aspdotnet 마이그레이션
11290정성태9/3/201723927개발 환경 구성: 326. 아파치 서버에서 ASP.NET을 실행하는 mod_aspdotnet 모듈 [2]
11289정성태9/3/201721585개발 환경 구성: 325. GAC에 어셈블리 등록을 위해 gacutil.exe을 사용하는 경우 주의 사항
11288정성태9/3/201718272개발 환경 구성: 324. 윈도우용 XAMPP의 아파치 서버 구성 방법
11287정성태9/1/201727540.NET Framework: 680. C# - 작업자(Worker) 스레드와 UI 스레드 [11]
11286정성태8/28/201714845기타: 67. App Privacy Policy
11285정성태8/28/201723447.NET Framework: 679. C# - 개인 키 보안의 SFTP를 이용한 파일 업로드파일 다운로드1
11284정성태8/27/201721495.NET Framework: 678. 데스크톱 윈도우 응용 프로그램에서 UWP 라이브러리를 이용한 비디오 장치 열람하는 방법 [1]파일 다운로드1
11283정성태8/27/201717257오류 유형: 418. CSS3117: @font-face failed cross-origin request. Resource access is restricted.
11282정성태8/26/201719704Math: 22. 행렬로 바라보는 피보나치 수열
11281정성태8/26/201721489.NET Framework: 677. Visual Studio 2017 - NuGet 패키지를 직접 참조하는 PackageReference 지원 [2]
11280정성태8/24/201718546디버깅 기술: 94. windbg - 풀 덤프에 포함된 모든 모듈을 파일로 저장하는 방법
11279정성태8/23/201730175.NET Framework: 676. C# Thread가 Running 상태인지 아는 방법
11278정성태8/23/201718291오류 유형: 417. TFS - Warning - Unable to refresh ... because you have a pending edit. [1]
11277정성태8/23/201719542오류 유형: 416. msbuild - error MSB4062: The "TransformXml" task could not be loaded from the assembly
11276정성태8/23/201723891.NET Framework: 675. C# - (파일) 확장자와 연결된 실행 파일 경로 찾기 [2]파일 다운로드1
11275정성태8/23/201732841개발 환경 구성: 323. Visual Studio 설치 없이 빌드 환경 구성 - Visual Studio 2017용 Build Tools [1]
11274정성태8/22/201719412.NET Framework: 674. Thread 타입의 Suspend/Resume/Join 사용 관련 예외 처리
... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...