Welcome to F#(5) - 아주 조금씩 심화되는 탐색전.

F# 2009. 4. 20. 08:50 Posted by 알 수 없는 사용자

- 벌써 다섯번째네?


네, 맞습니다;; 벌써 다섯번째 포스트입니다. 그런데 영~ 포스트는 나아질 기미를 보이진 않는군요. 모든게 제 잘못입니다;; 계속 노력중이니 좋게 봐주시길 부탁드립니다.^^ 오늘은 지난번에 했던 거 보다 조금 더 복잡한 코드를 가지고 이야기를 해보려고 합니다. 그리고 그 코드를 바탕으로 C#과의 비교를 통해서 좀 더 의미있는 것들을 이야기 해보고 싶습니다. 읽어보시고 의미가 없다면 따뜻한 피드백으로 질타를...;;

 

- 알았으니까 예제부터 보자!

open System.IO

let rec getAllFiles dir =
    List.append 
      (dir |> Directory.GetFiles |> Seq.to_list) 
      (dir |> Directory.GetDirectories |> Seq.map getAllFiles |> Seq.concat |> Seq.to_list)
 
//여기서는 그렇게 가져온 파일리스트를 '.'을 기준으로 짤라낸다.
let splitedAllFiles = getAllFiles @"C:\ATI" |> List.map (String.split ['.'])
 
//여기서는 각 파일을 돌면서, 확장자가 사용자가 원하는 확장자인거만 걸러내고, 개수를 리턴한다.
let Count ext = splitedAllFiles |> List.filter (fun file -> if file.Length = 2 then file.[1] = ext else false) |> List.length

 

그리고 위의 예제를 F#인터렉티브에서 확인해본 결과입니다.(PowerPack.dll을 로딩하는 부분빼곤 전부 Alt+Enter를 이용했습니다.)


 > #r "FSharp.PowerPack.dll";;

--> Referenced 'C:\Program Files (x86)\FSharp-1.9.6.2\bin\FSharp.PowerPack.dll'

>

 

val getAllFiles : string -> string list
val splitedAllFiles : string list list
val Count : string -> int


> Count "txt";;
val it : int = 129
>

 

위 코드는 명시된 디렉토리를 모두 훑어서 거기에 있는 파일들의 경로를 가져오고, 그 파일들 중에 사용자가 명시한 확장자를 가진 파일이 몇개나 되는지를 출력하는 코드로 Expert F#에 나온코드를 조금 수정하고 덧붙인 것입니다. 일단 이 예제를 가지고 C#과의 비교를 통해서, 함수형언어로서의 F#이 가지는 장점에 대해 조금알아보고자 합니다. 그럼 우선 위의 코드에 대해서 알아보겠습니다.

 

- 첫번째는?

우선, 위의 코드에서는 두가지의 자료구조가 사용되었는데요, Seq과 List가 그것입니다. Seq는 C#에서의 IEnumerable<>타입을 의미하구요, List는 List<>을 의미합니다. 특성도 비슷합니다. 조금 더 익숙하실 C#을 통해 먼저 알아보죠. LINQ에서 보면, 쿼리를 날렸을때,  IEnumerable<>타입이 리턴되는데요, 이 IEnumerable<>로 리턴된 시퀀스는  아직 데이터를 가지고 있는게 아닙니다. 실제로 IEnumerable<>의 각요소에 접근해야 할때가 되서야 LINQ의 쿼리가 실행됩니다. 예제코드를 먼저 보시져~!

 

class Program
    {
        static void Main(string[] args)
        {
            int[] list1 = new int[] { 98, 34, 5, 134, 64, 57, 99, 320, 21, 55, 62, 39 };
            IEnumerable<int> queriedList1 = list1.Where(num => num <= 100);

            foreach (int num in queriedList1)
            {
                Console.WriteLine(num);
            }

            list1[0] = 198; //98 -> 198
            list1[1] = 134; //34 -> 134

            Console.WriteLine("---------------second call-------------");

            foreach (int num in queriedList1)
            {
                Console.WriteLine(num);
            }
        }
    }

 

위코드를 보시면, int형 배열에 대해서 100보다 작은 수만 골라서 쿼리를 날려서 IEnumerable<int>형으로 리턴을 받습니다. 그리고 처음에 모든 수를 돌면서 출력을 하고, 그 다음에 쿼리의 대상이었던 배열의 값중에 처음 두개를 100보다 크게 바꾼뒤에 다시 시퀀스돌면서 숫자를 출력하고 있습니다. 여기서 주목하실건, 쿼리를 두번 날린게 아니라 그저  IEnumerable<int>시퀀스 안의 요소를 한번더 돌면서 출력한 것 뿐이라는 겁니다. 결과를 보시죠.

 

 

위의 결과를 보시면, 두번째 foreach에서는 바꿔준 값들이 나오지 않는 걸 알 수 있습니다. 즉, 쿼리는 만들어지긴 하되 그 실행시기는 직접 요청될때로 연기(Deferred)된 것입니다. 이런 IEnumerable의 특성으로 인해, 변경된 내용을 가져오는데 쿼리를 두번날릴 필요가 없이 그저 전체요소를 한번더 돌기만 하면 자연스럽게 변경된 내용을 읽어오는게 되는 것입니다. 이와는 반대로 List는 즉시 모든 값을 읽어옵니다. 그래서 위의 코드에서,

 

IEnumerable<int> queriedList1 = list1.Where(num => num <= 100);

