수학

수학 (Math)

Raku는 수학을 곧장 언어 문법으로 끌어안은 언어예요. 집합 연산에 자연스러운 기호(∩, ∪)를 쓰고, 무한 수열을 쓸 수 있으며, 상미분방정식을 수치 적분하는 것까지 Raku의 표현력으로 풀어낼 수 있어요. 이 문서에서는 Raku의 숫자 타입 체계부터 집합, 수열, 수학 상수, 그리고 응용 수학에서 자주 쓰는 모델 시뮬레이션까지 차근차근 살펴볼게요.

출처: Raku Documentation — Math

본문

산술 (Arithmetic)

Raku는 서로 다른 데이터 타입을 사용해 산술 연산을 할 수 있어요. Num, Rat, Complex는 모두 덧셈·뺄셈·곱셈·나눗셈 연산 아래에서 체(field)처럼 동작할 수 있어요. (엄밀히 말하면 부동소수점 표현을 다루는 데이터 타입은 산술 과정의 본질적인 부정확성 때문에 수학적 의미의 체는 아니에요. 하지만 대부분의 경우에 그런 수학적 대상에 충분히 가까운, 컴퓨터 친화적인 버전이라고 볼 수 있죠.) 대응하는 수학적 체는 다음과 같아요:

Raku 클래스 체 (Field)
Rat
Num
Complex

정수, 즉 수학에서 보통 ℤ라고 부르는 Int는 수학적 체가 아니라 환(ring)이에요. 곱셈 역원에 대해 닫혀 있지 않기 때문이죠. 다만 정수 나눗셈 div를 쓰면 연산 결과가 항상 정수예요. 반면 /를 쓰면 일반적으로 결과가 Rat이 돼요.

게다가 Int는 무한 정밀도 산술을 할 수 있어요. (정확히는 메모리가 허용하는 만큼 무한에 가깝죠. Numeric overflow가 여전히 발생할 수 있어요.) 숫자가 너무 커도 Num으로 넘어가지 않아요:

my @powers = 2, 2 ** * ... Inf; say @powers[4].chars; # OUTPUT: «19729␤»

엄밀히 말하면 수학적 체처럼 동작하는 유리수 클래스는 FatRat예요. 효율을 위해 Rat로 연산하다가 숫자가 충분히 크거나 분자·분모 차이가 커지면 Num으로 넘어가요. FatRat은 기본 Int 클래스처럼 임의 정밀도로 동작할 수 있어요.

생태계의 몇몇 모듈은 추가 데이터 타입을 수학적으로 다룰 수 있어요:

숫자는 그것이 실제로 나타내는 숫자 클래스에 자동으로 덕타이핑(duck-typing)돼요:

.^name.say for (4, ⅗, 1e-9, 3+.1i); # OUTPUT: «Int␤Rat␤Num␤Complex␤»

산술 연산은 피연산자의 타입을 고려해 수행돼요:

say .33-.22-.11 == 0; # OUTPUT: «True␤»

이 경우 모든 숫자가 Rat로 해석되어 연산이 정확해요. 일반적으로 대부분의 다른 언어는 이 숫자들을 부동소수점으로 해석하는데, 필요하다면 Raku에서도 그렇게 할 수 있어요:

say .33.Num -.22.Num - .11.Num; # OUTPUT: «1.3877787807814457e-17␤»

이런 경우를 위해 Raku에는 대략 같음 연산자 도 있어요:

say .33.Num -.22.Num - .11.Num ≅ 0; # OUTPUT: «True␤»

집합 (Sets)

Raku에는 Set 데이터 타입이 있고, 대부분의 집합 연산도 지원해요. 합집합과 교집합은 네이티브 연산일 뿐 아니라, 그 자연스러운 기호인 ∩과 ∪를 그대로 사용해요. 예를 들어 이 코드는 제한된 수의 집합에 대해 집합 산술의 기본 법칙을 검사해요:

my @arbitrary-numbers = ^100;
my \U = @arbitrary-numbers.Set;

