IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
메인 컨텐츠로 가기

한국 developerWorks  >  리눅스  >

보다 나은 프로그래밍으로 가는 길: 4장

함수 프로그래밍(Functional programming)

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.


난이도 : 초급

Teodor Zlatanov, 프로그래머, Gold Software Systems

2002 년 1 월 01 일

dW 시리즈를 통해 보다 나은 펄 프로그래밍을 위한 완벽한 가이드를 제공하고 있다. 이번에는, 함수 프로그래밍을 비롯하여, 속도와 우수성을 기대하고 있는 펄 프로그래머에게 중요한 펄 이디엄을 소개한다.

함수 프로그래밍(FP)은 접근 방식이 될 수 있고 솔루션도 될 수 있으며 또는 종교가 될 수도 있다. 나는 개인적으로 이것이 종교가 되길 바라고 있다. 프로그래머의 무기고에는 적어도 한 개 이상의 무기가 있어야 하기 때문이다.

함수 프로그래밍 ("comp.lang.functional FAQ" 에서 발췌)

함수 프로그래밍은 명령어의 실행 보다는 수식의 계산을 강조하는 프로그래밍 스타일이다. 수식은 기본 값들을 조합하는 함수들을 사용하여 형성된다. 함수 언어는 함수 스타일의 프로그래밍을 지원한다.

어떻게 사용 할 것인가?

종종 말해왔었지만, '그것이 존재하기 때문에 사용한다'는 식의 툴 사용을 자제하기 바란다. 망치로는 못을 박고 스크류드라이버로 스크류를 돌린다. 모든 툴을 이해하고 적절한 것을 사용하라. 여러분의 인생이 행복해 진다.

절차적(전통적인) 프로그래밍이 부작용(side effect)을 위해서 함수를 사용하는 것을 다루었던 반면 함수 프로그래밍은 함수를 값에 적용하는 것을 다룬다는 것이 함수 프로그래밍의 관점이다. comp.lang.functional FAQ (참고자료)의 레퍼런스는 함수 프로그래밍의 방법론과 의도를 이해하는 최고의 출발점이다.

실제로는 절차 언어인 펄에서, 함수 프로그래밍은 하나의 접근 방식으로서만 가능하다. 실제 솔루션은 주로 map() 또는 grep() 함수들을 사용하여 함수적 솔루션을 시뮬레이션 하는 것이다. 함수 프로그래밍 접근방식은 세 가지 이유로 가치가 있다. 우선, FP는 프로그래머에게 문제에 대한 새로운 시각을 제공하고 그것으로 인해서 좀 더 나은 솔루션이 가능해 질 수 있다. 둘째, Schwartzian과 Guttman-Rosler 변형과 많은 기타 펄 이디엄은 map()grep()의 도움 없이는 사용하거나 이해하기가 힘들다. 셋째, map()grep() 없이 알고리즘을 구현하면, 펄에서의 함수호출은 매우 '비싸기' 때문에 심각하게 느려질수 있다.




위로


map() 함수

map() 함수는 리스트의 모든 엘리먼트에 붙여진 고무 스탬프와 같은 것이다 ("perldoc -f map" 참조). 이것은 두 부분으로 구성되어 있다. (블록(또는 수식)과 리스트):


Listing 1. map() 작동

# map {BLOCK GOES HERE} LIST;
map {$_++} @p;                          # increment every element of @p 
# map EXPRESSION, LIST;
map -f,@p;                              # file test every element of @p

벤치마킹을 하면 foreach() 루프는 map() 보다 더 낫다. 퍼포먼스를 테스트 하려는 것이 아니라면 CPU 전산에 map()을 사용하지 않는 것이 좋다. 효율성을 상실하지 않고 코드를 좀더 우아하고 단순하게 만들 때 사용하라.

