대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 선언형과 새로운 추상화 대상으로부터 함수형 프로그래밍 F#
등록일 조회수 6086
첨부파일  

선언형과 새로운 추상화 대상으로부터

함수형 프로그래밍 F#



최근 들어 Java와 C#, C++ 그리고 Python 등 다양한 언어들이 함수형 프로그래밍 패러다임을 도입하기 위해 노력하고 있다. 또한 함수형 언어 Haskell, F#, Scala 그리고 Clojure 등도 개발자로부터 많은 관심을 받고 있다. 이러한 관심은 함수형 프로그래밍의 추상화 대상으로부터 비롯된 것이다. 이번 시간에는 함수형 프로그래밍의 대표적인 특징인 선언형에 대해 살펴본다.



2002년에 C#이 처음 발표될 때 Java 진영 개발자들은 “C#은 Java의 짝퉁이다”라고 비판하곤 했다. 그러나 최근 C#과 Java는 언어적으로 상당히 다른 행보를 보이고 있다. 그 동안 C#에는 많은 변화가 있었다. 그 중 가장 대표적인 것은 LINQ(Language-INtegrated Query)로 대표되는 함수형 프로그래밍 패러다임의 도입이다. <그림 1>과 같이 2007년 .NET Framework 3.5의 C# 3.0에 LINQ가 처음 배포됐다.



버전 단위로 추가된 C#의 주요 기능을 살펴보면 향후 C#의 미래를 예측해 볼 수 있다. 특히 C# 3.0부터 본격적으로 함수형 프로그래밍 패러다임이 도입되면서 그와 관련된 람다 표현식(Lambda expressions)과 LINQ 등이 추가됐다. 그리고 이러한 함수형 프로그래밍 패러다임 도입이 지금으로부터 8년 전인 2007년을 기점으로 가속화되고 있다.



Java는 2014년 3월 ‘Java SE 8’을 발표하면서 C# 3.0에서 도입한 함수형 프로그래밍 패러다임인 람다 표현식을 도입했다. C#과 비교하면 약 8년 늦은 것이었다.



Java의 함수형 프로그래밍 도입이 C#에 비해 늦게 이뤄진 데는 여러 가지 이유가 있지만, Java가 더 이상 전성기를 누리지 못하고 있다는 점도 한 몫 한다. 그 때문에 Java 진영에서는 10여년 전과는 다른 모습으로 C#을 바라보고 있다. “C#은 Java의 미래다”라고 Java를 질책하고 있는 것이다. 이처럼 함수형 프로그래밍 패러다임은 C#, Java, C++, Python으로 대표되는 대중적 프로그래밍 언어의 발전에 중요한 영향을 미치고 있다. 함수형 프로그래밍 패러다임을 살펴보기 전에 C#에 도입된 LINQ 예제로 기존 프로그래밍 방식과 함수형 프로그래밍 방식의 차이점을 살펴보자. <리스트 1>은 C# 2.0의 사양만으로 구현한 소스 코드다. 소스 데이터에서 양의 정수만을 연산 대상이 되는 데이터로 수집한 다음 정렬하는 간단한 예제 프로그램이다.



<리스트 1> C# 2.0 양의 정수 정렬 using System; using System.Collections.Generic; namespace CSharp2_0 { class Program { static void Main(string[] args) { int[] source = new int[] { 0, -5, 12, -54, 5, -67, 3, 6 }; List< int> results = new List< int>(); foreach (int integer in source) { if (integer > 0) { results.Add(integer); } } Comparison< int> comparison = delegate(int a, int b) { return b - a; }; results.Sort(comparison); foreach (int item in results) { Console.WriteLine(item); } } } }



<리스트 1>을 컴파일한 후 실행하면 <그림 4>와 같이 양의 정수 데이터만을 대상으로 정렬된 순서에 따라 출력되는 것을 확인할 수 있다.



<리스트 1>과 동일한 기능을 수행하는 코드를 LINQ로 구현하면 <리스트 2>와 같다



