무엇을 바꾸려 했는가 — 0.1 + 0.2 == 0.3이 True가 되는 언어

Raku에서는 0.1 + 0.2 == 0.3이 True를 반환한다. 소수 리터럴이 기본적으로 유리수이기 때문이다. 이 한 줄은 Perl 6이 무엇을 우선했는지를 잘 보여준다 — 순순히 썼을 때 놀라지 않는 것. 시길 불변성, grammar, 다중 디스패치, 메타 연산자, Junction, 지연 리스트. 늘어놓으면 알 수 있다. 어느 것이나 언어 하나 분량의 일이었다. 15년이 걸린 이유의 절반은 여기에 있다.

perlrakulanguage-designtype-systemoperatorsprogramming-languages

먼저 한 줄

say 0.1 + 0.2 == 0.3;   # True

많은 언어에서 False가 되는 이 식이, Raku에서는 True를 반환한다.

이유는 단순해서, **Raku의 소수 리터럴은 기본적으로 유리수(Rat)**이기 때문이다. 0.1은 「1/10에 가장 가까운 배정밀도 부동소수점 수」가 아니라, 분수 1/10 그 자체로 유지된다. 부동소수점이 필요하면 Num을 명시한다.

이 판단에는 대가가 있다. 유리수 연산은 부동소수점보다 느리다. 분모가 커지면 정밀도 관리도 필요하다. 그럼에도 기본값을 이쪽으로 한 것은, 순순히 썼을 때 놀라지 않는 것을 우선했기 때문이다.

이 한 줄이, Perl 6의 설계 전체의 축소판이 되어 있다. 올바름을, 구현 비용으로 산다. 이번 회는 그 쇼핑 목록이다.

1. 시길 불변성 — 가장 유명한 비호환

Perl 5에서는, 변수의 시길이 접근하는 방식에 따라 바뀐다.

my @array = (1, 2, 3);
my $first = $array[0];      # @가 아니라 $
my @slice = @array[0, 1];   # 여기는 @

규칙으로서는 일관되어 있다. 「시길은 꺼내지는 값의 개수를 나타낸다」. 그러나 초학자가 가장 걸려 넘어지는 점이기도 했다.

Perl 6은 반대의 규칙을 택한다.

my @array = 1, 2, 3;
my $first = @array[0];      # @ 그대로

「시길은 변수의 타입이지, 문맥이 아니다.」

단 하나의 규칙 변경이지만, 이것으로 Perl 5의 코드는 문법 수준에서 돌아가지 않게 된다. 호환성을 끊는다는 결단이, 여기에 집약되어 있다.

2. twigil — 시길 다음의 한 글자

Perl 6은 시길 뒤에 한 글자를 더 놓을 수 있다. 변수의 종류를 철자로 나타내는 구조다.

twigil 의미
! 클래스의 속성(private) $!name
. 속성의 접근자 경유 $.name
* 동적 변수(호출 측에서 거슬러 찾는다) $*IN, $*CWD
? 컴파일 타임 상수 $?FILE, $?LINE
^ 자동 선언되는 위치 매개변수 { $^a + $^b }

보는 것만으로 스코프의 종류를 알 수 있다. Perl 5의 local(동적 스코프)이 $*로 정리된 것이 가장 알기 쉬운 예다.

3. OO를 언어에 넣는다

2화에서 쓴 대로, Perl 5의 OO는 「기존 부품의 조합」이었다. bless는 함수이고, new는 관습이다.

Perl 6은 class / has / method / role언어 기능으로 가진다.

class Point {
    has $.x = 0;
    has $.y = 0;
    method to-string { "($!x, $!y)" }
}

주목할 것은 role이다. 다중 상속의 대안으로 도입된 것으로, Smalltalk의 Traits 연구에서 유래한다. 상태와 행위를 합성 가능한 단위로 묶고, 충돌은 컴파일 타임에 검출된다.

그리고 이 오브젝트 모델은, 완성을 기다리지 않고 Perl 5로 역류했다. Moose다. 2화에서 언급했지만, Moose는 Perl 6의 오브젝트 모델의 이식이었다. Perl 6의 설계는, 그 자체가 출시되기 전에 Perl 5를 바꾸고 있었다.

4. 타입을 쓸 수 있게 한다(다만 임의)

sub add(Int $a, Int $b --> Int) { $a + $b }

쓰지 않아도 되지만, 쓰면 런타임에 검사된다. 점진적 타이핑이다.

나아가 subset으로 술어가 붙은 타입을 만들 수 있다.

subset Even of Int where * %% 2;
subset Positive of Numeric where * > 0;

sub half(Even $n) { $n div 2 }
half(4);   # OK
half(3);   # 타입 제약 에러

where 절에 임의의 술어를 쓸 수 있다. 검증을 함수의 입구가 아니라, 타입의 정의 쪽에 둘 수 있다.

1화에서 든 「쉬운 것은 쉽게, 어려운 것은 가능하게」의 타입판에 해당한다.

5. 다중 디스패치

같은 이름의 함수를, 인자의 타입 · 개수 · 제약으로 가려 쓴다.

multi greet(Str $name)          { "Hello, $name" }
multi greet(Int $times)         { "Hi " x $times }
multi greet(Str $name, Int $n)  { "Hello, $name" x $n }
multi greet($x where * < 0)     { "Negative!" }