map()이 할 수 있는 일이 많이 있다. 무엇보다도, 이것은 $_ 변수를 변경함으로서 어레이 엘리먼트를 변경할 수 있다. 블록 또는 (덜 일반적이긴 하지만) 수식 안에서, $_ 는 리스트의 현재 엘리먼트이다. 여러분은 어떤 엘리먼트를 보고있는지 알지 못한다. 전체 포인트는 하나의 함수를 모든 엘리먼트에 독립적으로 매칭하는 것이다. map()은 프로그래머가 어레이 오프셋을 독립적으로 생각하도록 함으로서 코딩 효율성과 스타일을 향상시킨다.


Listing 2. foreach() vs. map()

# "normal" (procedural)  way
foreach (sort keys %ENV)
{
 print "$_ => $ENV{$_}\n";
}
# FP way
map { print "$_ => $ENV{$_}\n" } sort keys %ENV;

Listing 2에서, FP 방식이 근본적으로는 다르지 않지만 함수 플로우를 오른쪽에서 왼쪽으로 전달한다. 우선 키의 리스트가 획득되면, 소팅된 후 print() 함수는 소팅된 키 리스트의 각 엘리먼트에 적용된다.


Listing 3. map()을 이용하여 리스트 변경하기

# these are the users
@users = ('joe', 'ted', 'larry');
# and this is an on-the-fly substitution of user names with hash references
map { $_ = { $_ => length } } @users;
# @users is now ( { 'joe' => 3 },
#                 { 'ted' => 3 },
#                 { 'larry' => 5 } )

Listing 3은 map()으로 전달된 리스트가 어떻게 완벽하게 재작성될 수 있는지를 보여주고 있다. 이 경우, @users 어레이에는 사용자 이름만 포함되어있지만 map()이 적용된 후에는, 어레이에는 '사용자이름 => 바이트길이'와 함께 해시 레퍼런스가 포함되어있다.


Listing 4. map()을 이용하여 리스트 변경하기, part 2

use File::stat;
use Data::Dumper;
@files = ('/etc/passwd', '/etc/group', '/etc/fstab', '/etc/vfstab');
print Dumper 
     map { $sb = stat $_; 
           $_ = (defined $sb) ? 
                { 
                  name => $_,  
                  size =>$sb->size(), 
                  mtime => $sb->mtime() 
                } : 
                undef
         } @files;

Listing 4에서, 파일의 리스트를 만들었다. 그런다음 하나의 문장에서 각 파일의 이름, 크기, mtime(변경 시간)에 대한 엔트리와 함께 해시들의 리스트를 만들었다. 존재하지 않는 파일은 해시 레퍼런스 대신 "undef" 레퍼런스를 갖는다. 마지막으로, Data::Dumper는 전체 리스트를 보기좋게 프린팅한다.

Listing 4와 같은 코드는 다른 사람들을 위해서 문서화되어야 한다. 모든 라인을 머리속에 주입시키려고 애쓰지 않아도 된다. 우아한 코드는 적당한 포매팅이나 코멘트가 빠진 추한 오리일 뿐이다.




위로


grep() 함수

UNIX를 사용해왔던 사람들이라면 grep() 함수는 간단히 사용할 수 있다. 이것은 grep 유틸리티 처럼 작동한다.

grep()의 신택스는 map()과 같다. 블록 또는 수식은 패스될 수 있고 $_ 는 실험할 때 현재 엘리먼트로 앨리어스 된다. grep()으로 전달된 리스트의 엘리먼트를 변경하는 것은 좋은 생각이 아니다. 이것은 map()에 해당하는 것이다. grep 에는 grep()을 사용하고 'map' 에는 map()을 사용한다. 이 규칙의 유일한 예외라면, 소팅하는 동안 임시적인 해시 필드 또는 어레이 엔트리를 만들어야 한다면 나중에 그것들을 반드시 제거해야 한다.


Listing 5. grep() 작동

# grep {BLOCK GOES HERE} LIST;
grep {$_ > 1} @p;                       # only accept numbers more than 1
grep {$_++} @p;                         # please don't do this
# grep EXPRESSION, LIST;
grep /hi/,@p;                           # only accept matching elements
grep !/hi/,@p;                          # do not accept matching elements

