トラック
/
Elm
Elm
/
演習
/
パイパーのパイ
パイパーのパイ

パイパーのパイ

学習演習

はじめに

末尾再帰

関数が実行する最後の処理が自分自身の呼び出しであるとき、その関数は末尾再帰的であると言います。

関数が呼び出されるたびに、そのローカル変数と引数を持つ_スタックフレーム_が関数呼び出しスタックの一番上に積まれます。 関数が戻ると、そのスタックフレームはスタックから取り除かれます。

末尾再帰関数では、末尾呼び出し最適化(末尾呼び出し除去とも言います)が可能になります。 これは、前の関数がもうそのスタックフレームを必要としないことが保証されているとき、次の関数呼び出しで最後のスタックフレームを再利用できるようにする最適化です。 これにより、関数呼び出しスタックがあふれる心配を軽減できます。これは、スタック上にあまりにも多くのフレームが積まれ、新しく作るためのメモリが残っていない状態のことです。

Elmにおける末尾呼び出し最適化

Elmコンパイラーは、ある条件を満たすと、JavaScriptへコンパイルする際に末尾呼び出し最適化を自動的に行うことができます。

最適化は、ある分岐の_最後_の処理が、単純な関数適用でその関数自身を呼び出すことによって行われるときに、再帰関数に対して適用されます。 いくつか例を見てみましょう。

factorial : Int -> Int
factorial n =
  if n <= 1 then
    n
  else
    n * factorial (n-1)

上の実装は末尾再帰ではありません。else分岐の最後の処理が掛け算n *だからです。

factorial : Int -> Int
factorial n =
  factorialHelper n n

factorialHelper : Int -> Int -> Int
factorialHelper n resultSoFar =
  if n <= 1 then
    resultSoFar
  else
    factorialHelper (n-1) (n * resultSoFar)

上の実装は末尾再帰であり、最適化されます。else分岐の最後の処理がfactorialHelperの自分自身の呼び出しだからです。 Int -> Intという型シグネチャを持つ関数では、これは不可能です。そのため実際には、ヘルパー関数を定義することで末尾呼び出し最適化を実現することがよくあります。

説明

パイパーは、パイ作りが大好きです。

彼女が名前がきっかけでパイ作りを始めたのか、それとも趣味に合わせて名前を変えたのかは、誰にもわかりません。 一見すると、後者の可能性は低そうに思えますが、実はパイパーはパイにすっかり魅了されているんです。 彼女はいつもキッチンで工夫を重ね、レシピを調整し、腕を磨いていて、友人たちを心から喜ばせています。

彼女が最近夢中になっているもの? それは、できるだけ円に近い、数学的に完璧なパイを焼くこと。そのために頼りにしているのが、彼女の大好きな数、そう、πです。

パイパーは、πを繰り返し計算で求める素敵な公式を見つけました。それがニュートン・オイラー収束変換です。

π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!

πを計算して、パイパーが数学的に完璧なパイを焼くのを手伝いましょう。

1. 階乗

まずはウォーミングアップです。 階乗演算子は、ふつう!と書きます。その定義は次のとおりです。

0! = 1
n! = 1 * 2 * 3 * ... * n

factorial関数を定義して、階乗を末尾再帰で計算できるようにしましょう。

factorial 4
    -- 24

2. 二重階乗

二重階乗演算子は、ふつう!!と書きます。その定義は次のとおりです。

0!! = 1
n!! = 1 * 3 * 5 * ... * n (for odd n)
n!! = 2 * 4 * 6 * ... * n (for even n)

doubleFactorial関数を定義して、階乗を末尾再帰で計算できるようにしましょう。

factorial 5
    -- 15
factorial 6
    -- 48

3. ニュートン・オイラー収束変換

pipersPi関数を定義しましょう。これは、ニュートン・オイラー収束変換の公式から指定された項数を使って、πを末尾再帰で近似します。

π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!

まずは最初の項を一緒に計算してみましょう。 上限を(無限大ではなく)0にすると、次のようになります。

π / 2 ≈ Sum for k from 0 to 0 of ( k! ) / ( 2 * k + 1 )!!
π / 2 ≈ ( 0! ) / ( 2 * 0 + 1 )!!
π / 2 ≈ 0! / 0!!
π / 2 ≈ 1 / 1
π / 2 ≈ 1
π ≈ 2

項を1つ増やすごとに、近似の精度が上がります。

pipersPi 0
    -- 2.0
pipersPi 1
    -- 2.6666666
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Elm Exercism

パイパーのパイを始める準備はできましたか?

Exercismに登録すれば、28個のコンセプト110個の演習、そして本物の人間によるメンタリングとともに、Elmを学んでマスターできます。すべて無料です。