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

1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13511정성태1/4/20242180닷넷: 2193. C# - ASP.NET Web Application + OpenAPI(Swashbuckle) 스펙 제공
13510정성태1/3/20242109닷넷: 2192. C# - 특정 실행 파일이 있는지 확인하는 방법 (Linux)
13509정성태1/3/20242149오류 유형: 887. .NET Core 2 이하의 프로젝트에서 System.Runtime.CompilerServices.Unsafe doesn't support netcoreapp2.0.
13508정성태1/3/20242179오류 유형: 886. ORA-28000: the account is locked
13507정성태1/2/20242840닷넷: 2191. C# - IPGlobalProperties를 이용해 netstat처럼 사용 중인 Socket 목록 구하는 방법파일 다운로드1
13506정성태12/29/20232398닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/20232957닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/20232530닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/20232392Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232497닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232302개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232371디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20233056닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232459오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232443Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232409Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232555Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232659닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232338개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232266Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232390개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232173개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232104오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232409개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232221개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232102오류 유형: 883. dotnet build/restore - error : Root element is missing
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...