Java나 C++의 오버로드가 정적인 타입으로 해결되는 데 비해, Raku는 런타임의 값의 타입으로 해결한다. Common Lisp의 CLOS에 가깝다.

후보가 여럿 매치될 때는 더 한정적인 후보가 이긴다. where 제약이 붙은 후보는, 타입만 있는 후보보다 한정적인 것으로 취급된다.

6. 연산자를 메타로 다룬다

Perl 6의 특징적인 발명이 메타 연산자다. 연산자를 받아서, 연산자를 만든다.

메타 연산자 의미
reduce [ ] [+] 1..10 접기. 55
hyper »« @a »+» 1 각 요소에 적용
cross X @a X @b 곱집합
zip Z @a Z @b 대응짓기
negate ! !== 부정판을 자동 생성
assign = min= 대입판을 자동 생성

연산자를 N개 더하는 것이 아니라, 연산자를 만드는 규칙을 더한다.

여기에는 Perl 5에 대한 반성이 있다. Perl 5는 연산자를 추가하는 방향으로 부풀었다. 3화에서 쓴 「RFC는 『더하고 싶다』만 모인다」는 편향에 대한, 설계 측의 답이기도 하다.

여기에, 이 연재에서 네 번째의 「형(型)」이 있다.

더하는 대상을, 기능에서 규칙으로 끌어올린다. 기능은 더할수록 늘어나지만, 규칙은 더해도 늘어나지 않는다.

7. Junction — 값의 중첩

my $x = 2;
my @list = 1, 2, 3;

if $x == any(@list) { say '어느 하나와 같다' }   # 출력된다
if $x >  all(@list) { say '전부보다 크다' }      # 출력되지 않는다

say    $x == any(@list);   # any(False, True, False) — 분배된 결과가 그대로 보인다
say so $x == any(@list);   # True — 진위값 문맥에서 접힌다

any / all / one / none이며, 비교하면 자동으로 분배된다. say하면 분배 결과가 그대로 보이고, ifso의 진위값 문맥에서 비로소 접힌다. 양자역학의 중첩에 비유되는 기능이다.

타입 시스템에도 영향을 주고 있어, Junction은 Any 아래에 있지 않다. 그래서 「보통의 타입」을 받는 함수에 Junction을 넘기면, 자동으로 분배가 일어난다. 언어의 타입 계층에, 이 기능을 위한 자리가 확보되어 있다.

8. 지연 리스트와 무한 수열

my @fib = 1, 1, * + * ... *;   # 무한 피보나치
say @fib[^10];                  # (1 1 2 3 5 8 13 21 34 55)

...은 수열 연산자, 끝의 *(Whatever)는 「무한히」를 나타낸다. 리스트가 기본적으로 지연 평가되므로, 무한 수열을 변수에 넣고, 나중에 필요한 만큼 꺼낼 수 있다.

9. 문법 자체를 바꿀 수 있다

Raku의 파서는 Raku의 grammar로 쓰여 있다. 그리고 사용자가 컴파일 타임에 문법을 확장할 수 있다.

sub infix:<∈>($x, @set) { $x (elem) @set }
say 2 ∈ (1, 2, 3);     # True

infix:<...> / prefix:<...> / postfix:<...>라는 이름으로 연산자를 정의할 수 있고, 결합성과 우선순위도 지정할 수 있다. 정의한 순간, 그것은 정말로 구문이 된다.

grammar 자체에 대해서는 11화에서 다룬다. Raku에서 가장 다른 언어에 없는 것이다.

늘어놓아 보면 알 수 있는 것

여기까지 9항목을 늘어놓았다. 다시 한 번 봐 주었으면 한다.

  • 유리수를 기본값으로 — 수치 타워의 설계
  • 시길 불변성 — 언어의 겉모습의 전면 변경
  • OO를 언어에 넣는다 — 오브젝트 모델과 MOP의 설계
  • 점진적 타이핑 — 타입 시스템의 설계
  • 다중 디스패치 — 디스패치 기구의 설계
  • 메타 연산자 — 연산자 체계의 설계
  • Junction — 타입 계층에 새로운 주민의 추가
  • 지연 리스트 — 평가 전략의 변경
  • 문법 확장 — 파서를 사용자에게 개방

어느 하나만 해도, 언어 하나 분량의 일이다.

게다가 서로 독립적이지 않다. Junction은 타입 계층에 영향을 주고, 지연 평가는 다중 디스패치의 해결에 영향을 주고, 문법 확장은 전부에 영향을 준다. 조합의 수만큼, 정해야 할 것이 있다.

3화의 마지막에 「15년이 걸린 이유는 넷 있다」고 썼다. 그 첫 번째가 이것이다.

설계가 너무 웅대했다.

이것은 태만의 이야기가 아니다. 하려고 한 일의 양의 이야기다. 그리고 하려고 한 것은 대체로 실현되었다. 15년을 들여서.

다음 회부터는, 그것을 실제로 움직이려 한 사람들의 이야기로 옮긴다. 우선, 5년 가까이 「돌아가는 것」이 없었다는 사실부터.


다음 회(5화): 구현이 없던 4년 반과, Pugs의 1년. 2005년 2월, Haskell로 쓰인 Perl 6이 나타난다. 그것은 돌아갈 뿐 아니라, 사양과 구현의 관계 자체를 다시 만들었다.

← Back to Perl과 Raku의 계보