빠른 필터링에는 grep()을 사용하는 것이 편리하다. 하지만 어떤 환경에서는 foreach() 루프가 더욱 빠르다. 의심스럽다면 벤치마킹을 해보라.


Listing 6. 홀수 필터링에 grep() 사용하기

my @list = (1, 2, 3, 'hi');
my @results;
# the procedural approach
foreach (@list)
{
 push @results, $_
  unless $_%2;
}
# using grep - FP approach
@results = grep { !($_ % 2) } @list;

다른 예제도 있다. 디렉토리에서 봐야 한다면 여기에서 모든 파일 이름들을 검색한다:


Listing 7. 디렉토리에 있는 모든 파일이름을 얻기 위해 grep() 사용하기

opendir(DIR, ".") || die "can't opendir: $!"; # get the directory handle
my @f = grep { /^[^\.]/ && -f } readdir(DIR); # filter only files into @f

Listing 7의 첫번째 라인은 현재 디렉토리를 열거나 적절한 공지와 함께 프로그램을 종료한다.

두 번째 라인은 readdir() 함수를 호출하여 숨겨진 파일과 디렉토리 같은 '파일이 아닌 객체'를 실행시킨다.

foreach() 루프의 4~5 라인이 수행하는 것을 두 라인으로 수행했다. 주석을 반드시 달도록 하라; 위에 보이는 짧은 주석은 제품 코드에는 충분하지 않다. 가끔씩, grep()은 스칼라 콘텍스트에서 사용된다:


Listing 8. 모든 펄 프로세스를 실행시키기 위해 grep() 사용하기

use Proc::ProcessTable;                 # get this module from CPAN
use strict;
my $table = new Proc::ProcessTable;
my @procs;
if (@procs = grep { defined $_->cmndline && 
                            $_->cmndline =~ /^perl/ } 
                @{$table->table})
{
 print $_->pid, "\n" foreach @procs;
}
else
{
 print "No Perl interpreters seem to be running.\n";
}

여기에서 동시에 grep()의 리턴을 @procs 어레이에 할당하고, 이것이 모든 엘리먼트들을 포함하고 있는지를 테스트한다. 패턴과 맞는 엘리먼트가 없다면 그 결과에 대한 메시지를 프린트한다. foreach() 루프와 같은 코드는 5~6 줄이 걸린다. 이러한 것이 충분히 이야기되지 않을 경우, Listing 8과 같은 코드는 다른 사람들이 보고 코드의 의도와 결과를 즉시 알 수 있도록 주석을 달도록 한다.




위로


map으로 소팅하기: Schwartzian과 Guttman-Rosler 변형

펄의 sort() 함수는 "일종의" 절차이다. 이것은 코드 블록 또는 함수 이름을 가져다가 모든 엘리먼트들을 소팅하는데 사용하기 때문이다. map()grep()과 마찬가지로, sort() 는 비교되는 값에 레퍼런스를 다룬다. 그래서 그들을 변경하는 것은 비교되는 값들을 변경하는 것이된다. 그렇게 하지 않도록 조심하라.

펄의 소팅 기능들은 사용하기에 매우 쉽다. 간단한 형식의 소트는 다음과 같이 이루어진다:


Listing 9. 기본적인 sort()

@new_list = sort @old_list;             # sort @old_list alphabetically

소트 디폴트는 리스트에서 모든 스칼라에 간단한 스트링 비교를 사용한다. 리스트에 알파벳 순으로 소트되는 사전 단어들이 포함되어 있다면 이러한 방식은 무방하지만 리스트에 숫자가 포함되어 있다면 상황은 달라진다. 왜냐하면 스트링 비교에서 "1"은 "010" 앞에 오기 때문이다. 숫자는 값으로 비교되어야 한다. 스트링에 의해서가 아니다.

다행히 이것은 수행하기가 쉽고 일반적인 펄 이디엄이다:


