Welcome to F#(2) - 두번째 만남.

F# 2009. 4. 8. 23:40 Posted by 알 수 없는 사용자

-애프터
첫번째 만남이 끝난뒤에, 어떻게 애프터를 이어나가야 할지 무처이나 고민했습니다. 첫만남이야 가볍게 서로의 얼굴도 보고, 성격도 맛보는 정도에서 끝난다지만 애프터에서는 서로에 대해서 천천히 자세히 알아가야 할텐데, 갑자기 참여중인 프로젝트의 상황이 악화되서 시간이 잘 나지 않고, 이해부족과 실력부족이 겹쳐서 어떻게 글을 계속 써나가야할지 고민이 많았습니다. 그러다가 직장동료에게 물어봤습니다.

"만약에 튜토리얼을 쓴다면, 어떻게 쓰는게 좋을까?"
"글쎄, 나는 그런게 좋던데. 왜 작은 예제에서 시작해서 하나씩 추가되어 가면서 설명해주는 그런거 있잖아."

이거다 싶었습니다. 그래서 제 내공이 받쳐줄지는 모르겠지만, 최대한 예제를 통해서 F#과의 애프터를 진행해볼까 합니다. 언제나 그렇듯이 제 글에 어색하거나 잘못된 내용이 있을수 있으니 언제나 따뜻한 피드백으로 격려해주시기 바랍니다.(말씀드렸듯이 차가운 피드백은 바로 반사-_-) 그럼 일단 애프터 장소로 가서 F#을 좀 더 만나보도록 하죠~.

-F#에서는 타입을 안쓰는거 같더라?
일단 F#에서 타입을 결정하는데 쓰이는 타입유추(Type Inference)에 대해서 알아보겠습니다. 타입유추는 F#에서만 쓰이는 개념은 아닙니다만, 저 같이 함수형언어에 친숙하지 않은 경우에는 생소함을 느끼게 됩니다. 우선 아래와 같은 예제를 작성해서 아래 두줄을 드래그 해서 F# Interactive로 보내보죠. F# Interactive를 띄우는 방법과 F# Interactive로 보내는 방법은 잊지 않으셨죠? 잊으셨다면, 이전 포스트를 참조해주세요~.

#light

let count = 1
printfn "%d" count


실행결과에 대해서 이야기 하기전에 #light에 대한 설명을 조금 드리자면, 지난 포스트에서 말씀드린 것 같이 이 지시어는 F#에서 좀더 간략한 문법을 사용할 수 있도록 해줍니다. 그런데, 이런 키워드를 명시적으로 꼭 선언해야 하는 이유는 다음과 같습니다. F#은 OCaml으로 부터 영감을 받아서 설계되고 핵심적인 부분중에 일부분을 공유하고 있씁니다. 그리고 OCaml의 코드를 컴파일할 수 있는 기능을 갖추고 있는데, 간단한 OCaml코드의 경우는 수정없이 컴파일 가능하게 되어있습니다. 그래서 그 경우를 위해서 #light라고 명시해줘야만 F#의 간단한 문법이 적용되는 것입니다.

그러면, 다시 소스코드로 넘어가서 그 소스의 결과는 아래와 같습니다.

val count : int
1
 
즉, 한줄씩 평가 돼서 그 결과가 나오는 건데요, 첫번째 줄은 count라는 value를 선언했고 그 타입은 int라는 말입니다. 그리고 그 다음줄은 count의 값을 출력한 결과인거죠. 눈치 빠른 분이라면, 아마 여기서 뭔가를 눈치채셨을 겁니다. 타입을 정해주지 않았는데, count의 타입이 int라는걸 문맥을 통해 유추해냈습니다. 그리고 아래의 printfn의 출력패턴을 보면 "%d", 즉 C와 동일하게 정수형값을 출력하겠다는 말인데요, 앞의 평가식에서 count가 정수형이라는 걸 유추해냈기 때문에 에러없이 1을 출력하고 프로그램을 종료합니다. 이런걸 타입 유추라고 부릅니다.(원문으로는 Type Inference인데, inference에 대한 딱히 다른 용어를 찾지 못해서 일단 유추라고 하겠습니다.-_-)