<리스트 2> C# 3.0 양의 정수 정렬 using System; using System.Collections.Generic; using System.Linq; namespace CSharp3_0 { class Program { static void Main(string[] args) { int[] source = new int[] { 0, -5, 12, -54, 5, -67, 3, 6 }; var results = from integer in source where integer > 0 orderby integer descending select integer; foreach (int item in results) { Console.WriteLine(item); } } } }



<리스트 1>과 <리스트 2>의 실행 결과는 동일하다. 소스 코드의 차이점을 살펴보면, 기존 소스 코드에서 for, while 같은 루프 구문과 delegate를 사용해 구현한 익명 함수(anonymous method)가 모두 사라진 것을 볼 수 있다. 또 SQL 구문와 유사한 LINQ 구문으로 변경된 것을 확인할 수 있다. LINQ 문법보다는 LINQ가 무엇을 추구하고 있는지 좀 더 자세히 살펴보자. <리스트 1>은 양의 정수 정렬이라는 문제를 ‘해결’하기 위해 ‘어떻게(HOW)’ 할 것인지에 중점을 둔다. 그 때문에 문제 해결을 위한 구체적인 세부 과정(해법)을 구현(정의)하고 있다. 이러한 스타일의 프로그래밍 방식을 ‘명령형 프로그래밍(Imperative programming)’이라 한다.

명령형 프로그래밍은 프로그래밍의 상태와 해당 상태를 변경시키는 구문의 관점에서 컴퓨터가 수행할 명령들을 순서대로 써놓은 프로그래밍 패러다임이다. 그러나 <리스트 2>는 양의 정수 정렬이라는 문제를 ‘설명’하기 위해 ‘무엇(WHAT)’을 할 것인지에 무게를 둔다. 그 때문에 문제 해결을 위한 구체적인 제어 흐름(Control flow) 없이 로직(Logic)을 기술한다. 이러한 스타일의 프로그래밍 방식을 ‘선언형 프로그래밍(Declarative programming)’이라 한다.

LINQ에서 사용하는 from, where, orderby, select가 바로 자연어 수준에서 문제를 설명하기 위한 무엇(WHAT)에 해당한다. LINQ 문법을 알지 못해도 자연어 수준에서 LINQ 구문을 통해 해결하고자 하는 문제를 이해할 수 있다. 문제 해결(HOW)에서 문제 설명(WHAT)으로 관점이 바뀌었기 때문이다. 문제를 바라보는 관점의 변화를 통해 문제의 본질에 좀 더 집중할 수 있게 됐다.

선언형 프로그래밍의 세부 분류는 매우 다양하다. <그림 5>과 같이 개발자라면 설계 과정에서 한 번쯤은 사용해봤을 UML이 모델링에 속한다. UML에서 제시하는 다양한 다이어그램을 작성하며 문제 해결을 위한 구체적인 과정을 기술해 본 경험이 없을 것이다. 그 이유는 UML 역시 선언형 프로그래밍의 특징을 갖고 있기 때문이다.



함수형 프로그래밍은 선언형 프로그래밍의 세부 분류 중 하나다. 함수형 프로그래밍은 수학적 함수를 기준으로 문제를 바라보고 그에 대한 설명을 기술한다. 수학적 함수는 명령형 프로그래밍 언어에서 이야기하는 함수와는 다른 특징을 갖고 있다. 이와 관련된 내용은 차후에 F# 함수형 프로그래밍 기본 개념을 다룰 때 자세히 소개할 것이다. 여기서는 C, C++, C#, Java 등의 명령형 프로그래밍 언어 함수와는 다른, 수학적 관점에서 접근하는 함수라고만 알고 있자. 함수형 프로그래밍은 데이터플로우(Dataflow)와도 밀접한 관련이 있다. 데이터플로우는 오퍼레이션(operation) 사이의 데이터 흐름을 <그림 6>과 같이 방향 그래프(directed graph)로 기술하는 프로그래밍 패러다임이다.



데이터플로우에서 이야기하는 오퍼레이션은 함수형 프로그래밍의 함수로 정의할 수 있다. <리스트 2>의 LINQ로 구현한 소스 코드를 수학적 함수의 관점에서 함수 단위로 변경하면 <리스트 3>과 같다.