Listing 10. 숫자 sort()

@old_list = (1, 2, 5, 200, '010');
@new_list = sort { $a 
 $b } @old_list; # sort @old_list numerically

010에 싱글 쿼트를 달았다. 왜냐하면 펄에서 0으로 시작하는 숫자들은 8진법으로 인터프리트 되기 때문에 010은 십진수의 8 이 된다. 쿼트없이 직접 시도해보라. 더욱 분명해질 것이다. 이것은 또한 필요할 경우 자동적으로 펄 스칼라가 어떻게 숫자로 변환되는지를 보여주고 있다.

Listing 9에서 Listing 10의 @old_list에 대해 디폴트 소트를 실행하면 200이 5앞에 온다는 것을 알 수 있다.

소팅된 리스트를 바꾸려면, reverse() 함수를 리스트가 소팅된 후에 리스트에 붙이거나 소팅 함수를 변경할 수 있다. 예를 들어, Listing 10에서 하나를 역 소트 하면 비교 코드 블록은 { $b $a }이 될 수 있다.

스칼라도 소트할 수 있다. 이것으로 충분하지 않다. 어레이와 해시 같은 데이터 구조에 대해서도 소팅은 수행된다. 펄은 유연한 신택스 덕택에 모든 종류의 소팅을 지원한다. 예를 들어, 한 묶음의 해시 레퍼런스를 소팅해야 한다면, 해시의 '이름' 키가 소팅 필드이다. 알파벳순서로 소팅을 원한다면 cmp 연산자를 사용해야 한다:


Listing 11. 해시 멤버에 대한 소트

# create a list with two hash references
@old_list = ( { name => "joe" }, { name => "moe" } );
# sort @old_list by the value of the 'name' key
@new_list = sort { $a->{name} cmp $b->{name} } @old_list;

이제 좀 더 재미있는 주제로 돌입해보자. 소팅된 객체에서 데이터를 얻는 것이 비싸다면? 만일 소트되어야 하는 값을 얻어야 할 때마다 split() 함수를 스트링에 붙여야 한다면? 비교값이 필요할 때 마다 split() 을 실행시키는 것은 전산적으로 볼 때 낭비다. 그리고 동료들도 여러분을 비웃을지도 모른다. 비교 값들의 임시 리스트를 만들수 있다. 하지만 쉽지않은 작업이고 버그가 생기가 쉽다. Schwartzian 변형을 사용하는 것이 더 낫다.

Schwartzian 변형은 다음과 같다:


Listing 12. Schwartzian 변형

# create a list with some strings
@old_list = ( '5 eagles', '10 hawks', '2 bulls', '8 cows');
# sort @old_list by the first word in each string, numerically
@new_list = map($_->[1], 
                sort( { $a->[0] 
 $b->[0] }
                       map( [ (split)[0], $_ ], 
                            @old_list)));

오른쪽에서 왼쪽으로 보자. 우선 map() 을 @old_list 리스트에 붙여 새로운 임시 리스트를 만들었다. map()이 리스트를 변형할 수 있다는 것을 기억하라. 이번 경우, 스트링의 split() 에서 나온 첫 번째 값과 @old_list에 있는 각 스트링으로 구성되어 있는 어레이를 포함시키기 위해서 @old_list를 다시쓴다. 그 결과는 새로운 리스트이다; @old_list에 어떤 변화도 없다.

그런다음 비교 값으로 소팅했다 (@old_list의 어레이 엘리먼트 중 첫 번째 엘리먼트). @old_list는 전체 프로세스에서 실제로 변경되지 않는다는 것에 주목하라.

그리고 나서 두 번째의 어레이 엘리먼트를 현재 값으로 매핑함으로서 스트링으로 돌아가 소팅된 리스트에 대해 또 다른 map() 을 수행했다. $_->[1] 은 " $_ 가 참조한 리스트에서 두 번째 객체에 저장된 값이 되도록 $_ 를 재작성하라." 라는 의미이다.