my @sets;

@sets.push: Set.new( @arbitrary-numbers.pick( @arbitrary-numbers.elems.rand)) for @arbitrary-numbers;

my (@union, @intersection);

for @sets -> $set {
    @union.push: $set ∩ $set === $set;
    @intersection.push: $set ∪ $set === $set;
}

say "Idempotent union is ", so @union.all;
# OUTPUT: «Idempotent union is True␤»
say "Idempotent intersection is ", so @intersection.all;
# OUTPUT: «Idempotent intersection is True␤»
my (@universe, @empty-set, @id-universe, @id-empty);

for @sets -> \A {
    @universe.push: A ∪ U === U;
    @id-universe.push: A ∩ U === A;
    @empty-set.push: A ∩ ∅ === ∅;
    @id-empty.push: A ∪ ∅ === A;
}

say "Universe dominates ", so @universe.all;    # OUTPUT: «Universe dominates True␤»
say "Empty set dominates ", so @empty-set.all;  # OUTPUT: «Empty set dominates True␤»

say "Identity with U ", so @id-universe.all;    # OUTPUT: «Identity with U True␤»
say "Identity with ∅ ", so @id-empty.all;       # OUTPUT: «Identity with ∅ True»

이 코드는 Raku가 이미 정의해둔 공집합을 사용해 집합 대수의 등식이 성립하는지 검사해요. 그뿐 아니라 시길리스(sigilless) 변수와 집합 연산자의 유니코드 형태를 이용해 원래 수식에 최대한 가까운 표현을 썼어요. 예컨대 A ∪ U === U값 동일성 연산자 ===를 쓴 것만 빼면 위키백과 항목의 실제 수학 표현과 거의 같아요.

드 모르간(De Morgan)의 법칙도 아래처럼 테스트할 수 있어요:

my @alphabet = 'a'..'z';
my \U = @alphabet.Set;
sub postfix:<⁻>(Set $a) { U ⊖ $a }
my @sets;
@sets.push: Set.new( @alphabet.pick( @alphabet.elems.rand)) for @alphabet;
my ($de-Morgan1,$de-Morgan2) = (True,True);
for @sets X @sets -> (\A, \B){
    $de-Morgan1 &&= (A ∪ B)⁻  === A⁻ ∩ B⁻;
    $de-Morgan2 &&= (A ∩ B)⁻  === A⁻ ∪ B⁻;
}
say "1st De Morgan is ", $de-Morgan1;
say "2nd De Morgan is ", $de-Morgan2;

여집합(complement) 연산으로 선언하는데, 이는 전체집합 U와 우리 집합 사이의 대칭차(symmetrical difference) ⊖를 계산해요. 일단 선언하면 (A ∪ B)⁻처럼 A와 B의 합집합의 여집합 같은 연산을 원래 수학 표기법에 가까운 형태로 표현하기 쉬워져요.

수열 (Sequences)

수열(sequence)은 반복을 허용하는 순서 있는 객체 모음이에요. Raku에서는 Seq라는 일급 데이터 타입이에요. Seq는 자연수처럼 무한 수열도 표현할 수 있어요:

my \𝕟 = 1,2 … ∞;
say 𝕟[3];         # OUTPUT: «4␤»

무한 수열은 끝맺음으로 ∞, Inf 또는 *(Whatever)를 사용해요. 은 리스트 생성자(list generator)인데, 처음 몇 숫자만 넣어주면 등차수열과 등비수열까지 이해할 수 있어요:

say 1,5,9 … * > 100;
# OUTPUT: «(1 5 9 13 17 21 25 29 33 37 41 45 49 53 57 61 65 69 73 77 81 85 89 93 97 101)␤»
say 1,3,9 … * > 337; # OUTPUT: «(1 3 9 27 81 243 729)␤»

첫 수열은 생성된 숫자가 100보다 커질 때 종료되고, 등비수열인 두 번째 수열은 337보다 커질 때 종료돼요.

