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/
정성태

... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
12972정성태2/16/20228489Windows: 200. Intel CPU의 내장 그래픽 GPU가 작업 관리자에 없다면? [4]
12971정성태2/15/202210125.NET Framework: 1157. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 muxing.c 예제 포팅 [7]파일 다운로드2
12970정성태2/15/20228243.NET Framework: 1156. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 h264 형식의 파일로 쓰기 [1]파일 다운로드1
12969정성태2/14/20226699개발 환경 구성: 638. Visual Studio의 Connection Manager 기능(Remote SSH 관리)을 위한 명령행 도구 - 두 번째 이야기파일 다운로드1
12968정성태2/14/20226939오류 유형: 794. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for '...'.
12967정성태2/14/20227306VC++: 153. Visual C++ - C99 표준의 Compund Literals 빌드 방법 [4]
12966정성태2/13/20227141.NET Framework: 1155. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 yuv420p + rawvideo 형식의 파일로 쓰기파일 다운로드1
12965정성태2/13/20227146.NET Framework: 1154. "Hanja Hangul Project v1.01 (파이썬)"의 C# 버전
12964정성태2/11/20227458.NET Framework: 1153. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 avio_reading.c 예제 포팅파일 다운로드1
12963정성태2/11/20228195.NET Framework: 1152. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리 (저해상도 현상 해결)파일 다운로드1
12962정성태2/9/20228033오류 유형: 793. 마이크로소프트 스토어 - 제품이 존재하지 않습니다. 재고가 없는 것일 수 있습니다.
12961정성태2/8/20228182.NET Framework: 1151. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 프레임의 크기 및 포맷 변경 예제(scaling_video.c) [7]파일 다운로드1
12960정성태2/8/20227606개발 환경 구성: 637. ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 세 번째 이야기
12959정성태2/7/20228306.NET Framework: 1150. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 두 번째 이야기 [2]파일 다운로드1
12958정성태2/6/20228334.NET Framework: 1149. C# - ffmpeg(FFmpeg.AutoGen) - 비디오 프레임 디코딩 [2]파일 다운로드1
12957정성태2/6/20227928개발 환경 구성: 636. ffmpeg.exe를 이용해 planar 포맷의 데이터를 packed 형식으로 변환하는 방법? [2]
12956정성태2/4/20227158.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [2]파일 다운로드1
12955정성태2/4/20226628개발 환경 구성: 635. 비주얼 스튜디오에서 실행하던 ASP.NET Core (.NET Framework) 응용 프로그램을 명령행에서 실행하는 방법 (2)
12954정성태2/4/20226468VS.NET IDE: 173. 비주얼 스튜디오 - Output 창에 색상이 지정된 출력 결과가 "[39m[22m" 식의 문자로 나오는 문제
12953정성태2/2/20226670Linux: 48. Windows 11 + WSL 우분투 GUI 환경에서 한글 출력
12952정성태2/2/20227191.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)파일 다운로드1
12951정성태2/2/20227137.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)파일 다운로드1
12950정성태2/1/20226695.NET Framework: 1146. .NET 6에 추가되지 않은 Generic Math (예: INumber<T>)
12949정성태2/1/20226563.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비파일 다운로드1
12948정성태1/30/20226699.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력파일 다운로드1
12947정성태1/30/20227858개발 환경 구성: 634. ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...