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)
11333정성태10/17/201720004오류 유형: 422. 윈도우 업데이트 - Code 9C48 Windows update encountered an unknown error.
11332정성태10/17/201720772디버깅 기술: 104. .NET Profiler + 디버거 연결 + .NET Exceptions = cpu high
11331정성태10/16/201718841디버깅 기술: 103. windbg - .NET 4.0 이상의 환경에서 모든 DLL에 대한 심벌 파일을 로드하는 파이썬 스크립트
11330정성태10/16/201717948디버깅 기술: 102. windbg - .NET 4.0 이상의 환경에서 DLL의 심벌 파일 로드 방법 [1]
11329정성태10/15/201722678.NET Framework: 693. C# - 오피스 엑셀 97-2003 .xls 파일에 대해 32비트/64비트 상관없이 접근 방법파일 다운로드1
11328정성태10/15/201725729.NET Framework: 692. C# - 하나의 바이너리로 환경에 맞게 32비트/64비트 EXE를 실행하는 방법파일 다운로드1
11327정성태10/15/201719456.NET Framework: 691. AssemblyName을 .csproj에서 바꾼 경우 빌드 오류 발생하는 문제파일 다운로드1
11326정성태10/15/201719533.NET Framework: 690. coreclr 소스코드로 알아보는 .NET 4.0의 모듈 로딩 함수 [1]
11325정성태10/14/201720291.NET Framework: 689. CLR 4.0 환경에서 DLL 모듈의 로드 주소(Base address) 알아내는 방법
11324정성태10/13/201721777디버깅 기술: 101. windbg - "*** WARNING: Unable to verify checksum for" 경고 없애는 방법
11322정성태10/13/201719773디버깅 기술: 100. windbg - .NET 4.0 응용 프로그램의 Main 메서드에 Breakpoint 걸기
11321정성태10/11/201721226.NET Framework: 688. NGen 모듈과 .NET Profiler
11320정성태10/11/201722067.NET Framework: 687. COR_PRF_USE_PROFILE_IMAGES 옵션과 NGen의 "profiler-enhanced images" [1]
11319정성태10/11/201729733.NET Framework: 686. C# - string 배열을 담은 구조체를 직렬화하는 방법
11318정성태10/7/201721966VS.NET IDE: 122. 비주얼 스튜디오에서 관리자 권한을 요구하는 C# 콘솔 프로그램 제작 [1]
11317정성태10/4/201727468VC++: 120. std::copy 등의 함수 사용 시 _SCL_SECURE_NO_WARNINGS 에러 발생
11316정성태9/30/201724918디버깅 기술: 99. (닷넷) 프로세스(EXE)에 디버거가 연결되어 있는지 아는 방법 [4]
11315정성태9/29/201741388기타: 68. "시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지" 구매하신 분들을 위한 C# 7.0/7.1 추가 문법 PDF [8]
11314정성태9/28/201723143디버깅 기술: 98. windbg - 덤프 파일로부터 닷넷 버전 확인하는 방법
11313정성태9/25/201720775디버깅 기술: 97. windbg - 메모리 덤프로부터 DateTime 형식의 값을 알아내는 방법파일 다운로드1
11312정성태9/25/201724134.NET Framework: 685. C# - 구조체(값 형식)의 필드를 리플렉션을 이용해 값을 바꾸는 방법파일 다운로드1
11311정성태9/20/201717429.NET Framework: 684. System.Diagnostics.Process 객체의 명시적인 해제 권장
11310정성태9/19/201721874.NET Framework: 683. WPF의 Window 객체를 생성했는데 GC 수집 대상이 안 되는 이유 [3]
11309정성태9/13/201719270개발 환경 구성: 335. Octave의 명령 창에서 실행한 결과를 복사하는 방법
11308정성태9/13/201720971VS.NET IDE: 121. 비주얼 스튜디오에서 일부 텍스트 파일을 무조건 메모장으로만 여는 문제파일 다운로드1
11307정성태9/13/201723438오류 유형: 421. System.Runtime.InteropServices.SEHException - 0x80004005
... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...