임의의 생성기를 쓸 수 있다는 점 덕분에 피보나치 수 같은 수열도 쉽게 만들 수 있어요:

say 1,1, * + * … * > 50;#  OUTPUT: «(1 1 2 3 5 8 13 21 34 55)␤»

실제로 이 방식으로 황금비의 근삿값을 계산할 수도 있어요:

my @phis = (2.FatRat, 1 + 1 / * ... *);
my @otherphi = (1 - @phis[200], 1 + 1 / * ... *);
say @otherphi[^10, |(20, 30 ... 100)];  # OUTPUT:
# «((-0.61803398874989484820458683436563811772030918
# -0.61803398874989484820458683436563811772030918
# -0.61803398874989484820458683436563811772030918
# -0.61803398874989484820458683436563811772030918
# -0.61803398874989484820458683436563811772030918
# -0.618033…»

Math::Sequences 모듈은 이미 정의된 많은 수학 수열을 제공해요. 백과사전의 수열들 중 일부는 ℤ처럼 원래 이름을 그대로 가지기도 해요.

일부 집합 연산자는 수열에도 적용되며, 객체가 그 수열의 일부인지 알아내는 데 쓸 수 있어요:

say 876 ∈ (7,14 … * > 1000) ; # OUTPUT: «False␤»

이 특별한 경우에는 876이 7의 배수인지 바로 알 수 있지만, 복잡한 생성기를 쓰는 다른 수열에도 같은 원리가 적용돼요. 그리고 집합 포함(inclusion) 연산자도 쓸 수 있어요:

say (55,89).Set ⊂ (1,1, * + * … * > 200); # OUTPUT: «True␤»

다만 이는 실제로 부분수열(subsequence)인지까지 따지지는 않아요. 단지 두 요소가 존재하는지만 보는 거예요. 집합은 순서가 없으므로, 부분수열을 명시적으로 Set으로 캐스팅하지 않아도(또는 Seq로 캐스팅하지 않아도) 포함 연산자를 적용하기 위해 자동으로 강제(coerce)돼요.

수학 상수 (Mathematical constants)

Raku는 수학 상수 모음을 포함해요:

say π; # OUTPUT: «3.141592653589793␤»
say τ; # Equivalent to 2π; OUTPUT: «6.283185307179586␤»
say 𝑒; # OUTPUT: «2.718281828459045␤»

이 상수들은 ASCII 등가 e, pi, tau로도 사용할 수 있어요.

Math::Constants 모듈은 앞에서 언급한 황금비 φ나 플랑크 상수 ℎ 같은 추가 물리·수학 상수들을 포함해요.

Raku는 유니코드 그래핌을 사용하는 변수 정의와, 시길 없이 만든 변수·상수 이름까지 허용하므로, 가능하면 개념의 실제 수학적 이름으로 명명하는 것이 좋은 관례로 여겨져요.

상미분방정식의 수치 적분 (Numerical integration of ordinary differential equations)

Raku는 놀라운 프로그래밍 언어라서 멋진 수학을 많이 할 수 있어요. 응용 수학자 작업의 상당 부분은 자신이 만든 모델을 시뮬레이션하는 거예요. 그래서 어떤 코딩 언어든 수치 적분기(numerical integrator)는 필수예요. 이것을 Raku에서 하는 법을 배우는 건 아주 유용해요.

요구사항 (Requirements)

생태계에는 이 작업을 수월하게 해주는 Raku 모듈이 몇 가지 있어요:

  • Math::Model — 수학·물리 모델을 쉽고 자연스럽게 작성하게 해줌.
  • Math::RungeKutta — 상미분 선형 방정식 시스템을 위한 Runge-Kutta 적분.

이 예시에서는 유용한 문법을 가진 Math::Model을 쓰겠지만, 이 모듈이 Math::RungeKutta도 요구한다는 점을 기억하세요. 예시를 쓰기 전에 _zef_로 두 모듈을 설치하기만 하면 돼요.