타입유추는 코드를 분석해서 제약사항들을 모아서 이루어집니다. 여기서 제약사항이란, 매개변수가 가질 수 있는 타입을 명시해줌으로써 제약을 가하는 개념인데요, 문맥에서 타입을 유추할때 +,-,*,/같은 경우는 기본적으로 매개변수를 int타입으로 유추됩니다. 그리고 아래의 예제들을 F# Interative에 평가해보죠.

let printInt x = printfn "%d" x

let printString x = printfn "%s" x

그러면, 아래와 같은 결과가 나오죠.

val printInt : int -> unit

val printString : string -> unit

즉, 문맥상 정수형을 출력하니까 x는 int형이 된거고, 문맥상 스트링을 출력하니깐 x는 스트링으로 유추된 거죠. 그리고 아래의 예제를 더 보시죠.(그리고 참고로 unit은 많은 분들이 기대하시는 것 처럼 unsigned int는 아니구요, 정확하게 같진 않지만 void를 나타내는 F#의 타입입니다.-_-)

let plusplus x = (x + x)
let plusplus2Times (x) = plusplus x + plusplus x
-----결과----
val plusplus : int -> int
val plusplus2Times : int -> int

그리고 아래와 같이 바꿔서 다시 평가해보죠.

let plusplus x = (x + x)
let plusplus2Times (x:float) = plusplus x + plusplus x
-----결과----
val plusplus : float -> float
val plusplus2Times : float -> float

역시 사용되는 문액에 따라 plusplus를 호출하는 plusplus2Times에서 int형 x를 넘겨주면, plusplus의 매개변수도 int타입이 되고, float을 사용하겠다고 타입에 제약을 정해주면(즉, 매개변수 x가 될 수 있는 타입에 명시적으로 제약을 주는 것) 그에 따라서 plusplus에 넘어가는 매개변수와 리턴타입역시 그에 따라 float타입으로 유추되는 것을 볼 수 있죠. 즉, F#에서는 타입을 명시해줄 수도 있지만, 이런 작업을 직접 해주지 않으면 기본적으로는 타입유추를 통해 타입을 알아내고 적용한다는 겁니다.

-타입유추라는거 왠지 불안해보이는데?
이런 생각을 해볼 수 있습니다. '과연 이렇게 컴파일러가 타입을 추측해주는게 무슨 장점이 있는거냐?' 이 포스트에서 관련된 내용을 많이 찾아볼 수 있었습니다. 저자는 타입유추의 광팬이로군요. 언제든지 C#같은 언어에서 제공하는 var타입이나 익명메서드등을 사용해서 자기 대신 컴파일러가 타입에 대한 문제를 다루도록 한답니다. 즉, '타입유추는 타입안정성을 해치지 않고 그저 컴파일러가 대신 타입을 찾을뿐이다', '타입을 일일이 적지 안아도 되니깐 리팩토링시에도 편리하다', '타이핑을 줄일 수 있다'랍니다. 그리고 반론도 만만치 않습니다. 아주 긴 댓글로 서로 의견을 주고 받고 있군요. 즉, 한마디로 좋다 나쁘다를 정할 수 없는 취향의 문제인것 같기도 합니다.(우주 끝날때까지도 결론이 안 날문제들이 바로 취향에 관련된 문제죠-_-)

-F#은 어떤 타입이야?
그리고 타입이야기가 어쩌다 새어나왔으니, 타입을 한번 적어보겠습니다.
 타입  예  설명
 int  int  그냥 32비트 정수입니다.
 type option  int option, option<int>  선언된 타입의 값이 있거나, 혹은 값이 없는 걸 의미하는 None을 가집니다. Optional parameter와 비슷한 개념입니다.
 type list  int list, list<int>  선언된 타입의 immutable한 값들의 linked list입니다. 리스트의 각 값들은 [5;2;88]처럼 같은 타입을 가져야 합니다.
 type1 -> type2  int -> string  함수타입입니다. 즉, type1을 받아서 결과값으로 type2를 리턴한다는 거죠.
 type1 * ... * typeN  int * string  한쌍(a pair), 두쌍 그리고 그 이상의 타입들의 조합이 가능한 튜플타입입니다. (1, "3")같은 경우 말이죠.
 type []  int[]  배열타입이죠. 1차원이고, 고정된 크기의 mutable한 값들의 모음입니다.
 unit  unit  하나의 값을 나타내는 ()을 나타내는데, 명령형 언어의 void와 비슷한 의미입니다.
 'a, 'b  'a, 'b, 'Key, 'Value  제네릭하게 어떤 타입이든지 올 수 있는 변수타입입니다.

Expert F#에 나오는 예제를 마지막으로 보겠습니다.

 String.split [' '] "hello world"

 String.split ['a';'e';'i';'o';'u'] "hello world"

일단 위의 코드를 F# Interactive에 보내서 평가해보면 "The value, constructor, namespace or type 'split' is not defined. A construct with this name was found in FSharp.PowerPack.dll,......"이런 에러가 뜹니다. 즉, String클래스에 split이라는 값이나, 생성자, 네임스페이스 혹은 타입이 존재하지 않는데, FSharp.PowerPack.dll에서 발견이 되었다는 친절한 메세지 입니다. CTP버전으로 오면서 많은 기능이 FSharp.PowerPack.dll로 옮겨졌기 때문입니다. String은 Microsoft.FSharp.Core.String을 참조하는데요 Microsoft.FSharp안에 있는 Core,Collection,Text,String같은 네임스페이스는 String.split이나 open String같이 한 단어로 참조가 가능하고 합니다. 이럴땐, FSharp.PowerPack을 참조추가해줘야겠죠. 참조추가후에 open 지시어를 통해서 해당 네임스페이스의 내용을 알아내야 하는데, F# Code에선 기본적인 Core나, Operators, Collections같은 네임스페이스는 암시적으로 open을 한다고 합니다. 그리고 비주얼 스튜디오가 아닌 F# Interactive에서 참조추가를 하려면, 아래 처럼하면 됩니다.

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

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


그리고 다시 F# Interactive로 보내서 평가해보면 아래와 같은 결과나 나옵니다.

 val it : string list = ["hello"; "world"]
>
val it : string list = ["h"; "ll"; " w"; "rld"]

이 예제에서 ' ', 'a'는 그냥 문자고, "hello world"같은건 문자열, ['a';'e';'i';'o';'u']는 문자의 리스트, ["hello"; "world"]는 문자열의 리스트라는 걸 알 수 있습니다.

-애프터의 만족도는?
변명이지만, 시간도 조금은 부족했고, 아는것도 부족하다보니 포스트의 내용이 썩 만족스럽지는 못한 것 같습니다. 애프터가 만족스럽지 못하셨다면, 제탓이 가장크겠군요-_-. 그래도 마음에 드신분이 있다면, 몇번 애프터를 더 해보고 손이라도 잡아봐야 겠죠. 다음번엔 이어서 기본적인 타입에 대해서 좀 더 알아보고, .NET공원에서 놀이기구를 몇개 타보는 걸로 해볼 생각입니다. 그리고 이번포스트에서 제대로 조사하지 못한 부분역시 조사해볼 생각입니다. 데이트장소는 고전적이긴 하지만 놀이공원도 괜찮죠. 그럼 도움이 되는 분들이 있길 바라면서 다음 포스트에서 뵙겠습니다.

-참고자료
1. Expert F#, Don Syme, Adam Granicz, Antonio Cisternino, APRESS.
2. http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec2.aspx#_Toc207785764
3. http://blogs.msdn.com/jaredpar/archive/2008/09/09/when-to-use-type-inference.aspx