<리스트 3> C# 함수형 양의 정수 정렬 using System; using System.Linq; namespace Functional { class Program { static void Main(string[] args) { int[] source = new int[] { 0, -5, 12, -54, 5, -67, 3, 6 }; source .Where(integer => integer > 0) // 조건 .OrderByDescending(integer => integer) // 정렬 .ToList() // 데이터 변환 .ForEach(item => // 출력 { Console.WriteLine(item); }); } } }



변경된 <리스트 3>의 구현 내용을 데이터플로우 관점에서 방향 그래프로 표현하면 <그림 7>과 같다. <그림 7>에서 확인할 수 있듯이 ‘데이터 변환(IOrderedEnumerable List)’만을 제외하고는 문제 내용의 자연어 수준에서 문제를 기술했던 내용과 동일하다는 것을 확인할 수 있다. ‘타입 변경’은 프로그래밍 언어라는 특성으로 인해 추가된 내용이다.



또한 함수형 프로그래밍 패러다임의 특징과 데이터플로우 패러다임의 특징을 모두 갖고 있는 것이 바로 리액티브 프로그래밍(Reactive Programming) 패러다임이다. .NET 프레임워크에서는 Rx(Reactive Extension)을 통해 리액티브 프로그래밍을 제공한다. 특히 Rx는 기존 .NET 프레임워크의 특징을 조합한 ‘Rx = Observables + LINQ + Schedulers’의 특징을 갖고 있다. LINQ를 통해 함수형 프로그래밍 패러다임과 비동기 데이터 흐름(Observables)에서 동시성(Schedulers)으로 이벤트 기반 프로그램을 개발할 수 있도록 제공한다.

사용자 이벤트를 처리해 ‘등록’ 버튼을 활성화하는 예제와 텍스트 컨트롤 배경 색상을 변경하는 예제를 통해 함수형 프로그래밍과 데이터플로우 프로그래밍의 특징을 확인해 보자.



<그림 8>에서 제시하는 제약 조건은 ‘이름’ 입력 문자가 4자 이상일 때, 그리고 ‘메일’ 입력 문자가 @ 문자 이후에 최소 2자 이상일 때 ‘등록’ 버튼을 활성화한다. 만약 조건에 부합하지 않으면 문자 입력 텍스트 컨트롤 배경 색상을 스카이블루(SkyBlue)로 표시하며 ‘등록’ 버튼을 비활성화한다. <그림 8>에서 제시된 제약 조건을 설명할 수 있을까? 선언형 프로그래밍의 문제를 바라보는 관점의 세부 분류인 함수형 프로그래밍과 데이터플로우의 시각화 특징을 모두 갖고 있는 리액티브 프로그래밍으로 이를 해결할 수 있다. 리액티브 프로그래밍은 이벤트 기반 프로그램을 개발할 수 있기 때문이다. <리스트 4>는 Rx을 통해 제약 조건을 해결한 리액티브 프로그래밍 소스 코드다. 구현한 소스 코드 세부 내용과 Rx 매커니즘을 이해하기 보다는 함수 단위로 문제 설명 로직만 살펴보도록 하자.