맬서스 모델 (Malthus model)

수리 생태학(mathematical Ecology)의 _'Hello World'_라 할 수 있는 맬서스 성장 모델부터 시작해볼게요. 맬서스 성장 모델은 단순 지수 성장 모델(simple exponential growth model)이라고도 하는데, 함수가 함수가 자라는 속도에 비례한다는 아이디어에 기반한 본질적으로 지수 성장이에요. 방정식은 이렇게 생겼죠:

dx/dt = g*x

x(0) = x₀

여기서 _g_는 인구 증가율(population growth rate)이고, 때로는 맬서스 매개변수(Malthusian parameter)라고 불러요.

이걸 Raku로 어떻게 옮길까요? Math::Model이 아주 이해하기 쉬운 방식으로 도와줘요:

use Math::Model;

my $m = Math::Model.new(
    derivatives => {
        velocity => 'x',
    },
    variables   => {
        velocity           => { $:growth_constant * $:x },
        growth_constant    => { 1 }, # basal growth rate
    },
    initials    => {
        x       => 3,
    },
    captures    => ('x'),
);

$m.integrate(:from(0), :to(8), :min-resolution(0.5));
$m.render-svg('population growth malthus.svg', :title('population growth'));

무슨 일이 일어나는지 완전히 이해하려면 단계별로 살펴볼게요.

단계별 설명 (Step by step explanation)

  • 먼저 계산을 수행하는 모듈 Math::Model을 로드해요.
use Math::Model;
  • 모든 정보를 담기 위해 모델을 생성해요.
