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)
12946정성태1/28/20226323오류 유형: 792. .NET Core - 로컬 개발 중에 docker 호스팅으로 바꾸는 경우 SQL 서버 접근 방법
12945정성태1/28/20226618오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
12944정성태1/28/20229075.NET Framework: 1143. C# - Entity Framework Core 6 개요
12943정성태1/27/20227897.NET Framework: 1142. .NET 5+로 포팅 시 플랫폼 호환성 경고 메시지(SYSLIB0006, SYSLIB0011, CA1416)파일 다운로드1
12942정성태1/27/20228151.NET Framework: 1141. XmlSerializer와 Dictionary 타입파일 다운로드1
12941정성태1/26/20229556오류 유형: 790. AKS/k8s - pod 상태가 Pending으로 지속되는 경우
12940정성태1/26/20226894오류 유형: 789. AKS에서 hpa에 따른 autoscale 기능이 동작하지 않는다면?
12939정성태1/25/20227636.NET Framework: 1140. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP3 오디오 파일 인코딩/디코딩하는 예제파일 다운로드1
12938정성태1/24/202210000개발 환경 구성: 633. Docker Desktop + k8s 환경에서 local 이미지를 사용하는 방법
12937정성태1/24/20227829.NET Framework: 1139. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 오디오(mp2) 인코딩하는 예제(encode_audio.c) [2]파일 다운로드1
12936정성태1/22/20227770.NET Framework: 1138. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 멀티미디어 파일의 메타데이터를 보여주는 예제(metadata.c)파일 다운로드1
12935정성태1/22/20227999.NET Framework: 1137. ffmpeg의 파일 해시 예제(ffhash.c)를 C#으로 포팅파일 다운로드1
12934정성태1/22/20227575오류 유형: 788. Warning C6262 Function uses '65564' bytes of stack: exceeds /analyze:stacksize '16384'. Consider moving some data to heap. [2]
12933정성태1/21/20228087.NET Framework: 1136. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)파일 다운로드1
12932정성태1/20/20228521.NET Framework: 1135. C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c) [2]파일 다운로드1
12931정성태1/20/20226549개발 환경 구성: 632. ASP.NET Core 프로젝트를 AKS/k8s에 올리는 과정
12930정성태1/19/20227290개발 환경 구성: 631. AKS/k8s의 Volume에 파일 복사하는 방법
12929정성태1/19/20227088개발 환경 구성: 630. AKS/k8s의 Pod에 Volume 연결하는 방법
12928정성태1/18/20227182개발 환경 구성: 629. AKS/Kubernetes에서 호스팅 중인 pod에 shell(/bin/bash)로 진입하는 방법
12927정성태1/18/20226962개발 환경 구성: 628. AKS 환경에 응용 프로그램 배포 방법
12926정성태1/17/20227502오류 유형: 787. AKS - pod 배포 시 ErrImagePull/ImagePullBackOff 오류
12925정성태1/17/20227542개발 환경 구성: 627. AKS의 준비 단계 - ACR(Azure Container Registry)에 docker 이미지 배포
12924정성태1/15/20229065.NET Framework: 1134. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) [2]파일 다운로드1
12923정성태1/15/20227956개발 환경 구성: 626. ffmpeg.exe를 사용해 비디오 파일을 MPEG1 포맷으로 변경하는 방법
12922정성태1/14/20227040개발 환경 구성: 625. AKS - Azure Kubernetes Service 생성 및 SLO/SLA 변경 방법
12921정성태1/14/20225941개발 환경 구성: 624. Docker Desktop에서 별도 서버에 설치한 docker registry에 이미지 올리는 방법
... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...