<리스트 4> 등록 버튼 활성화와 텍스트 컨트롤 배경 색상 제약 조건의 리액티브 프로그래밍 using System; using System.Diagnostics; using System.Drawing; using System.Reactive.Linq; using System.Text.RegularExpressions; using System.Windows.Forms; namespace FunctionalReactiveProgramming { public partial class MainForm : Form { public MainForm() { InitializeComponent(); buttonRegister.Enabled = false; ValidEventUI(); } private void ValidEventUI() { var nameValid = Observable .FromEventPattern< EventArgs>(textBoxName, "TextChanged") // 이벤트 .Select(e => ((TextBox)e.Sender).Text) // 타입 변환 .DistinctUntilChanged() // 변경 var mailValid = Observable .FromEventPattern< EventArgs>(textBoxMail, "TextChanged") // 이벤트 .Select(e => ((TextBox)e.Sender).Text) // 타입 변환 .DistinctUntilChanged() // 변경 대기 .Select(text => // 조건 { Regex regex = new Regex(@"^([w.-]+)@([w-]+)((.(w){2,3})+)$"); return regex.Match(textBoxMail.Text).Success; }); nameValid.Subscribe(result => // 색상 { if (!result) textBoxName.BackColor = Color.SkyBlue; else textBoxName.BackColor = Color.White; }); mailValid.Subscribe(result => // 색상 { if (!result) textBoxMail.BackColor = Color.SkyBlue; else textBoxMail.BackColor = Color.White; }); var combineEnabled = Observable.CombineLatest(nameValid, mailValid, (enabledName, enabledMail) => // 조합 { if (enabledName && enabledMail) return true; else return false; }); combineEnabled.Subscribe(enabled => // 활성화 { if (enabled) buttonRegister.Enabled = true; else buttonRegister.Enabled = false; }); } } }





<리스트 4>에서 작성된 프로그래밍 코드를 시각화해 보면 <그림 9>와 같다. <그림 9>에서 흰색 배경의 ‘타입 변경’과 ‘변화 대기’를 제외하고는 프로그램 요구사항인 자연어 표현(로직)과 1:1로 매칭된다. 리액티브 프로그래밍 구현 소스 코드 수준으로 자연어 수준이 추상화돼 프로그램을 개발할 수 있다. 자연어 수준의 높은 코드 표현력을 제공할 수 있는 건 선언형 프로그래밍를 기반으로 한 함수형 프로그래밍이기 때문에 가능한 것이다.

‘타입 변경’은 텍스트 변경 이벤트로 전달되는 object 이벤트 데이터를 string으로 타입 변경하고, ‘변화 대기’는 동일한 텍스트를 매번 이벤트 처리하지 않기 위해 이전 텍스트 데이터와 변화된 데이터일 때까지 대기 처리한다. 지금까지 확인한 내용을 살펴보면 함수형 프로그래밍은 수학적 함수를 기반으로 선언형 프로그래밍의 세부 분류에 속하기 때문에 문제 해결이 아닌 문제 설명의 관점에서 함수 단위로 프로그래밍을 한다. 그 결과, 프로그래밍의 특징으로 새롭게 추가되는 내용을 제외하고는 최대한 자연어 수준에서 프로그래밍이 가능하며 프로그래밍된 소스 코드는 데이터플로우로 시각화할 수 있다.

함수형 프로그래밍과 관련된 시각화 관련 노력은 국내외 소프트웨어 학자의 연구 활동 결과를 통해 확인할 수 있다. 지금도 지속적으로 연구되고 있으며 검색을 통해 관련 논문을 쉽게 확인할 수 있다. 위키 백과(en.wikipedia.org)에서는 가장 이상적인 프로그래밍 언어는 순수한(Pure) 함수형 언어라고 소개하고 있다. 그러나 대부분의 함수형 언어들은 순수한 함수형 언어가 아니다. F#, Scala, Clojure 모두 객체지향 패러다임을 제공하고 있으며, 우리가 매일 작성하고 있는 수학적 함수와 관련 없는 함수도 구현할 수 있도록 제공하고 있다. 그렇다면 명령형과 함수형 프로그래밍 패러다임이 이처럼 다른 이유는 무엇일까?

모든 프로그래밍 패러다임은 특정 문제 해결을 위해 추상화로부터 시작한다. 추상화 대상이 서로 다르기 때문에 동일한 문제를 바라 보는 관점의 차이가 발생한다. 명령형 프로그래밍은 폰 노이만 구조를 추상화해 문제를 바라본다. 폰 노이만 구조가 추상화 대상이 된 이유는 <그림 10>과 같이 현대 컴퓨터 구조가 CPU, 메모리, 프로그램으로 돼 있기 때문이다.



명령형 프로그래밍은 컴퓨터를 추상화했기 때문에 컴퓨터가 수행해야 하는 구체적인 명령들을 기술하게 된다. 어셈블러를 사용할 때는 2진수와 같은 시스템 수준에서 작업을 했고 C, C++, C# , Java 등과 같은 고수준 언어들도 여전히 폰 노이만 구조의 CPU, 메모리, 프로그램 구조에 의존적인 소스 코드를 작성한다.

그러나 함수형 프로그래밍은 수학적 함수를 추상화한 프로그래밍 패러다임이다. 폰 노이만 구조의 컴퓨터 작동 원리보다 더 높은 수학적 함수 추상화를 통해 프로그래밍을 제공한다. 수학적 모델을 기반으로 하기 때문에 증명하기가 쉽다. 폰 노이만 구조 추상화 보다 더 추상적인 개념인 수학적 함수 추상화를 통해 얻을 수 있는 장점에 대해서는 ‘함수형 프로그래밍 F#’ 연재를 통해 살펴볼 것이다.



지금가지 기존 명령형 프로그래밍 패러다임에서는 경험할 수 없는 문제 설명 관점을 함수형 프로그래밍을 통해 살펴봤다. 이로써 선언형 프로그래밍 패러다임의 특징을 알 수 있었다. 함수형 프로그래밍은 선언형 프로그래밍 패러다임의 특징을 수학적 함수인 순수 함수(Prue Function)를 통해 구체화한다. 순수 함수는 명령형 프로그래밍의 비순수 함수(Impure Function) 보다 선언형 프로그래밍 패러다임에 적합하다. 이런 특징 덕분에 명령형 프로그래밍 패러다임의 주요 프로그래밍 언어들이 앞다퉈 함수형 프로그래밍 패러다임 개념을 도입하고 있는 것이다.

지금까지 선언형 프로그래밍 패러다임과 추상화를 기준으로 명령형과 함수형 프로그래밍 패러다임의 차이점을 확인해 봤다. 함수형 프로그래밍을 좀 더 자세히 살펴보면, 데이터 관점과 함수 관점에서 명령형 프로그래밍과는 많은 차이점을 갖고 있다. 다음 시간에는 이와 관련된 내용을 살펴보도록 하겠다.



F# 이모저모

F# 발전사
F#은 2005년 처음 등장했다. 2005년 1.x 버전일 때는 윈도우 운영체제만 지원했다. 그러다 2.0 버전부터 리눅스와 OS X를 추가하면서 멀티 플랫폼을 지원하기 시작했다. 3.0에서는 자바스크립트(JavaScript)와 함께 웹 프로그래밍(Web Programming) 영역, GPU를 활용한 분석 프로그래밍(Analytical Programming) 영역까지 적용 범위를 확대했다. 멀티 플랫폼을 지원하면서부터 비주얼 스튜디오뿐 아니라 다양한 개발 도구를 제공하고 있다. 특히 대화형(Interactive) 스크립팅 개발 환경을 제공하기 때문에 기존 스크립트 언어가 가진 특징도 그대로 지원한다.

F# 버전 단위 주요 기능 F# 1.0에서 제공하는 핵심 기능은 당연히 함수형 프로그래밍이다. 2015년 3월 기준으로 최신 정식 버전은 3.1이다. 올해 안에 비주얼 스튜디오 2015와 함께 4.0이 제공될 예정이다. F# 3.0에서는 LINQ qury expressions 기능을 통해 Information Rich Programming까지 제공한다. 주요 기능 목록들을 자세히 살펴 보면 대부분 데이터 중심 기능이라는 것을 확인할 수 있다. F#이 함수형 프로그래밍 패러다임을 따르기 때문에 명령형 프로그래밍 패러다임에 비해 데이터 처리 관련 기능이 더 풍부하다. F#에서 제시된 주요 기능들은 ‘함수형 프로그래밍 F#’ 연재를 통해 지속적으로 자세히 살펴볼 예정이다. 아래 그림은 F#이 버전별로 제공하는 주요 기능 목록이다.

F# 학습 F# 프로그래밍을 학습하기 위해 개발 도구 설치는 필수다. 그러나 F# 공식 사이트(www.tryfsharp.org)에서는 웹브라우저에서 특별한 개발 도구 설치 없이 바로 F# 프로그래밍 언어를 학습할 수 있도록 예제 소스를 비롯한 다양한 정보를 제공하고 있다.



출처 : 마이크로소프트웨어 4월호

제공 : 데이터전문가 지식포털 DBguide.net