トラック
/
Factor
Factor
/
演習
/
司書の台帳
司書の台帳

司書の台帳

学習演習

はじめに

シーケンスを1つの値にまとめたいこともあれば、そのまとめ方が途中で生み出すすべての中間の値を見たいこともあります。Factorはこれを2つの道具に分けています。1つの値に畳み込むsequencesのreduceと、途中経過を求めるmath.statisticsの累積系の関数です。

reduce:汎用的な畳み込み

reduce ( seq init quot: ( prev elt -- next ) -- result )

reduceはシーケンスを1つずつたどりながら、途中結果(アキュムレーター)を持ち運び、それを2引数のクォーテーションに渡します。クォーテーションはその途中結果と次の要素を受け取り、スタックに残したものが新しいアキュムレーターになります。

USING: math sequences ;

{ 1 2 3 4 } 0 [ + ] reduce .         ! => 10
{ 1 2 3 4 } 1 [ * ] reduce .         ! => 24

0以外の初期値と独自の結合方法は、sumやproductでは手が届かないreduceの領域です。たとえば、シーケンスの中で最大の値です。それを上回る値が1つもなければ、既定値が使われます。

USING: math.order ;

{ 3 1 -4 5 -2 } 0 [ max ] reduce .   ! => 5
{ -3 -1 -4 }    0 [ max ] reduce .   ! => 0

初期値の0も比較に加わります。すべての要素が負けると0が結果になるので、値がすべて負のシーケンスでも、任意の最小値ではなく0が結果になります。

累積リダクション

最終結果だけでなく、すべての中間結果が欲しいこともあります。math.statisticsの累積系は、入力と同じ長さのシーケンスを返します。それぞれの位置には、先頭からその位置までを縮約した結果が入ります。

cum-sum     ( seq -- newseq )    ! running total
cum-product ( seq -- newseq )    ! running product
cum-min     ( seq -- newseq )    ! running minimum
cum-max     ( seq -- newseq )    ! running maximum
USING: math.statistics ;

{ 3 1 4 1 5 9 2 6 } cum-sum .        ! => { 3 4 8 9 14 23 25 31 }
{ 1 2 3 4 } cum-product .            ! => { 1 2 6 24 }
{ 3 1 4 1 5 9 2 6 } cum-min .        ! => { 3 1 1 1 1 1 1 1 }
{ 3 1 4 1 5 9 2 6 } cum-max .        ! => { 3 3 4 4 5 9 9 9 }

便利なパターンが、連鎖させた累積リダクションです。1つの出力がそれ自体シーケンスなので、そのまま次に渡せます。これで「途中経過の途中経過」を2語で表せます。組み合わせは自由です。各ステップが何をまとめているかに応じて組み合わせてください。

produce:展開

reduceはシーケンスを消費して1つの値にします。produce(sequences)はその逆で、種からシーケンスを生成します。そのとき、テストとステップを繰り返します。

produce ( pred quot -- seq )

各繰り返しでは、まず現在の状態に対してpredを実行します。真とみなせる値が返れば、quotが呼ばれて次の要素を作り、状態を更新します。predがfを返すと繰り返しは止まり、集めた要素が返されます。

古典的な例がフィボナッチ数列です(それぞれの数は直前の2つの和です)。途中の状態はペア(a, b)です。各ステップはbを出力し、ペアを(b, a + b)に置き換えます。

USING: kernel math sequences ;

! Fibonacci numbers strictly below 100:
0 1 [ dup 100 < ] [ tuck + over ] produce 2nip .
! => { 1 1 2 3 5 8 13 21 34 55 89 }

途中の状態は2つの値にまたがるので、本体ではtuck(kernel)を使ってペアを進めます。tuckは3要素を並べ替えて、いちばん上の値を2番目の下に複製するものです。最後は2nip(こちらもkernelにあり、nipの2要素版です)で後片付けをします。呼び出しを左から右に読むと、次のようになります。

  • 述語[ dup 100 < ]はペアのいちばん上(次に出力する数)をのぞき、それが上限を下回っている間は続けます。
  • 本体[ tuck + over ]は状態を(b, a + b)に進めてbを出力し、スタックに3つの値を残します。下が新しいペアで、上が出した数です。
  • produceが止まったあと、後ろに残った2つの値(最後のペア)は2nipで捨てられ、生成されたシーケンスだけが残ります。

produceはreduceのちょうど双対です。reduceがシーケンスを1つの値に畳み込むのに対し、produceは1つの値からシーケンスへと展開します。

説明

図書館の司書として、利用者アカウントの台帳をつけています。毎週、机の上には2種類の仕事が届きます。

  • リクエストの待ち行列。利用者が適用を求めるクレジット(本の返却、支払い済みの延滞金)と、システムが記録した新しいデビット(新たに発生した延滞金)です。利用者のアカウントはクレジット保護されており、利用者を赤字にしてしまうほど大きなクレジットでも、負っている分までしか適用されません。そのため、途中の残高がゼロを下回ることはありません。
  • トランザクションのリスト。アカウントにすでに記録されている項目です。正の金額はデビット(新しい延滞金)、負の金額はクレジット(支払い)です。

毎週、帳簿を集計します。リクエストをすべて適用したあとの最終残高、トランザクションから計算する日ごとの残高の推移、そして延滞金が急増した期間を見つけるための、これまでの最低水準です。

1. リクエストの待ち行列を処理する

openingの残高とrequestsの配列(符号付きの金額)を受け取り、各リクエストを順に適用したあとの最終残高を返すように、protected-balanceを定義してください。残高をゼロより下に押し下げるような引き出しは、利用可能な金額までしか適用されません。そのため、途中の残高はゼロで下げ止まります。

100 { 50 -200 30 } protected-balance .
! => 30

500 { 100 -300 -250 } protected-balance .
! => 50

0 { -10 50 } protected-balance .
! => 50

2. 残高の推移

transactionsの配列を受け取り、同じ長さの数列を返すようにrunning-balanceを定義してください。そのi番目の要素は、最初のi+1件のトランザクションを終えた時点の残高です(開始残高をゼロとします)。

{ 50 -30 -20 100 } running-balance .
! => { 50 20 0 100 }

3. これまでの最低残高

transactionsの配列を受け取り、同じ長さの数列を返すようにleast-balance-so-farを定義してください。そのi番目の要素は、位置iまで(iも含めて)に見られた最も低い残高です。これが、これまでの最低水準です。アカウントが危険な状態に見えた日を見つけるのに役立ちます。

{ 50 -30 -20 100 } least-balance-so-far .
! => { 50 20 0 0 }

{ 200 -50 -100 -200 } least-balance-so-far .
! => { 200 150 50 -150 }

4. 目標値に届くまで半分にする

図書館では延滞金の免除プログラムを実施しています。利用者の未払い残高は、免除のしきい値以下になるまで、支払い期間ごとに半分にされます。principalとtargetを受け取り、半分にした値の数列(整数除算を使います)を返すように、halve-untilを定義してください。最初の半分にした値から始め、途中の値がtargetより厳密に大きい間は続けます。最後に出力される値は、target以下まで下がった最初の値になります。

100 5 halve-until .
! => { 50 25 12 6 3 }

64 1 halve-until .
! => { 32 16 8 4 2 1 }

3 5 halve-until .
! => { }
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Factor Exercism

司書の台帳を始める準備はできましたか?

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