Raku 예제 101
Raku 예제 101 (Raku by example 101)
Raku 프로그램의 기초적인 입문 예제예요.
탁구 토너먼트를 주최한다고 상상해 볼게요. 심판들이 각 경기 결과를 Player1 Player2 | 3:2 형식으로 알려주는데, 이건 Player1이 Player2를 3대 2로 이겼다는 뜻이에요. 각 선수가 이긴 매치 수와 세트 수를 합산해서 최종 우승자를 정해 주는 스크립트가 필요하죠.
입력 데이터(scores.txt라는 파일에 저장)는 이렇게 생겼습니다.
Beth Ana Charlie Dave
Ana Dave | 3:0
Charlie Beth | 3:1
Ana Beth | 2:3
Dave Charlie | 3:0
Ana Charlie | 3:1
Beth Dave | 0:3
첫 줄은 선수 명단이고, 그다음 줄마다 한 경기의 결과를 기록해요.
Raku로 이 문제를 푸는 방법 하나를 보여드릴게요.
use v6;
# start by printing out the header.
say "Tournament Results:\n";
my $file = open 'scores.txt'; # get filehandle and...
my @names = $file.get.words; # ... get players.
my %matches;
my %sets;
for $file.lines -> $line {
next unless $line; # ignore any empty lines
my ($pairing, $result) = $line.split(' | ');
my ($p1, $p2) = $pairing.words;
my ($r1, $r2) = $result.split(':');
%sets{$p1} += $r1;
%sets{$p2} += $r2;
if $r1 > $r2 {
%matches{$p1}++;
} else {
%matches{$p2}++;
}
}
my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
for @sorted -> $n {
my $match-noun = %matches{$n} == 1 ?? 'match' !! 'matches';
my $set-noun = %sets{$n} == 1 ?? 'set' !! 'sets';
say "$n has won %matches{$n} $match-noun and %sets{$n} $set-noun";
}
이 코드는 다음 출력을 만들어 냅니다.
Tournament Results:
Ana has won 2 matches and 8 sets
Dave has won 2 matches and 6 sets
Charlie has won 1 match and 4 sets
Beth has won 1 match and 4 sets
이제 이 프로그램을 부분별로 뜯어볼게요.
use v6
use v6;
모든 Raku 프로그램은 이와 비슷한 use v6; 줄로 시작해야 해요. 이 줄은 프로그램이 기대하는 Raku 버전을 컴파일러에 알려줍니다. 예를 들어 6.c는 Raku 버전의 예시예요. 실수로 이 파일을 Perl로 실행하면, 도움이 되는 오류 메시지를 받게 됩니다.
문장 (Statements)
# start by printing out the header.
say "Tournament Results:\n";
Raku 프로그램은 0개 이상의 문장(statement)으로 이루어져 있어요. 문장은 세미콜론이나 행 끝의 중괄호로 끝납니다. Raku에서 한 줄 주석은 단일 해시 문자 #로 시작해서 행 끝까지 이어져요. Raku는 여러 줄/내장 주석도 지원합니다. 컴파일러는 주석을 프로그램 텍스트로 평가하지 않으며, 주석은 사람이 읽기 위한 것뿐이에요.
렉시컬 스코프 (Lexical scope)
my $file = open 'scores.txt';
my는 렉시컬 변수(lexical variable)를 선언해요. 렉시컬 변수는 선언 지점부터 블록 끝까지 현재 블록 안에서만 보입니다. 둘러싼 블록이 없다면 남은 파일 전체에서 보이는데(그것이 사실상 둘러싼 블록이 되죠), 블록은 중괄호 { } 사이에 둘러싸인 코드의 어떤 부분이든 돼요.
시길 (Sigils)
my $file = open 'scores.txt';
변수 이름은 *시길(sigil)*로 시작해요. 시길은 $, @, %, & 같은 비알파벳·비숫자 기호이거나, 가끔은 이중 콜론 ::이기도 해요. 시길은 변수의 구조적 인터페이스, 즉 단일 값으로 다룰지, 복합 값으로 다룰지, 서브루틴인지 등을 나타냅니다. 시길 뒤에는 *식별자(identifier)*가 오는데, 문자, 숫자, 밑줄로 구성될 수 있어요. 글자 사이에는 대시 -나 아포스트로피 '도 쓸 수 있어서, isn't와 double-click은 유효한 식별자입니다.
스칼라 (Scalar)
시길은 변수의 기본 접근 방식을 나타내요. @ 시길 변수는 위치적(positional)으로 접근하고, % 시길 변수는 문자열 키로 접근합니다. 반면에 $ 시길은 어떤 단일 값이든 담고 어떤 방식으로든 접근할 수 있는 일반적인 스칼라 컨테이너를 나타내요. 스칼라는 심지어 Array나 Hash 같은 복합 객체도 담을 수 있어요. $ 시길은 (배열이나 해시처럼) 여러 값을 기대하는 컨텍스트에서도 단일 값으로 다뤄야 한다는 신호입니다.
파일핸들 (Filehandle)
내장 함수 open은 여기서 scores.txt라는 파일을 열고 파일핸들(filehandle), 즉 그 파일을 나타내는 객체를 반환해요. 할당 연산자 =는 그 파일핸들을 왼쪽의 변수에 할당합니다. 즉 $file이 이제 그 파일핸들을 저장한다는 뜻이에요.
문자열 리터럴 (String literals)
'scores.txt'는 *문자열 리터럴(string literal)*이에요. 문자열은 텍스트 조각이고, 문자열 리터럴은 프로그램에 직접 나타나는 문자열입니다. 이 줄에서는 open에 제공되는 인자죠.
배열 (Arrays)
my @names = $file.get.words;
오른쪽은 $file에 저장된 파일핸들에서 get이라는 메서드(method), 즉 이름 붙은 동작의 묶음을 호출해요. get 메서드는 파일에서 한 줄을 읽어 행 끝을 제거하고 반환합니다. get을 호출한 뒤 $file의 내용을 출력해 보면 첫 줄이 더 이상 없다는 걸 볼 수 있어요. words도 메서드인데, get이 반환한 문자열에서 호출됩니다. words는 자신의 invocant(동작 대상이 되는 문자열)를 단어들의 리스트, 여기서는 공백으로 구분된 문자열들로 분해해요. 단일 문자열 'Beth Ana Charlie Dave'를 문자열 리스트 'Beth', 'Ana', 'Charlie', 'Dave'로 바꾸는 거죠. 마지막으로 이 리스트는 Array @names에 저장됩니다. @ 시길은 선언된 변수를 Array로 표시해요. 배열은 순서 있는 리스트를 저장합니다.
해시 (Hashes)
my %matches;
my %sets;
이 두 줄은 해시 두 개를 선언해요. % 시길은 각 변수를 Hash로 표시합니다. Hash는 키-값 쌍의 순서 없는 모음이에요. 다른 언어에서는 해시 테이블, 딕셔너리(dictionary), *맵(map)*이라고 부르죠. 해시 테이블에 %hash{$key}로 어떤 $key에 해당하는 값을 질의할 수 있어요. 점수 계산 프로그램에서 %matches는 각 선수가 이긴 매치 수를, %sets는 각 선수가 이긴 세트 수를 저장해요. 두 해시 모두 선수 이름으로 인덱싱됩니다.
for
for $file.lines -> $line {
...
}
for는 리스트의 각 항목에 대해 중괄호로 구분된 블록을 한 번씩 실행하는 루프를 만들고, 매 반복마다 변수 $line을 그 항목의 현재 값으로 설정해요. $file.lines는 scores.txt 파일에서 읽은 줄들의 리스트를 만드는 데, 이미 $file.get을 한 번 호출했으므로 파일의 두 번째 줄부터 끝까지입니다. 첫 반복에서 $line은 문자열 Ana Dave | 3:0을 담고, 두 번째에서는 Charlie Beth | 3:1을 담고, 이런 식입니다.
my ($pairing, $result) = $line.split(' | ');
my는 여러 변수를 동시에 선언할 수 있어요. 할당의 오른쪽은 split이라는 메서드 호출로, 문자열 ' | '를 인자로 전달합니다. split은 자신의 invocant를 문자열 리스트로 분해해서, 리스트 항목들을 구분자 ' | '로 이어 붙이면 원래 문자열이 됩니다. $pairing은 반환된 리스트의 첫 항목을, $result는 두 번째를 받아요. 첫 줄을 처리한 뒤 $pairing은 문자열 Ana Dave를, $result는 3:0을 담습니다. 다음 두 줄도 같은 패턴을 따릅니다.
my ($p1, $p2) = $pairing.words;
my ($r1, $r2) = $result.split(':');
첫 번째는 두 선수의 이름을 추출해서 $p1과 $p2에 저장하고, 두 번째는 각 선수의 결과를 추출해서 $r1과 $r2에 저장해요. 파일의 첫 줄을 처리한 뒤 변수들은 다음과 같은 값을 담습니다.
| Variable | Contents |
|---|---|
| $line | 'Ana Dave |
| $pairing | 'Ana Dave' |
| $result | '3:0' |
| $p1 | 'Ana' |
| $p2 | 'Dave' |
| $r1 | '3' |
| $r2 | '0' |
그다음 프로그램은 각 선수가 이긴 세트 수를 셉니다.
%sets{$p1} += $r1;
%sets{$p2} += $r2;
위 두 문장은 += 복합 할당 연산자를 포함해요. 이것들은 단순 할당 연산자 =를 쓰는 다음 두 문장의 변형인데, 처음 봤을 때 이해하기 더 쉬울 수 있어요.
%sets{$p1} = %sets{$p1} + $r1;
%sets{$p2} = %sets{$p2} + $r2;
프로그램에서는 =를 쓰는 장황한 버전보다 += 복합 할당 연산자를 쓰는 축약 버전이 선호돼요. 짧은 버전이 타이핑을 덜 하기 때문만이 아니라, += 연산자가 해시의 키-값 쌍에서 정의되지 않은 값을 조용히 0으로 초기화하기 때문이에요. 다시 말해 +=를 쓰면 %sets{$p1} = 0 같은 별도 문장을 넣지 않아도 $r1을 의미 있게 더할 수 있습니다. 이 동작은 아래에서 좀 더 자세히 보겠습니다.
Any와 +=
%sets{$p1} += $r1;
%sets{$p2} += $r2;
%sets{$p1} += $r1은 왼쪽 변수의 값을 $r1만큼 증가시켜라는 뜻이에요. 첫 반복에서 %sets{$p1}은 아직 정의되지 않아서 Any라는 특별한 값으로 기본 설정됩니다. += 연산자는 정의되지 않은 값 Any를 값이 0인 숫자로 편리하게 취급해서, 다른 값을 의미 있게 더할 수 있게 해 줘요. 덧셈은 숫자 연산이므로, 덧셈을 수행하기 위해 문자열 $r1, $r2 등은 자동으로 숫자로 변환됩니다.
팻 애로우, 쌍, 자동생성 (Fat arrow, pairs, autovivification)
이 두 줄이 실행되기 전에 %sets는 비어 있어요. 아직 없는 항목에 더하면 그 항목이 값 0에서 시작해 그 자리에서(just-in-time) 생겨나요. 이 동작을 *자동생성(autovivification)*이라고 합니다. 이 두 줄이 처음 실행된 뒤 %sets는 'Ana' => 3, 'Dave' => 0을 담아요. (팻 애로우 =>는 Pair에서 키와 값을 구분합니다.)
후위/전위 증가 (Postincrement, preincrement)
if $r1 > $r2 {
%matches{$p1}++;
} else {
%matches{$p2}++;
}
$r1이 $r2보다 숫자로 크면 %matches{$p1}이 1 증가하고, 그렇지 않으면 %matches{$p2}가 증가해요. +=의 경우처럼, 두 해시 값 중 어느 것이든 이전에 없었다면 증가 연산으로 자동생성됩니다. $thing++은 $thing += 1의 변형인데, 후자와 달리 표현식의 반환값이 증가 이전의 $thing이고 증가된 값이 아니라는 점이 달라요. 다른 많은 프로그래밍 언어처럼 ++를 접두사로 쓸 수도 있고, 그때는 증가된 값을 반환합니다: my $x = 1; say ++$x는 2를 출력해요.
토픽 변수 (Topic variable)
my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
이 줄은 각각 단순한 세 단계로 이루어져 있어요. 배열의 sort 메서드는 배열 내용의 정렬된 버전을 반환합니다. 그런데 배열의 기본 정렬은 내용물 자체로 정렬해요. 선수 이름을 우승자 우선 순서로 출력하려면, 코드는 이름이 아니라 선수의 점수로 배열을 정렬해야 합니다. sort 메서드의 인자는 정렬 기준 데이터로 배열 원소(선수 이름)를 변환하는 데 쓰는 블록이에요. 배열 항목은 토픽 변수 $_로 전달됩니다.
블록 (Blocks) — 블록은 이미 봤어요. for 루프의 -> $line { ... }와 if 문 모두 블록으로 동작했죠. 블록은 선택적 시그니처(-> $line 부분)가 있는 자기 완결형 Raku 코드 조각이에요.
선수를 점수로 정렬하는 가장 단순한 방법은 @names.sort({ %matches{$_} })입니다. 이건 이긴 매치 수로 정렬하죠. 그런데 Ana와 Dave는 둘 다 매치 두 개를 이겼어요. 그 단순한 정렬은 이긴 세트 수를 고려하지 않는데, 그게 토너먼트 우승자를 가르는 2차 기준이에요.
안정 정렬 (Stable sort) — 두 배열 항목이 같은 값을 가지면 sort는 그들을 만난 순서 그대로 둡니다. 컴퓨터 과학자들은 이걸 *안정 정렬(stable sort)*이라고 불러요. 프로그램은 Raku sort의 이 성질을 이용해 두 번 정렬함으로써 목표를 달성합니다. 먼저 이긴 세트 수(2차 기준)로, 그다음 이긴 매치 수(1차 기준)로요. 첫 정렬 후 이름 순서는 Beth Charlie Dave Ana입니다. 두 번째 정렬 후에도 그대로인데, 누구도 매치를 덜 이기면서 세트를 더 많이 이긴 경우가 없기 때문이에요. 그런 상황은 특히 큰 토너먼트에서 언제든 가능합니다.
sort는 오름차순, 즉 작은 것에서 큰 것으로 정렬해요. 이는 원하는 순서와 반대죠. 그래서 코드는 두 번째 정렬 결과에 .reverse 메서드를 호출하고, 최종 리스트를 @sorted에 저장합니다.
표준 출력 (Standard output)
for @sorted -> $n {
my $match-noun = %matches{$n} == 1 ?? 'match' !! 'matches';
my $set-noun = %sets{$n} == 1 ?? 'set' !! 'sets';
say "$n has won %matches{$n} $match-noun and %sets{$n} $set-noun";
}
선수와 점수를 출력하기 위해 코드는 @sorted를 순회하면서 $n을 각 선수의 이름으로 설정해요. 이 코드를 "sorted의 각 원소에 대해, $n을 그 원소로 설정하고 블록의 내용을 실행해라"로 읽으면 됩니다. $match-noun 변수는 선수가 매치 하나를 이겼다면 문자열 match를, 0개 이상을 이겼다면 matches를 담아요. 그렇게 하기 위해 삼항(ternary) 연산자(?? !!)를 씁니다. %matches{$n} == 1이 True로 평가되면 match가 반환되고, 그렇지 않으면 matches가 반환돼요. 어느 쪽이든 반환값은 $match-noun에 저장됩니다. $set-noun에도 같은 방식이 적용돼요.
say 문장은 인자를 표준 출력(보통 화면)에 출력하고 줄바꿈을 붙입니다. (끝에 줄바꿈이 싫으면 print를 쓰세요.)
참고로 say는 .gist 메서드를 호출해서 어떤 데이터 구조를 잘라낼 수 있으므로, 정확한 출력이 필요하면 put이 더 안전해요.
변수 보간 (Variable interpolation)
프로그램을 실행하면 say가 그 문자열의 내용을 있는 그대로 출력하지 않는 걸 볼 수 있어요. $n 자리에 변수 $n의 내용, 즉 $n에 저장된 선수 이름을 출력합니다. 코드를 내용물로 자동 치환하는 이 동작을 *보간(interpolation)*이라고 해요. 이 보간은 큰따옴표 "..."로 구분된 문자열에서만 일어납니다. 작은따옴표 '...' 문자열은 보간하지 않아요.
큰따옴표/작은따옴표 문자열 (Double-quoted and single-quoted strings)
my $names = 'things';
say 'Do not call me $names'; # OUTPUT: «Do not call me $names»
say "Do not call me $names"; # OUTPUT: «Do not call me things»
Raku의 큰따옴표 문자열은 $ 시길 변수뿐 아니라 중괄호 안의 코드 블록도 보간할 수 있어요. 중괄호 안에는 임의의 Raku 코드가 나올 수 있으므로, Array와 Hash도 중괄호로 감싸 보간할 수 있습니다.
중괄호 안의 배열은 각 항목 사이에 공백 하나를 넣어 보간돼요. 중괄호 안의 해시는 일련의 줄로 보간됩니다. 각 줄에는 키, 탭 문자, 그 키와 연관된 값, 그리고 마지막 줄바꿈이 들어가요. 이제 그 예시를 보죠. 이 예시에서는 문자열 리스트를 더 쉽게 만드는 특별한 문법도 볼 거예요. <...> quote-words 구조가 그것인데, <와 > 사이에 단어를 넣으면 모두 문자열로 간주되어 각각 큰따옴표 "..."로 감쌀 필요가 없어요.
say "Math: { 1 + 2 }";
# OUTPUT: «Math: 3»
my @people = <Luke Matthew Mark>;
say "The synoptics are: {@people}";
# OUTPUT: «The synoptics are: Luke Matthew Mark»
say "{%sets}";
# OUTPUT (From the table tennis tournament):
# Charlie 4
# Dave 6
# Ana 8
# Beth 4
배열과 해시 변수가 큰따옴표 문자열에 (중괄호 안이 아니라) 직접 나타날 때는, 그 이름 뒤에 후위 원둘레 연산자(postcircumfix operator), 즉 문장을 따르는 괄호 쌍이 붙어 있어야만 보간됩니다. 변수 이름과 후위 원둘레 연산자 사이에 메서드 호출이 있어도 돼요.
젠 슬라이스 (Zen slices)
my @flavors = <vanilla peach>;
say "we have @flavors"; # OUTPUT: «we have @flavors»
say "we have @flavors[0]"; # OUTPUT: «we have vanilla»
# so-called "Zen slice"
say "we have @flavors[]"; # OUTPUT: «we have vanilla peach»
# method calls ending in postcircumfix
say "we have @flavors.sort()"; # OUTPUT: «we have peach vanilla»
# chained method calls:
say "we have @flavors.sort.join(', ')";
# OUTPUT: «we have peach, vanilla»
다른 파일에서 Raku 메서드 호출하기 (Calling a raku method from another file)
다른 파일의 Raku 서브루틴을 호출하려면, 최소한의 raku 모듈 또는 클래스 파일을 만드는 것이 관례예요.
# MyClass.rakumod
class C {
has $.x;
}
그러면 스크립트에서 이 파일을 use할 수 있습니다.
# myscript.raku
use MyClass;
my $instance = C.new(x=>42);
say $instance.x;
-I. 지시어는 raku 인터프리터에게 디렉터리 .에서 모듈을 찾으라고 알려줍니다.
> raku -I. myscript.raku
연습문제 (Exercises)
1. 예시 프로그램의 입력 형식이 중복돼요. 모든 선수의 이름이 담긴 첫 줄은 불필요합니다. 왜냐하면 어느 선수들이 토너먼트에 참가했는지는 이어지는 행들의 이름을 보면 알 수 있으니까요. 그래서 원칙적으로 그 줄을 안전하게 삭제할 수 있어요. @names 변수를 쓰지 않고 프로그램이 동작하게 하려면 어떻게 해야 할까요? 힌트: %hash.keys는 %hash에 저장된 모든 키의 리스트를 반환합니다.
정답: scores.txt에서 첫 줄을 제거한 뒤, (그것을 읽었을) 줄 my @names = $file.get.words;을 지우고:
my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
...이걸 다음으로 바꿉니다:
my @sorted = %sets.keys.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
2. 중복된 @names 변수를 삭제하는 대신, 그 변수를 사용해서 첫 줄에 언급되지 않은 선수가 나타나면(예: 오타 때문에) 경고할 수도 있어요. 그렇게 하려면 프로그램을 어떻게 바꿀까요? 힌트: membership 연산자를 사용해 보세요.
정답: @names를 @valid-players로 바꿔요. 파일의 줄들을 순회할 때 $p1과 $p2가 @valid-players에 있는지 확인합니다. membership 연산자에 대해서는 (elem)과 !(elem)도 쓸 수 있다는 점을 기억하세요.
...;
my @valid-players = $file.get.words;
...;
for $file.lines -> $line {
my ($pairing, $result) = $line.split(' | ');
my ($p1, $p2) = $pairing.split(' ');
if $p1 ∉ @valid-players {
say "Warning: '$p1' is not on our list!";
}
if $p2 ∉ @valid-players {
say "Warning: '$p2' is not on our list!";
}
...
}