Guttman-Rosler 변형은 매력적이다. 하지만 여러분을 더욱 혼돈에 빠트릴 수 있다.(참고자료).




위로


FP를 사용해야 할 때

다시한번 말하지만 툴에 대해 숙지하라! 함수 프로그래밍은 지금까지 검토했듯이 훌륭한 툴이다. 많은 문제들을 간단하게 하고 쉽게 만든다. 언제 FP를 사용 할 것인가?

  • 무엇보다도, 펄에서 FP는 하나의 접근방식일 뿐이라는 것을 기억하라. 함수적 솔루션을 모방하더라도 솔루션은 절차적(procedural)이 될 것이다. 문제는 FP가 언제 사용되는가가 아니라 얼마나 많이 사용될 수 있는가 이다.
  • 복잡한 소팅을 해야 한다면, Schwartzian 변형이나 Guttman-Rosler 변형이 적절하다. 이들은 규칙적인 소팅에 대한 함수적 대체라고 할 수 있다.
  • FP함수들이 연결되어 있다면 FP를 고려해보라. 예를들어, 단계마다 여러 함수들에 의한 리스트 변경은 FP 접근방식으로 이루어질 수 있다.
  • 사용되는 즉시 버려지는 임시적인 변수들이 많다면, 그들의 수를 감소시키는데 FP를 고려해보라.
  • 리스트 또는 해시에 적용된 함수들의 필터링, 소팅, 변형은 FP가 될 수 있다.
  • 함수가 많은 부작용을 갖고 있고 매개변수들도 많다면 FP가 적절하지 않다.
  • FP와 관련해서 반복 알고리즘(recursive algorithms)은 둘 중 하나의 방법을 취할 수 있다. FP 접근방식으로 수행했을 때 더 나아지거나 또는 그 반대가 될 수 있다.
  • 중요한 퍼포먼스에는 FP를 피하도록 하라. 접근방식을 선택하기 위해서는 Benchmark 모듈을 사용하라. 가끔씩 FP는 속도 향상에 놀랍게 기여하지만 코드 퍼포먼스를 저하시킬 수 있다.
  • One-liner는 FP와 잘맞는다.
  • 복잡한 펄 코드는 코드 작동을 모호하게 하는 방식으로 grep()ma

    사용자 이름의 리스트를 사용자 ID로 변형하는 map()를 사용하는 코드를 작성한다.

  • 사용자 ID를 찾기 위해 (1)을 사용하는 프로그램을 만든다. 특정 이름별로 사용자 리스트 필터링을 할 수 있도록 한다.
  • root가 소유한 프로세스가 실행되는 지를 점검하는 코드를 작성한다 (UNIX system).
  • 작은 데이터 세트에 대해서, Benchmark the Schwartzian 변형, 여러분이 만든 소팅코드, Guttman-Rosler 변형을 벤치마킹한다. 이를 위해 Benchmark 모듈을 사용한다.
  • 큰 데이터 세트에도 (4)을 사용한다t. 예를 들어, 시스템 상의 모든 파일 이름들을 크기별로 소팅한다.
  • comp.lang.functional FAQ에서Erlang, Scheme, Haskell을 참조한다.
  • 펄 버전의 grep 프로그램을 만든다. grep()함수가 바로 머리속에 떠오르는가? 이 경우 grep() 을 사용하면 안된다. 큰 파일을 프로세스 해야 하고 grep()을 실행시키기 위해 메모리의 모든 파일 내용을 보관하는 것은 이치에 맞지 않다. 보다 나은 솔루션을 찾도록 한다.



위로


참고자료




위로


필자소개

Teodor Zlatanov는 1999년에 보스턴 대학에서 컴퓨터 공학을 전공했다. 졸업 후 Perl, Java, C, C++를 사용하여 프로그램을 개발하였다.





위로


기사에 대한 평가

매우 불만족 (1)
불만족 (2)
보통 (3)
만족 (4)
매우 만족 (5)




위로



    IBM 소개개인정보 보호정책문의