위코드를 아래와 같이 수정하고,

List<int> queriedList1 = list1.Where(num => num <= 100).ToList();


다시 예제를 돌려보면, 아래와 같은 결과가 나오는 걸 확인할 수 있습니다.

 

 

즉, 이미 수행시점에서 모든 요소를 미리(Immediately)가져 왔기 때문에, 원본값이 수정된 뒤에서 쿼리로 가져온 값에는 변화가 없는 것이죠. 이런 특성은 F#에서도 여전히 유효합니다.

 

> seq { 1 .. 100000000 };;
val it : seq<int> = seq [1; 2; 3; 4; ...] 
 

F# 인터렉티브에서는 위와같은 결과가 나옵니다. 즉, 1부터 1억까지의 숫자의 시퀀스를 선언하면, 모든 숫자가 다 즉시 만들어지는게 아니라는 거죠. 결과에서 보듯이 1부터 4까지만 나온걸 보실 수 있습니다. 즉, F#인터렉티브가 네번째 요소까지만 미리 읽어오도록 하고 있는거지요. 그래서 주의 하셔야 할점도 있습니다. List는 미리다 읽어오므로 메모리에 다 올라갈 수 있을지를 미리 생각해봐야 하고, Seq는 미리 다 읽어오는게 아니므로 읽어오기 전에 데이터변경이 되면 다른 결과가 나올 수 있습니다. 이 부분을 유념해야 합니다.

 

- 두번째는?


그럼 맨 처음에 파일들을 읽어오는 코드를 계속 보기로 하죠. 위에서 "|>"라는 기호가 보이는데 이 건 뭘까요? 뭐 리눅스같은데서 shell을 좀 다뤄보셨거나 하면 pipe겠구나 하는건 금방 아실 수 있겠죠.^^;; 이건 정방향 파이프 연산자(forward pipe operator)라고 하는데요, |> 앞쪽의 리턴값을 |> 뒤쪽의 메서드에 넘겨주는 역할을 합니다. 즉 위의 예제를 보자면, Directory.GetFiles의 인자로 dir를 넘겨주고, 그 결과를 Seq.to_list 의 인자로 넘겨주는 거죠. 굉장히 심플하게 중간 결과를 어디에 담아둘지 고민하지 않고 함수들을 연결할 수 있게 도와주는 연산자입니다.

아, 그리고 여기서 주목하실 부분이 있다면, 닷넷프레임워크의 메서드인 Directory.GetFiles나 Directory.GetDirectories를 F#에서도 First-class function으로 사용할 수 있습니다. 이런 부분이 F#의 진정한 강점이 아닌가 싶네요. 그리고 기존의 C#이나 VB.NET을 이용하시던 분들이 좀더 자연스럽게 F#이용해서 효율적인 함수형 프로그램을 작성할 수 있게 해주는 원동력이기도 하구요.


- First-class function?


Fisrt-class function이란 함수형언어의 특징중 하나로서, 함수가 다른 함수의 파라미터로 들어갈 수 있고, 연산결과로 리턴될 수 있으며 명령형언어에서는 추가적으로 변수에 대입가능한(자바스크립트를 떠올리시면 됩니다.) 함수라는 걸 의미합니다. Higher-order function은 함수를 인자로 받거나 함수를 결과로 리턴하는 함수를 의미하구요.

 

그 다음줄도 역시 Directory.GetDiretories에 dir을 넘겨줘서 현재 디렉토리의 하위 디렉토리들의 시퀄스를 가져오고 그걸 Seq.map에 넘겨줘서 각 디렉토리별로 getAllFiles를 실행해서 모든 하위 디렉토리의 파일경로를 가져오도록 합니다. 그리고 그 결과를 Seq.concat을 통해서 하나의 시퀀스로 합치구요, 그 다음에 그 시퀀스를 list로 변환합니다. 그리고 모든 결과를 하나의 list로 합쳐서 리턴합니다.

 

그리고 splitedAllFiles에서는 그렇게 만든 리스트의 모든 요소를 대상으로 '.'을 기준으로 파일경로와 확장자로 분리합니다. 그리고 마지막 Count함수에서는 그 결과를 List.filter함수에게 넘겨줘서 확장자가 사용자가 명시한 확장자와 일치하는 것만 추려서 하나의 리스트로 만든뒤, 그 리스트의 길이를 리턴합니다. 이렇게 해서 해당 디렉토리와 그 하위 디렉토리에서 특정 확장자를 가지는 파일의 개수를 읽어오는 프로그램이 완성되는 것입니다.

 

 

- C#과 비교한대매?


이번 포스트에서는 F#의 예제코드를 통해서 F#의 Seq, List의 특성을 알아봤고, |>연산자를 통해서 함수들을 쉽게 연결하는 것을 보았습니다. 다음 포스트에서는 이 F#의 코드와 동일한 C#코드를 통해서 비교를 조금 해보고자 합니다. 글의 내용이 여전히 만족스럽진 않지만, 저도 부족한 머리 박아가면서 쓰는 글이니 좀 봐주시구요;; 따뜻한 피드백 부탁드립니다.

 

- 참고자료

1. Expert F#, Don Syme, Adam Granicz, Antonio Cisternino, APRESS

2. Pro LINQ: Language Integrated Query in C# 2008, Joseph C. Rattz, Jr. , APRESS.

3. Programming Language Pragmatics 2nd Edition, Michael L. Scott, Morgan Kaufmann Publishers