my $m = Math::Model.new(
  • 모델에 있는 도함수(derivative)들을 선언해요. 방정식을 기억한다면 변수 _x_와 그 도함수 x'(보통 velocity라고 부름)가 있어요.
derivatives => {
 velocity => 'x',
},
  • 그 다음에 우리 모델이 어떻게 진화하는지 선언해요. 적분 변수가 아닌 도함수(이 경우엔 _x_만)를 위한 공식과, 공식에서 쓰는 다른 변수(성장률)를 위한 공식만 있으면 돼요.
variables => {
 velocity => { $:growth_constant * $:x},
 growth_constant => { 1 }, # basal growth rate
},
  • 마지막으로 초기 조건(initial conditions)을 선언하고, _captures_를 써서 시뮬레이션이 돌아가는 동안 Math::Model이 어떤 변수를 기록할지 알려줘요.
initials => {
 x => 3,
},
captures => ('x'),

이 시점에서 모델이 준비됐어요. 시뮬레이션을 실행하고 결과를 멋진 플롯으로 렌더링하면 돼요:

$m.integrate(:from(0), :to(8), :min-resolution(0.5));
$m.render-svg('population growth malthus.svg', :title('population growth'));

여기서 시간 범위와 해상도(resolution)를 고르고, 다음으로 플롯을 생성해요. 다 이해됐죠? 그럼 결과를 볼게요!

plot 이미지

멋져 보이죠! 솔직히 말하면 꽤 대표성이 떨어져요. 더 복잡한 상황의 다른 예시를 살펴볼게요!

로지스틱 모델 (Logistic model)

자원은 무한하지 않고 인구도 영원히 자라지 않아요. P-F 베르훌스트(Verhulst)도 같은 생각을 했고, 그래서 로지스틱 모델을 제시했어요. 이 모델은 재생산률이 기존 인구와 이용 가능한 자원의 양에 모두 비례하는 흔한 인구 성장 모델이에요 (다른 조건이 같을 때). 이렇게 생겼죠:

dx/dt = gx(1-x/k)

x(0)=x₀

여기서 상수 g는 성장률, k는 수용능력(carrying capacity)을 정의해요. 위 코드를 수정하면 시간에 따른 행동을 시뮬레이션할 수 있어요:

use Math::Model;

my $m = Math::Model.new(
    derivatives => {
        velocity => 'x',
    },
    variables   => {
        velocity           => { $:growth_constant * $:x - $:growth_constant * $:x * $:x / $:k },
        growth_constant    => { 1 },   # basal growth rate
        k                  => { 100 }, # carrying capacity
    },
    initials    => {
        x       => 3,
    },
    captures    => ('x'),
);

$m.integrate(:from(0), :to(8), :min-resolution(0.5));
$m.render-svg('population growth logistic.svg', :title('population growth'));

우리의 멋진 플롯을 볼게요: plot 이미지

보시다시피 인구는 최대치까지 성장해요.

강한 알리 효과 (Strong Allee Effect)

흥미롭죠? 이 방정식들이 기본적으로 보여도 종양 성장 같은 세상의 많은 행동과 연결돼 있어요. 끝내기 전에 흥미로운 경우 하나를 보여드릴게요. 로지스틱 모델이 정확할 수 있지만... 어떤 임계점부터 인구 규모가 너무 작아져서 개체들이 서로를 찾지 못해 생존율이나 재생산률이 떨어지면 어떻게 될까요?

이건 W.C. 알리(Allee)가 설명한 흥미로운 현상으로, 보통 알리 효과로 알려져 있어요. 이 효과와 관련된 다양한 행동을 얻는 간단한 방법은 로지스틱 모델을 출발점으로 삼아 항을 추가하고, 다음과 같은 3차 성장 모델을 얻는 거예요:

dx/dt=rx(x/a-1)*(1-x/k)

여기서 상수들은 모두 이전과 같고, A를 임계점(critical point)이라고 불러요.

우리 코드는 이렇게 돼요:

use Math::Model;

my $m = Math::Model.new(
    derivatives => {
        velocity_x    => 'x',
    },
    variables   => {
        velocity_x           => { $:growth_constant * $:x *($:x/$:a -1)*(1- $:x/$:k) },
        growth_constant    => { 0.7 },   # basal growth rate
        k                  => { 100 }, # carrying capacity
        a                  => { 15 },  # critical point
    },
    initials    => {
        x       => 15,
    },
    captures    => ('x'),
);

$m.integrate(:from(0), :to(100), :min-resolution(0.5));
$m.render-svg('population growth allee.svg', :title('population growth'));

이걸 직접 실행해서 임계점 주변의 초기 조건을 바꿀 때 나타나는 다양한 행동을 확인해보세요!

약한 알리 효과 (Weak Allee Effect)

강한 알리 효과가 임계 인구 규모 또는 밀도를 가진 인구통계학적 알리 효과라면, 약한 알리 효과는 임계 인구 규모 또는 밀도가 없는 인구통계학적 알리 효과예요. 즉 약한 알리 효과를 보이는 인구는 더 낮은 인구 밀도나 규모에서 1인당 성장률이 줄어들지만, 그 낮은 인구 규모·밀도에서도 항상 양의 1인당 성장률을 나타내요. 이 모델은 강한 모델과 약간 달라서 새 공식을 얻어요:

dx/dt=rx(1-x/k)*(x/a)ⁿ, with n>0

우리 코드는 이렇게 돼요:

use Math::Model;

my $m = Math::Model.new(
    derivatives => {
        velocity_x => 'x',
    },
    variables   => {
        velocity_x           => { $:growth_constant * $:x *(1- $:x/$:k)*($:x/$:a)**$:n },
        growth_constant    => { 0.7 },   # basal growth rate
        k                  => { 100 }, # carrying capacity
        a                  => { 15 },  # critical point
        n                  => { 4  }
    },
    initials    => {
        x       => 15,
    },
    captures    => ('x'),
);

$m.integrate(:from(0), :to(100), :min-resolution(0.5));
$m.render-svg('population growth allee.svg', :title('population growth'));

추가 정보 (Extra info)

물리를 좋아하시나요? 사용한 모듈의 창시자인 Moritz Lenz의 원본 게시물을 확인해보세요: https://perlgeek.de/blog-en/perl-6/physical-modelling.html.