Schemeで学ぶプログラミングの原理(授業資料)
本授業は、関数型プログラミング言語Schemeを用いて、プログラミングの基礎から応用までを体系的に学習する全17回の演習科目である。DrScheme(現Racket)を開発環境として使用し、各回で理論説明と実践演習を組み合わせた構成となっている。
カリキュラム構成
本授業は以下の4つのフェーズで構成される。
- 第1フェーズ:基礎(sp-1〜sp-4)
授業全体の方針を示した後、式の記述、関数定義、条件分岐といったSchemeの基本文法を習得する。
- 第2フェーズ:データ構造とリスト処理(sp-5〜sp-7)
リスト、シンボル、文字列といったデータ構造と、リストに対する繰り返し処理・生成方法を習得する。
- 第3フェーズ:設計と抽象化(sp-8〜sp-11)
プログラム設計法、高階関数、構造体を学び、実践的なプログラム構築能力を養う。
- 第4フェーズ:再帰とアルゴリズム(sp-12〜sp-17)
再帰を深化させ、数値計算やソートアルゴリズムに応用する。最終回では計算効率の異なる2つの実装方式を比較する。
学習内容一覧
| 回 | タイトル | 主要な学習内容 |
|---|---|---|
| sp-1 | 全体計画と方針 | 授業の目標、進め方、評価方法 |
| sp-2 | Schemeの式とプログラム | 式の評価、関数定義、基本演算 |
| sp-3 | 関数の組み合わせ | 関数の合成、モジュール化 |
| sp-4 | 条件式 | cond式、論理演算、条件分岐 |
| sp-5 | リスト、シンボル、文字列 | 基本データ型、リスト構造 |
| sp-6 | リストと繰り返し処理 | リスト走査、再帰的処理 |
| sp-7 | リストの生成 | リスト構築、データ生成 |
| sp-8 | プログラム設計法と種々のエラー | 設計手法、デバッグ、エラー対処 |
| sp-9 | 高階関数 | map、filter、関数を引数とする関数 |
| sp-10 | 構造体 | 複合データ型の定義と操作 |
| sp-11 | 構造体とグラフィックス | 構造体の応用、図形描画 |
| sp-12 | 再帰と繰り返しの回数 | 再帰プロセス、計算量の概念 |
| sp-13 | 数値微分と数値積分 | 数値計算、近似手法 |
| sp-14 | ニュートン法 | 反復法、収束計算 |
| sp-15 | リスト処理とクイックソート | 分割統治法、ソートアルゴリズム |
| sp-16 | consと種々のデータ構造 | ペア、木構造、データ構造の構築 |
| sp-17 | フィボナッチ数 | 木構造再帰、反復的プロセス、性能比較 |
到達目標
本授業を修了した受講者は、Schemeの文法とデータ構造を理解して基本的なプログラムを記述できる。また、再帰的思考と高階関数による抽象化を身につけ、計算効率を意識した実装方式の選択ができるようになる。
授業の特徴
本授業は、理論と実践のバランスを重視している。各回でスライドによる概念説明を行った後、DrSchemeを用いた演習で理解を定着させる。また、数値計算やソートアルゴリズムなど、計算機科学の古典的な題材を取り上げることで、プログラミングの基礎技術と計算機科学の基礎知識を同時に習得できる構成となっている。
sp-1. 全体計画と方針
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/ZR6Q25-2022-01-26-150009
Schemeプログラミングの演習では、DrRacket(ドクター・ラケット)という統合開発環境(IDE: Integrated Development Environment、プログラムの作成・実行・デバッグを一つのソフトウェアで行える環境)を使用する。DrRacketは元々DrSchemeという名称であり、Scheme言語の学習用に開発された。2010年にPLT SchemeがRacketに改名された際、DrSchemeもDrRacketに改名された。 Windowsの場合、スタートメニューの「Racket」フォルダからDrRacketを起動する。スタートメニューで「DrRacket」と入力して検索することも可能である。 macOSの場合、Applicationsフォルダに配置したRacketフォルダ内のDrRacketアイコンをダブルクリックする。 Linux(Unix系OS)の場合、デスクトップ環境によってはDrRacketアイコンが作成される。コマンドラインからは、インストールディレクトリのbinサブディレクトリにあるdrracket実行ファイルを起動する。 DrRacketを起動した後、本講義で使用するScheme言語を設定する必要がある。 設定完了後、下部の対話エリア(Interactions)に以下を入力してEnterキーを押す。 結果として「3」が表示されれば、環境構築は完了である。 公式ドキュメントとして https://docs.racket-lang.org/getting-started/index.html(Getting Started)がある。初心者向けの導入として推奨される。 教科書として https://htdp.org/(How to Design Programs, Second Edition)がある。プログラム設計の体系的な方法論を学べる教科書であり、DrRacketとの連携を前提に書かれている。 日本語書籍としては、スライドに記載のとおり以下がある。 Schemeプログラミングの演習に効果的に取り組むための学習サイクルを理解し、自律的な学習姿勢を身につける。 この段階で重要なのは、学習の全体像を把握することである。個々のスキル(再帰、リストなど)の詳細は後続の資料で学ぶため、ここでは用語の名前と概要を知っておく程度でよい。 スライド11の5ステップは、プログラミング学習における普遍的なアプローチである。特にステップ5「失敗を重ねながら、理解を深める」は、エラーを恐れずに試行錯誤することの重要性を示している。 DrRacketの言語設定で迷った場合は、「Beginning Student」から始めることを推奨する。この設定では、初学者が陥りやすいエラーに対して分かりやすいメッセージが表示される。 本資料の学習を終えた後、以下の項目を自己確認する。演習パート(クリックして展開)
環境構築:DrRacketのインストール
ダウンロードとインストール
OS別の起動方法
Scheme言語の設定
動作確認
(+ 1 2)推奨する参考資料
演習:パソコン演習の取り組み方を理解する
目的
手順
ヒント
参照
自己確認チェックリスト
(+ 1 2) の実行結果として 3 が表示されることを確認した
sp-2. Scheme の式とプログラム
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/KW4X15-2022-01-26-145940
スライドで使用されている「DrScheme」は、2010年に「DrRacket」へと名称変更された。PLT Scheme プロジェクトが Racket へとリブランディングされたことに伴う変更である。現在の最新バージョンは Racket 9.0(2025年11月リリース)であり、公式サイト https://racket-lang.org/ から無償でダウンロードできる。 スライドに記載されている Scheme のプログラムおよび式は、DrRacket の Intermediate Student 言語設定においてそのまま動作する。define による関数定義、四則演算、sqrt、expt、remainder、quotient などの組み込み関数はすべて同様に使用できる。 DrRacket を起動し、言語設定を「Intermediate Student」に変更する。設定手順は以下のとおりである。 DrRacket には2つのウインドウがある。定義用ウインドウは関数を記述する領域であり、実行用ウインドウは式を入力して実行結果を確認する領域である。 入れ子になった括弧を含むScheme式の書き方と実行方法を理解する。 Scheme では演算子(+, -, *, /)を括弧の先頭に記述し、その後に被演算子を並べる。これを前置記法(prefix notation)と呼ぶ。数学の記法 define を用いた関数定義の方法と、定義した関数の呼び出し方を習得する。 関数定義の構文は 複数の関数を定義し、expt, max, remainder, quotient などの組み込み関数を活用する方法を学ぶ。 各組み込み関数の意味は以下のとおりである。 実用的な単位変換を行う関数を自ら設計・実装する。 スライド65に「解答の例」として別の関数 foo が示されている。この構造を参考にするとよい。 与えられた数学的公式をSchemeの関数として実装する。 与えられた式は摂氏を求める式である。華氏を求めるには式を変形する必要がある。c = 5 × (f - 32) / 9 を f について解くと f = c × 9 / 5 + 32 となる。 複利計算の公式を関数として実装し、長期間の運用結果をシミュレーションする。 べき乗の計算には 様々な演算子を用いたScheme式の記述方法を習得する。 各演算子の使用例はスライド8-9, 12, 15に記載されている。負の数は数値の前にマイナス記号を付けて表現する(例:-5)。三角関数の引数はラジアン単位である。演習パート(クリックして展開)
現行環境(Racket / DrRacket)についての注記
主な差異と対応
スライドの記載
現行環境での対応
DrScheme
DrRacket
プログラム → PLT Scheme → DrScheme
Racket をインストール後、DrRacket を起動
Execute ボタン
Run ボタン(機能は同一)
Language → Choose Language → Intermediate Student
Language → Choose Language → Teaching Languages → How to Design Programs → Intermediate Student
互換性
演習を始める前の準備
例題1:簡単な数式
目的
手順
(* (+ 2 2) (/ (* (+ 3 5) (/ 30 10)) 2))ヒント
(2+2)*... とは異なり、(* (+ 2 2) ...) のように記述する点に注意する。
スペース(空白文字)の有無が重要である。(+2 2) や (*(+ 2 2) ...) のように演算子と数値の間にスペースがない場合はエラーとなる。
例題2:円の面積
目的
手順
(define (area-of-disk r) (* 3.14 (* r r)))(area-of-disk 5) と入力し、Enter キーを押す(area-of-disk 10) を実行し、結果が 314 となることを確認するヒント
(define (関数名 パラメータ) 式) である。パラメータ(parameter)とは、関数が受け取る値に付ける名前である。この例では r がパラメータであり、関数呼び出し時に具体的な値(5や10)が渡される。例題3:簡単なプログラム
目的
手順
(define (f1 x N) (/ (expt x N) N))
(define (f2 x y) (max x y))
(define (f3 x) (remainder x 100))
(define (f4 x) (quotient x 100))
(f1 2 5) → 結果:6.4(2の5乗は32、32÷5=6.4)(f2 3 4) → 結果:4(3と4の大きい方)(f3 123) → 結果:23(123を100で割った余り)(f4 123) → 結果:1(123を100で割った商)ヒント
関数
意味
例
expt
べき乗を計算する
(expt 2 3) は 2³ = 8
max
引数のうち最大値を返す
(max 3 5) は 5
remainder
剰余(割り算の余り)を返す
(remainder 10 3) は 1
quotient
商(割り算の整数部分)を返す
(quotient 10 3) は 3課題1:ドルから円への変換
目的
手順
ヒント
define ... ではなく (define ...))(d2y d) のように)課題2:摂氏から華氏への変換
目的
手順
ヒント
課題3:元利の計算
目的
手順
ヒント
expt を使用する。年利2%は小数で 0.02 と表現する。関数は3つのパラメータを受け取る必要がある(例:元金、年利、年数)。課題4:Scheme 式
目的
手順
計算内容
参考となる演算子
5 + 5
+
-5 + 5
+
3 * 4
*
8 / 12
/
(2+2) * (((3+5) * (30/10)) / 2)
+, *, /
3 + 4.5
+
2の平方根
sqrt
3の5乗
expt
356を4で割った余り
remainder
7の対数(eを底とする)
log
sin(0.7865)(ラジアン)
sin
ヒント
sp-3. 関数の組み合わせ
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/5D2JD5-2022-01-26-145918
この設定により、ステップ実行機能(stepper)が使用可能になる。 複合的な算術式がどのような順序で評価され、最終結果に至るかを理解する。括弧の内側が優先的に計算される規則を確認する。 各ステップで「どの部分式が計算されたか」に注目する。計算順序は以下のようになる。 括弧の最も内側から順に評価されることを確認する。 関数呼び出し時に、関数本体がパラメータの実際の値で置き換わる過程を理解する。 複数の関数を組み合わせてプログラムを構成する方法を学ぶ。一つの関数(sum-of-squares)が別の関数(sqr)を呼び出す構造を理解する。 複数の関数が連携する場合の実行過程を追跡し、データの流れを理解する。 実行過程は以下のようになる。 実際の問題(リング状の図形の面積計算)を関数の組み合わせでモデル化する方法を学ぶ。 リングの面積は「外側の円の面積 − 内側の円の面積」として計算される。 実行過程は以下のようになる。 外側の円の面積が先に計算され、次に内側の円の面積が計算される順序を確認する。 複数の関数が相互に連携する実践的なプログラムを作成する。ビジネスの利益計算モデルを関数で表現する方法を学ぶ。 このプログラムは劇場の利益を計算するモデルである。 各関数の役割を理解することが重要である。 関数間の依存関係として、 4つの関数が連携するプログラムの実行過程を追跡し、複雑な関数呼び出しの流れを理解する。 例題7のプログラムに 実行過程は多くのステップを含むが、基本的な流れは以下のとおりである。 同じ 例題7で作成したプログラムを使い、異なるチケット代での利益を計算する。 チケット代を変えると観客数が変化し、それに伴い収入と支出も変化する。チケット代と利益の関係を考察すると、最適なチケット代を見つける手がかりになる。 プログラムの一部を変更し、条件が変わった場合の結果を確認する。固定費がかからない場合の利益を計算する。演習パート(クリックして展開)
演習環境の準備
DrSchemeの起動と設定
例題1:実行結果に至る過程
目的
手順
(* (+ 2 2) (/ (* (+ 3 5) (/ 30 10)) 2))ヒント
(+ 2 2) → 4(+ 3 5) → 8(/ 30 10) → 3(* 8 3) → 24(/ 24 2) → 12(* 4 12) → 48例題2:関数のステップ実行
目的
手順
(define (area-of-disk r)
(* 3.14 (* r r)))
(area-of-disk 5)ヒント
(area-of-disk 5) が (* 3.14 (* 5 5)) に置き換わる様子を観察する。このとき、関数定義内のパラメータ r がすべて引数 5 で置き換えられる。DrSchemeでは 3.14 が 157/50 のように有理数(分数)で表示される場合があるが、計算結果は同じである。例題3:2乗の和
目的
手順
(define (sqr x)
(* x x))
(define (sum-of-squares x y)
(+ (sqr x) (sqr y)))(sum-of-squares 2 4)(sum-of-squares 20 30) を実行し、結果「1300」を確認するヒント
sum-of-squares 関数は内部で sqr 関数を2回呼び出している。(sum-of-squares 2 4) の場合、(sqr 2) が4を、(sqr 4) が16を返し、合計20となる。関数を分割することで、プログラムの意味が明確になる利点がある。例題4:ステップ実行(sum-of-squares)
目的
手順
(sum-of-squares 20 30)ヒント
(sum-of-squares 20 30) → (+ (sqr 20) (sqr 30))(sqr 20) → (* 20 20) → 400(sqr 30) → (* 30 30) → 900(+ 400 900) → 1300sqr 関数が呼び出されるたびに、その本体が展開される様子を確認する。例題5:リングの面積
目的
手順
(define (area-of-disk r)
(* 3.14 (* r r)))
(define (area-of-ring outer inner)
(- (area-of-disk outer) (area-of-disk inner)))(area-of-ring 5 3)ヒント
area-of-ring 関数は2つのパラメータ(outer:外径、inner:内径)を受け取り、area-of-disk 関数を2回呼び出して差を計算する。関数を分割することで、計算の意味が「外側 − 内側」であることが明確になる。例題6:ステップ実行(area-of-ring)
目的
area-of-ring 関数の実行過程を追跡し、関数間のデータの流れを詳細に理解する。手順
(area-of-ring 5 3)ヒント
(area-of-ring 5 3) → (- (area-of-disk 5) (area-of-disk 3))(area-of-disk 5) → (* 3.14 (* 5 5)) → (* 3.14 25) → 78.5(area-of-disk 3) → (* 3.14 (* 3 3)) → (* 3.14 9) → 28.26(- 78.5 28.26) → 50.24例題7:利益の計算
目的
背景知識
手順
(define (profit ticket-price)
(- (revenue ticket-price) (cost ticket-price)))
(define (revenue ticket-price)
(* (attendees ticket-price) ticket-price))
(define (cost ticket-price)
(+ 180 (* .04 (attendees ticket-price))))
(define (attendees ticket-price)
(+ 120 (* (/ 15 .10) (- 5.00 ticket-price))))(attendees 3)
(cost 3)
(revenue 3)
(profit 3)期待される結果
関数呼び出し
結果
(attendees 3)420
(cost 3)196.8
(revenue 3)1260
(profit 3)1063.2
ヒント
attendees 関数はチケット代から観客数を計算する。チケット代5ドルのとき観客数120人を基準とし、0.1ドル値下げするごとに15人増える設定であるcost 関数は固定費180ドルに、観客1人あたり0.04ドルの変動費を加算するrevenue 関数は観客数とチケット代を乗算するprofit 関数は収入から支出を減算するprofit は revenue と cost を呼び出し、これらはさらに attendees を呼び出す構造になっている。例題8:ステップ実行(profit)
目的
手順
(profit 3)プログラム全体
(profit 3) を追加した全体は以下のとおりである。(define (profit ticket-price)
(- (revenue ticket-price) (cost ticket-price)))
(define (revenue ticket-price)
(* (attendees ticket-price) ticket-price))
(define (cost ticket-price)
(+ 180 (* .04 (attendees ticket-price))))
(define (attendees ticket-price)
(+ 120 (* (/ 15 .10) (- 5.00 ticket-price))))
(profit 3)ヒント
profit が revenue と cost を呼び出すrevenue が attendees を呼び出すattendees の計算が完了すると revenue の計算が完了するcost が attendees を呼び出すprofit の減算が実行されるattendees 関数が複数回呼び出される点に注目する。課題1
目的
手順
(profit 3)
(profit 4)
(profit 5)ヒント
課題2
目的
手順
cost 関数を以下のように変更する
または
(define (cost ticket-price)
(+ 0 (* .04 (attendees ticket-price))))(define (cost ticket-price)
(* .04 (attendees ticket-price)))(profit 3)
(profit 4)
(profit 5)ヒント
cost 関数内の 180 が固定費を表している。この値を 0 に変更することで、固定費がかからない状況をシミュレートできる。課題1の結果と比較し、固定費が利益に与える影響を考察する。
sp-4. 条件式
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/ZX4EMZ-2022-01-26-145852
cond文(条件分岐を行う構文)を使って、入力値に応じて異なる結果を返す関数を作成する方法を学ぶ。 DrSchemeのstepper機能を使い、条件式がどのように評価されるかを段階的に理解する。 論理演算or(「または」を表す演算)とelse句を組み合わせた条件式を作成する方法を学ぶ。 or演算を含む条件式の評価過程を段階的に追跡し、論理演算の動作を理解する。 複雑な条件(and, or, notの組み合わせ)を用いた判定関数を作成する方法を学ぶ。 and, or, notが組み合わさった複雑な条件式の評価順序を理解する。 quotient(整数除算)とremainderを組み合わせたツエラーの公式を実装し、実用的な関数を作成する。 cond文を使った条件分岐を自力で設計し、段階的な料金表を関数として実装する。 既存の関数(easy-get-num-of-daysとis-leap?)を組み合わせて、より汎用的な関数を作成する。 quotientとremainderを組み合わせた数式を関数として実装する。演習パート(クリックして展開)
例題1:条件式
目的
手順
;; interest-rate: number -> number
;; to determine the interest rate
;; for the given amount
(define (interest-rate amount)
(cond
[(<= amount 1000) 0.040]
[(<= amount 5000) 0.045]
[(> amount 5000) 0.050]))(interest-rate 1000) を入力し、結果を確認する(interest-rate 1500) や (interest-rate 6000) を試し、条件によって結果が変わることを確認するヒント
<= や >= を使う例題2:ステップ実行(interest-rate)
目的
手順
(interest-rate 1500) を追加する
(<= 1500 1000) が false と評価される(<= 1500 5000) が true と評価される0.045 が得られるヒント
false になると、その節全体が消えることに注目する例題3:月の日数
目的
手順
(define (easy-get-num-of-days month)
(cond
[(= month 2) 28]
[(or (= month 4) (= month 6) (= month 9) (= month 11)) 30]
[else 31]))
(easy-get-num-of-days 1) → 31(easy-get-num-of-days 2) → 28(easy-get-num-of-days 4) → 30ヒント
else は「上記のどの条件も成り立たなければ」という意味であるor を使うと、複数の条件のいずれかが成立すればよい場合を簡潔に書ける例題4:ステップ実行(easy-get-num-of-days)
目的
手順
(easy-get-num-of-days 1) を追加する
(= 1 2) が false となり最初の節が消える(or (= 1 4) (= 1 6) (= 1 9) (= 1 11)) の各部分が順に評価されるfalse なので or全体が false となるヒント
false の場合にor全体が false となるtrue があれば、その時点でor全体が true となる例題5:うるう年の判定
目的
手順
(define (is-leap? year)
(cond
[(or (= (remainder year 400) 0)
(and (not (= (remainder year 100) 0))
(= (remainder year 4) 0))) true]
[else false]))
(is-leap? 2004) → true(4の倍数)(is-leap? 2005) → false(4の倍数でない)(is-leap? 1900) → false(100の倍数だが400の倍数でない)(is-leap? 2000) → true(400の倍数)ヒント
(remainder 2004 4) は0を返す例題6:ステップ実行(is-leap?)
目的
手順
(is-leap? 2004) を追加する
(remainder 2004 400) → 4、(= 4 0) → false(remainder 2004 100) → 4、(= 4 0) → false、(not false) → true(remainder 2004 4) → 0、(= 0 0) → trueヒント
例題7:曜日を求める
目的
手順
(define (zellar_f y m d)
(remainder (+ y
(quotient y 4)
(quotient y 400)
(- (quotient y 100))
(quotient (+ (* 13 m) 8) 5)
d)
7))
;; zellar : number number number -> number
;; to determine youbi (the day of the week) from year, month, and day.
;; The result is 0 ... 6 meaning 0 is Sunday, 1 Monday, and so on
(define (zellar y m d)
(cond
[(or (= m 1) (= m 2)) (zellar_f (- y 1) (+ m 12) d)]
[else (zellar_f y m d)]))
ヒント
(quotient 7 3) は2を返す課題1:重量に応じた料金計算
目的
手順
ヒント
decide price ではなく decide_price のように書く≦ や ≧ は使えない。<= や >= を使う課題2:うるう年を考慮した月の日数
目的
手順
(get-num-of-days 2004 2) → 29(うるう年の2月)(get-num-of-days 2005 2) → 28(平年の2月)(get-num-of-days 2004 4) → 30(4月)ヒント
課題3:数式の計算
目的
手順
[(20x + 8) / 7] mod 10[...] は小数点以下切り捨て(quotientで実現)mod は剰余(remainderで実現)ヒント
(20x + 8) / 7 の整数部分を求め、次にその結果を10で割った余りを求める
sp-5. リスト,シンボル,文字列
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/ZPW2EK-2022-01-26-145815
演習パート(クリックして展開)
環境設定
演習を始める前に、DrSchemeを起動し、言語設定を「Intermediate Student」に変更する。
- DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
- Language → Choose Language → Intermediate Student を選択する
- Executeボタンを押す
例題1:リストの式
目的
Schemeにおけるリストの基本的な記法を理解し、listキーワードを使ってリストを作成できるようになる。
手順
- スライド4を確認し、リストが「データの並び」であり「順序がある」ことを理解する
- スライド6を確認し、listキーワードの記法を確認する
- 実行用ウインドウに以下の式を入力し、Enterキーを押して実行する
(list 15 8 6 32 23) - 実行結果が
(list 15 8 6 32 23)と表示されることを確認する
ヒント
listを使った式は、それ自体が1つの式として評価される。入力した式がそのまま表示されるのは、リストが正しく作成されたことを意味する。
例題2:リストのfirstとrest
目的
リストの先頭要素を取得するfirstと、先頭を除いた残りを取得するrestの動作を理解する。
手順
- スライド7を確認し、firstとrestの意味を理解する
- 実行用ウインドウで以下の式を順に実行する
(first (list 15 8 6 32 23))(rest (list 15 8 6 32 23)) - firstの結果が
15(先頭要素)であることを確認する - restの結果が
(list 8 6 32 23)(先頭を除いた残り)であることを確認する
ヒント
restの結果もリストである。この性質により、restを繰り返し適用してリストの任意の位置にアクセスできる。スライド7の「rest の rest の rest の first は:32」という例を自分で実行して確認するとよい。
例題3:リストのfirstとrest(要素が1つの場合)
目的
要素が1つしかないリストに対するfirst, restの動作と、空リストemptyの概念を理解する。
手順
- スライド8を確認し、emptyの意味を理解する
- 実行用ウインドウで以下の式を順に実行する
(first (list 15))(rest (list 15)) - firstの結果が
15であることを確認する - restの結果が
emptyであることを確認する
ヒント
空リストemptyに対してfirstやrestを実行すると実行エラーになる(スライド9参照)。リストを処理する関数を作成する際は、空リストの場合の処理を考慮する必要がある。
例題4:append
目的
複数のリストを連結するappend関数の使い方を理解し、リストでない値を渡した場合のエラーを確認する。
手順
- スライド17を確認し、appendの意味を理解する
- 実行用ウインドウで以下の式を順に実行する
(append (list 1 2) (list 3 4))(append (list 1 2) (list 3 4) (list 5 6))(append (list 1 2) 3 (list 4 5)) - 最初の2つは正常に連結されることを確認する
- 3つ目はエラーになることを確認し、その理由を考える
ヒント
appendはリスト同士を連結する関数である。引数としてリスト以外の値(例:数値3)を渡すと実行エラーになる。
例題5:リストの基本操作
目的
firstとrestを組み合わせてリストの特定位置の要素を取得する関数を定義できるようになる。
手順
- スライド47を確認し、「リストの3番目 = リストのrestのrestのfirst」という関係を理解する
- 定義用ウインドウに以下のプログラムを入力する
(define (element3 a-list) (first (rest (rest a-list)))) - Executeボタンを押してプログラムを読み込ませる
- 実行用ウインドウで以下の式を実行する
(element3 (list 1 2 3 4))(element3 (list 15 8 6 32 23)) - 結果がそれぞれ
3、6であることを確認する - 追加で
(element3 (list 1 2))を実行し、エラーになることを確認する
ヒント
スライド52〜54に示されているように、関数呼び出しは引数の値が関数本体の変数に代入されて評価される。element3関数は要素数が2以下のリストに対してはエラーになる。その理由をスライド57の評価過程を参考に考えるとよい。
例題6:シンボル
目的
シンボル(symbol)の概念を理解し、cond文を使って条件に応じたシンボルを返す関数を作成できるようになる。
手順
- スライド19を確認し、シンボルの記法('を先頭につける)を理解する
- 定義用ウインドウに以下のプログラムを入力する
;;judge: number -> symbol (define (judge x) (cond [(<= x 20) 'Cold] [(and (< 20 x) (<= x 30)) 'Warm] [(< 30 x) 'Hot])) - Executeボタンを押す
- 実行用ウインドウで以下の式を実行する
(judge 15)(judge 20)(judge 25) - 結果がそれぞれ
'Cold、'Cold、'Warmであることを確認する
ヒント
cond文は上から順に条件を評価し、最初にtrueとなった条件に対応する値を返す。<= x 20 は「xが20以下」を意味し、境界値(この例では20)がどちらの条件に含まれるかを確認することが重要である。
例題7:数字かシンボルを出力
目的
条件に応じて異なる型(数値またはシンボル)の値を返す関数を作成できるようになる。cond文のelse節の使い方を理解する。
手順
- 定義用ウインドウに以下のプログラムを入力する
(define (ast x) (cond [(> x 0) x] [else '*])) - Executeボタンを押す
- 実行用ウインドウで以下の式を実行する
(ast 10)(ast 0)(ast -10) - 結果がそれぞれ
10、'*、'*であることを確認する
ヒント
elseはcond文において「それ以外のすべての場合」を表す。この関数では、xが正の場合はx自身を、0以下の場合はシンボル'*を返す。Schemeでは関数の戻り値の型が固定されている必要はない。
例題8:シンボル(シンボルの比較)
目的
symbol=?を使ったシンボルの比較方法を理解し、シンボルを入力として受け取りシンボルを返す関数を作成できるようになる。
手順
- スライド20を確認し、symbol=?の使い方を理解する
- 定義用ウインドウに以下のプログラムを入力する
;;reply: symbol -> symbol ;;to determine a reply for the greeting s (define (reply s) (cond [(symbol=? 'GoodMorning s) 'Hi] [(symbol=? 'HowAreYou s) 'Fine] [(symbol=? 'GoodAfternoon s) 'NeedANap] [(symbol=? 'GoodEvening s) 'BoyAmITired])) - Executeボタンを押す
- 実行用ウインドウで以下の式を実行する
(reply 'GoodMorning)(reply 'Hello) - 最初の結果が
'Hiであることを確認する - 2番目がエラーになることを確認し、その理由を考える
ヒント
スライド21、77に示されているように、シンボルの比較に=を使うのはよくある間違いである。数値の比較には=を、シンボルの比較にはsymbol=?を使用する。また、cond文のどの条件にも該当しない入力があった場合、エラーが発生する(スライド78参照)。
課題1:リスト操作の実行結果
目的
list, first, restの動作を様々なケースで確認し、実行結果を正確に予測できるようになる。
手順
- 以下の表の各セルについて、実行結果を予測する
- 実行用ウインドウで実際に実行し、予測と比較する
- エラーが発生する場合は「エラー」と記録する
| (list 1) | (list 1 2) | (list 1 2 3) | |
|---|---|---|---|
| (first (list ...)) の実行結果 | |||
| (rest (list ...)) の実行結果 | |||
| (first (rest (list ...))) の実行結果 | |||
| (first (rest (rest (list ...)))) の実行結果 |
ヒント
例題1〜3で学んだfirst, rest, emptyの動作を思い出すこと。特にemptyに対するfirst, restはエラーになる点に注意する(スライド9参照)。
課題2:真夏日・冬日判定関数
目的
複数の引数を受け取り、複数の条件を判定して適切な文字列を返す関数を設計・実装できるようになる。
手順
- 以下の判定基準を確認する
- "Tropical Day"(真夏日):1日の最高気温が30度以上
- "Summer Day"(夏日):1日の最高気温が25度以上
- "Frost Day"(冬日):1日の最低気温が0度未満
- "Ice Day"(真冬日):1日の最高気温が0度未満
- 関数summer-winter-dayの仕様を考える
- 入力:high(最高気温)、low(最低気温)
- 出力:上記のいずれかの文字列
- 条件の優先順位を検討する(複数の条件に該当する場合どれを返すか)
- 定義用ウインドウでプログラムを作成する
- 様々な入力値で実行し、正しく判定されることを確認する
ヒント
例題6のjudge関数を参考にする。条件の判定順序が重要である点に注意すること。例えば、最高気温が35度の場合、"Summer Day"と"Tropical Day"の両方の条件を満たすが、cond文では上から順に判定されるため、条件の記述順序によって結果が変わる。また、同時に真夏日と冬日の条件を満たす場合など、境界的なケースについてどう扱うか考える必要がある。
課題3:日付存在判定関数
目的
複雑な条件分岐(月ごとの日数、閏年判定など)を含む関数を設計・実装できるようになる。
手順
- 日付の有効性判定に必要な条件を整理する
- 月ごとの日数の違い(28日、30日、31日)
- 閏年の判定(2月の日数が変わる)
- 日が1以上であること
- 関数の仕様を考える
- 入力:y(年)、m(月)、d(日)
- 出力:日付が存在すればdを、存在しなければシンボル
'*を返す
- 例題7のast関数の構造を参考に、条件分岐を設計する
- 定義用ウインドウでプログラムを作成する
- 以下のテストケースで動作を確認する
- (関数名 2004 10 10) → 10
- (関数名 2004 10 0) → '*
- (関数名 2004 10 32) → '*
ヒント
閏年の判定条件は「4で割り切れる年は閏年。ただし100で割り切れる年は閏年ではない。ただし400で割り切れる年は閏年」である。まずは閏年判定を行わない簡易版を作成し、動作確認後に閏年対応を追加するとよい。例題7で学んだ「条件に応じて数値またはシンボルを返す」パターンが参考になる。
sp-6. リストと繰り返し処理
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/KLQ14Z-2022-01-26-145751
演習パート(クリックして展開)
環境設定
演習を始める前に、DrSchemeを以下の手順で設定する。
- DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
- 言語設定を「Intermediate Student」に変更する
- Language → Choose Language → Intermediate Student を選択
- Execute ボタンを押して設定を反映する
例題1:リストの総和
目的
再帰を用いてリストの全要素の総和を求める関数を作成し、再帰関数の基本構造を理解する。
手順
- 定義用ウインドウに以下のコードを入力する
(define (list-sum a-list)
(cond
[(empty? a-list) 0]
[else (+ (first a-list) (list-sum (rest a-list)))]))
- Execute ボタンを押してプログラムを読み込む
- 実行用ウインドウで以下を実行し、結果を確認する
(list-sum (list 1 2 3))
(list-sum (list 11 12 13))
ヒント
この関数は2つの場合分けで構成される。リストが空(empty)のときは終了条件として0を返す。そうでなければ、リストの先頭要素(first)と残り(rest)の総和を加算する。スライド17-20の図解で、この処理の流れを視覚的に確認するとよい。
例題2:ステップ実行(list-sum)
目的
DrSchemeのstepper機能を使い、再帰関数の計算過程を段階的に追跡する方法を習得する。
手順
- 定義用ウインドウに例題1のコードと実行文を入力する
(define (list-sum a-list)
(cond
[(empty? a-list) 0]
[else (+ (first a-list) (list-sum (rest a-list)))]))
(list-sum (list 1 2 3))
- Step ボタンを押してステップ実行を開始する
- Next ボタンを押しながら、各ステップで式がどのように変換されるかを観察する
- 以下の展開過程を実際の画面で確認する
(list-sum (list 1 2 3))から(+ 1 (list-sum (list 2 3)))への展開- 最終的に
(+ 1 (+ 2 (+ 3 0)))となり6が得られる過程
ヒント
ステップ実行では、関数呼び出しがその定義で置き換えられ、条件式が評価され、最終的に基本演算だけの式になる。この過程を理解することで、再帰の動作原理が明確になる。
例題3:平均点
目的
既存の関数を組み合わせて新しい関数を定義する方法を学ぶ。総和を求める関数と要素数を求める関数を利用して平均を計算する。
手順
- 定義用ウインドウに以下のコードを入力する
;; list-sum: list -> number
;; total of a list
;; (list-sum (list 40 90 80)) = 210
(define (list-sum a-list)
(cond
[(empty? a-list) 0]
[else (+ (first a-list) (list-sum (rest a-list)))]))
;; average: list -> number
;; average of a list
;; (average (list 40 90 80)) = 70
(define (average a-list)
(/ (list-sum a-list) (length a-list)))
- Execute ボタンを押す
- 実行用ウインドウで以下を実行する
(average (list 40 90 80))
(average (list 100 200 300 400 500))
ヒント
average関数自体は再帰を使用していない。list-sum(再帰関数)とlength(組み込み関数)の結果を除算しているだけである。このように、複雑な処理を単純な関数の組み合わせで実現できる点が関数型プログラミングの特徴である。
例題4:ステップ実行(average)
目的
複数の関数が組み合わされた場合の計算過程を追跡し、関数呼び出しの評価順序を理解する。
手順
- 定義用ウインドウに例題3のコードと実行文を入力する
;; (例題3のコードに加えて)
(average (list 40 90 80))
- Step ボタンとNext ボタンを使ってステップ実行する
- 以下の過程を確認する
(average (list 40 90 80))が(/ (list-sum ...) (length ...))に展開される- list-sum の計算が先に進み、210 が得られる
- length の計算で 3 が得られる
- 最終的に
(/ 210 3)が70になる
ヒント
average の中で list-sum が呼び出されると、list-sum の再帰が完了するまで average の計算は待機する。この入れ子の評価順序をステップ実行で確認するとよい。
例題5:「5」を含むか調べる
目的
リスト内に特定の値が存在するかを判定する関数を作成し、真偽値(true/false)を返す再帰関数の書き方を学ぶ。
手順
- 定義用ウインドウに以下のコードを入力する
;; contains-5?: list -> true or false
;; it investigates whether 5 is included
;; (contains-5? (list 3 5 7 9)) = true
(define (contains-5? a-list)
(cond
[(empty? a-list) false]
[else (cond
[(= (first a-list) 5) true]
[else (contains-5? (rest a-list))])]))
- Execute ボタンを押す
- 実行用ウインドウで以下を実行する
(contains-5? (list 1 2 3 4))
(contains-5? (list 3 5 7 9))
ヒント
この関数はcondが入れ子になっている。外側のcondでリストが空かどうかを判定し、内側のcondで先頭要素が5かどうかを判定する。5が見つかった時点でtrueを返し、それ以降の要素は調べない。スライド46-49の図解で処理の流れを確認するとよい。
例題6:ステップ実行(contains-5?)
目的
条件分岐を含む再帰関数の計算過程を追跡し、早期終了(5が見つかった時点で探索を打ち切る)の仕組みを理解する。
手順
- 定義用ウインドウに例題5のコードと実行文を入力する
;; (例題5のコードに加えて)
(contains-5? (list 3 5 7 9))
- Step ボタンとNext ボタンを使ってステップ実行する
- 以下の過程を確認する
- 先頭の3は5ではないので、rest である
(list 5 7 9)を調べる - 先頭の5が見つかり、true が返される
(list 7 9)は調べられない
- 先頭の3は5ではないので、rest である
ヒント
スライド54-55に詳細な展開過程が記載されている。特に、(= 3 5) が false と評価され、再帰呼び出しに進む部分を注意深く追跡するとよい。
例題7:ベクトルの内積
目的
2つのリストを同時に処理する再帰関数を作成し、複数パラメータを扱う再帰の書き方を学ぶ。
手順
- 定義用ウインドウに以下のコードを入力する
;; product: list list -> number
;; inner product of two vectors
;; (product (list 1 2 3) (list 4 5 6)) = 32
(define (product x y)
(cond
[(empty? x) 0]
[else (+ (* (first x) (first y))
(product (rest x) (rest y)))]))
- Execute ボタンを押す
- 実行用ウインドウで以下を実行する
(product (list 1 2 3) (list 4 5 6))
ヒント
内積の計算は 1×4 + 2×5 + 3×6 = 4 + 10 + 18 = 32 である。この関数では、2つのリストの先頭要素同士を掛け、残りのリスト同士で再帰的に内積を求めている。終了条件では一方のリスト(x)が空かどうかだけを判定しているが、これは2つのリストの長さが同じであることを前提としている。
よくある間違い
スライド62に記載があるとおり、終了条件を (= x empty) と書くとエラーになる。= は数値の比較にしか使えないため、リストが空かどうかを判定するには (empty? x) を使用する。
例題8:ステップ実行(product)
目的
2つのリストを同時に処理する再帰の計算過程を追跡し、両方のリストが同時に縮小していく様子を確認する。
手順
- 定義用ウインドウに例題7のコードと実行文を入力する
;; (例題7のコードに加えて)
(product (list 1 2 3) (list 4 5 6))
- Step ボタンとNext ボタンを使ってステップ実行する
- 以下の過程を確認する
(product (list 1 2 3) (list 4 5 6))から(+ 4 (product (list 2 3) (list 5 6)))への展開- 最終的に
(+ 4 (+ 10 (+ 18 0)))となり32が得られる
ヒント
スライド70-71に詳細な展開過程が記載されている。再帰呼び出しのたびに両方のリストのrestが取られる点に注目するとよい。
課題1:list-sum の計算過程の説明
目的
再帰関数の動作を言葉で説明できるようになり、計算過程の理解を深める。
手順
- DrScheme で例題2のステップ実行を再度行う
(list-sum (list 1 2 3))から6が得られる過程を観察する- 数行程度(3〜5行)で計算過程を説明する文章を作成する
ヒント
説明には以下の要素を含めるとよい。最初の式がどのように展開されるか、再帰呼び出しが何回行われるか、終了条件に到達したときに何が起こるか、最終的にどのように値が計算されるか。スライド24の概略図を参考にするとよい。
課題2:実行エラーの解決
目的
stepper を使ってエラーの原因を特定する方法を習得し、デバッグ(プログラムの誤りを発見・修正すること)の技術を身につける。
手順
- 定義用ウインドウに以下のコードを入力する
(define (list-length a-list)
(cond
[(empty? a-list) 0]
[else (+ 1 (list-length (rest a-list)))]))
(list-length 100)
- Execute ボタンを押す(赤い文字でエラーメッセージが表示される)
- Step ボタンを押してステップ実行を開始する
- エラーが発生する箇所まで Next ボタンで進む
- エラーの原因を特定し、報告を作成する
ヒント
このコードの関数定義自体には問題がない。呼び出し方に注目すること。list-length はリストを受け取る関数として定義されているが、呼び出し時に渡されている引数は何か。
課題3:シンボル出現の判定プログラム
目的
contains-5? を一般化し、任意のシンボルを検索できる関数を作成する。
手順
- contains-5? の構造を確認する(スライド45)
- 以下の仕様を満たす関数 contains? を設計する
- 入力:シンボルのリスト a-list、検索対象のシンボル a-symbol
- 出力:a-list が a-symbol を含むとき true、含まないとき false
- 設計した関数を実装し、テストする
ヒント
スライド76に「すべての要素が10以上か?」を判定する関数 all-are-large が掲載されている。この関数の構造を参考にするとよい。contains-5? との違いは、検索対象の値が固定(5)ではなくパラメータ(a-symbol)になる点である。
数値の比較には = を使うが、シンボルの比較には symbol=? を使用する。symbol=? は2つのシンボルが同一かどうかを判定する関数であり、同一であれば true、異なれば false を返す。使用例を以下に示す。
(symbol=? 'apple 'apple) ; → true
(symbol=? 'apple 'orange) ; → false
課題4:偶数を含むか調べる
目的
述語関数(predicate、真偽値を返す関数)を活用した判定処理を実装する。
手順
- contains-5? の構造を確認する
- 「先頭要素が5か」を判定している部分を「先頭要素が偶数か」に変更する方針で設計する
- 偶数の判定には組み込み関数
even?を使用する - 設計した関数を実装し、様々なリストでテストする
ヒント
(even? 4) は true を返し、(even? 3) は false を返す。contains-5? で (= (first a-list) 5) と書いていた部分を、even? を使った式に置き換えればよい。
課題5:Horner法による多項式計算
目的
数学的なアルゴリズムをプログラムとして実装する経験を積む。Horner法(多項式を効率的に計算する手法)を用いた再帰関数を作成する。
手順
- スライド79-80でHorner法のアルゴリズムを理解する
- 多項式 f(x) = a₀ + a₁x + a₂x² + ... + aₙxⁿ をHorner法で書き換えると f(x) = a₀ + x(a₁ + x(a₂ + ... + x(aₙ₋₁ + xaₙ)...)) となることを確認する
- 係数のリスト(a₀から aₙ の順)と x の値を受け取り、f(x) を計算する関数を設計する
- 設計した関数を実装し、テストする
ヒント
計算の順序に注目すること。Horner法では内側から外側に向かって計算する。つまり、まず aₙ から始めて、aₙ₋₁ + aₙ × x を計算し、次に aₙ₋₂ + (aₙ₋₁ + aₙ × x) × x を計算する、という手順を繰り返す。
係数リストを (list a₀ a₁ a₂ ... aₙ) の順で渡す場合、リストを逆順に処理する必要がある。あるいは、係数リストを (list aₙ aₙ₋₁ ... a₁ a₀) の順で渡す設計にすれば、first と rest を使った通常の再帰で処理できる。
例として、f(x) = 5 + 6x + 3x² で x = 2 のとき、Horner法では 5 + 2×(6 + 2×3) = 5 + 2×12 = 29 と計算する。
正解例
以下に、係数リストを高次から低次の順(aₙ, aₙ₋₁, ..., a₁, a₀)で渡す設計の実装例を示す。
;; horner: list number -> number
;; Horner法による多項式の計算
;; 係数リストは高次から低次の順(例:3x² + 6x + 5 なら (list 3 6 5))
;; (horner (list 3 6 5) 2) = 29
(define (horner coeffs x)
(cond
[(empty? coeffs) 0]
[else (+ (first coeffs)
(* x (horner (rest coeffs) x)))]))
この関数の動作を説明する。係数リスト (list 3 6 5) と x = 2 を与えた場合、以下のように計算が進む。
(horner (list 3 6 5) 2)は(+ 3 (* 2 (horner (list 6 5) 2)))に展開される(horner (list 6 5) 2)は(+ 6 (* 2 (horner (list 5) 2)))に展開される(horner (list 5) 2)は(+ 5 (* 2 (horner empty 2)))に展開される(horner empty 2)は終了条件により 0 を返す- 逆順に計算すると、
(+ 5 (* 2 0)) = 5、(+ 6 (* 2 5)) = 16、(+ 3 (* 2 16)) = 35
上記の計算結果 35 は、3×2² + 6×2 + 5 = 12 + 12 + 5 = 29 と一致しない。これは、上記の実装が Horner 法の計算順序と異なるためである。正しい Horner 法の実装には、アキュムレータ(累積変数)を用いる方法がある。
;; horner: list number -> number
;; Horner法による多項式の計算(アキュムレータ使用)
;; 係数リストは高次から低次の順(例:3x² + 6x + 5 なら (list 3 6 5))
;; (horner (list 3 6 5) 2) = 29
(define (horner coeffs x)
(horner-acc coeffs x 0))
;; horner-acc: list number number -> number
;; アキュムレータを用いた補助関数
(define (horner-acc coeffs x acc)
(cond
[(empty? coeffs) acc]
[else (horner-acc (rest coeffs) x (+ (first coeffs) (* acc x)))]))
この実装では、アキュムレータ acc に計算途中の値を保持しながら処理を進める。係数リスト (list 3 6 5) と x = 2 を与えた場合の計算過程は以下のとおりである。
(horner-acc (list 3 6 5) 2 0):acc = 0 + 3×0 ではなく、acc = 0、first = 3 より、新しい acc = 3 + 0×2 = 3(horner-acc (list 6 5) 2 3):新しい acc = 6 + 3×2 = 12(horner-acc (list 5) 2 12):新しい acc = 5 + 12×2 = 29(horner-acc empty 2 29):終了条件により 29 を返す
実行例を以下に示す。
(horner (list 3 6 5) 2) ; → 29 (3x² + 6x + 5 で x = 2)
(horner (list 1 0 0) 3) ; → 9 (x² で x = 3)
(horner (list 1 1 1 1) 2) ; → 15 (x³ + x² + x + 1 で x = 2)
補足:さらに学びたい人へ
スライド81-119には、リストの長さを求める my-length 関数と、n番目の要素を取得する my-list-ref 関数の実装例が掲載されている。これらは組み込み関数 length と list-ref と同等の機能を持つ関数を自作したものである。再帰の理解をさらに深めたい場合は、これらの例も学習するとよい。
sp-7. リストの生成
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/59RVXZ-2022-01-26-145714
演習パート(クリックして展開)
第1部:基礎知識の確認
演習を始める前に、以下の基礎知識をスライドで確認する。
リストの表記方法
Schemeではリストを2種類の方法で表記できる。
cons形式:(cons 'x (cons 'y (cons 'z empty)))のように、consを連ねて記述する方式。emptyは空リストを意味し、リストの末尾を示す。
list形式:(list 'x 'y 'z)のように、listを使って直接記述する方式。
両者は同じリストを表現する。consはリストのfirst(先頭要素)とrest(残りのリスト)をつなげてリストを構築する操作である。
第2部:例題演習
例題1:2次方程式
目的
2次方程式の解を求める関数を作成し、条件分岐(cond)を用いて解の個数に応じた出力を行う方法を学ぶ。
手順
1. スライド16〜17で2次方程式の解の公式と判別式D = b² - 4acの意味を確認する。
2. DrSchemeを起動し、Language → Choose Language → Intermediate Student を選択してExecuteボタンを押す。
3. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
(define (D a b c)
(- (* b b) (* 4 a c)))
(define (quadratic-roots a b c)
(cond
[(< (D a b c) 0) 'None]
[(= (D a b c) 0) (- (/ b (* 2 a)))]
[else (list (/ (+ (- b) (sqrt (D a b c))) (* 2 a))
(/ (+ (- b) (- (sqrt (D a b c)))) (* 2 a)))]))
4. 実行用ウインドウで以下を順に実行し、結果を確認する。
(quadratic-roots 1 -5 6)
(quadratic-roots 2 0 -1)
(quadratic-roots 1 2 1)
(quadratic-roots 1 0 1)
ヒント
判別式Dの値によって出力形式が変わることに注目する。D > 0のときはリスト(2つの解)、D = 0のときは数値(重解)、D < 0のときはシンボル'None(解なし)が返る。
(- b)は単項マイナス演算子であり、bの符号を反転させる。
例題2:リストの生成
目的
再帰(関数が自分自身を呼び出す構造)を用いて、指定した個数の要素からなるリストを生成する方法を学ぶ。
手順
1. スライド28〜30でリスト生成の再帰パターンを確認する。終了条件(n = 0のときemptyを返す)と再帰ステップ(consで先頭に要素を追加)の構造を理解する。
2. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
(define (1list n)
(cond
[(= n 0) empty]
[else (cons 1 (1list (- n 1)))]))
3. 実行用ウインドウで以下を実行し、結果を確認する。
(1list 0)
(1list 3)
ヒント
(1list 3)は内部で(cons 1 (1list 2))となり、さらに(cons 1 (cons 1 (1list 1)))と展開される。この展開過程を頭の中でたどってみること。
終了条件がないと無限ループになるため、n = 0のときにemptyを返す条件は必須である。
例題3:ステップ実行
目的
DrSchemeのstepper機能を使い、再帰関数の実行過程を視覚的に追跡する方法を学ぶ。
手順
1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
(define (1list n)
(cond
[(= n 0) empty]
[else (cons 1 (1list (- n 1)))]))
(1list 3)
2. Stepボタンを押してステップ実行を開始する。
3. Nextボタンを押しながら、式がどのように変換されていくかを観察する。
4. スライド33〜35の実行過程の概略と照らし合わせ、理解を確認する。
ヒント
ステップ実行では、各ステップでどの部分が評価されているかがハイライト表示される。
(1list 3)から(list 1 1 1)に至る過程で、consが順次適用されていく様子を確認する。
例題4:リストの生成
目的
例題2と同様の再帰パターンを用いて、シンボルを要素とするリストを生成する。数値以外の要素でも同じ手法が適用できることを確認する。
手順
1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
(define (astlist n)
(cond
[(= n 0) empty]
[else (cons '* (astlist (- n 1)))]))
2. 実行用ウインドウで以下を実行し、結果を確認する。
(astlist 0)
(astlist 3)
(astlist 10)
ヒント
'*はシンボル(symbol)である。クォート記号'をつけることで、*が乗算演算子ではなくシンボルとして扱われる。
1listとastlistのコードを比較し、違いが「consで追加する要素」だけであることを確認する。
例題5:賃金リストの生成
目的
入力リストの各要素に対して関数を適用し、結果を新しいリストとして生成する方法を学ぶ。これはリスト処理の基本パターンの一つである。
手順
1. スライド48〜50で、リストの先頭要素(first)と残り(rest)を処理する再帰パターンを確認する。
2. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
(define (hours->wages alon)
(cond
[(empty? alon) empty]
[else (cons (wage (first alon))
(hours->wages (rest alon)))]))
(define (wage h)
(* 12 h))
3. 実行用ウインドウで以下を実行し、結果を確認する。
(hours->wages (list 5 3 6))
(hours->wages (list 100 200 300 400))
(hours->wages empty)
ヒント
empty?はリストが空かどうかを判定する述語(predicate)である。
この関数は「リストの各要素を変換して新しいリストを作る」というパターンを示している。このパターンは多くの場面で応用できる。
(hours->wages (list 5 3 6))の結果が(list 60 36 72)となることを確認し、各要素に12が掛けられていることを理解する。
例題6:ステップ実行
目的
hours->wages関数の実行過程をステップ実行で追跡し、リスト処理の再帰パターンの動作を詳細に理解する。
手順
1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
(define (hours->wages alon)
(cond
[(empty? alon) empty]
[else (cons (wage (first alon))
(hours->wages (rest alon)))]))
(define (wage h)
(* 12 h))
(hours->wages (list 1 2 3))
2. Stepボタンを押してステップ実行を開始する。
3. Nextボタンを押しながら、以下の点に注目して観察する。
・firstとrestがどのように適用されるか
・wage関数がいつ呼び出されるか
・consがどのようにリストを組み立てていくか
ヒント
スライド53〜55の実行過程と照らし合わせながら進める。
再帰呼び出しが「入れ子」になっていく様子と、最終的にemptyに到達してから結果が組み立てられていく様子を確認する。
例題7:かけ算の表
目的
リストのリスト(入れ子構造)を生成する方法を学ぶ。複数の関数を組み合わせて複雑なデータ構造を構築する手法を理解する。
手順
1. スライド60〜64でhyou関数とgyou関数の関係を確認する。hyouは表全体、gyouは1行分を生成する。
2. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
;; gyou: number number -> list
;; 1行分(m × n, m × (n-1), ..., m × 1)を生成
(define (gyou m n)
(cond
[(= n 1) (cons m empty)]
[else (cons (* m n) (gyou m (- n 1)))]))
;; hyou: number number -> list-of-list
;; m行n列のかけ算の表を生成
(define (hyou m n)
(cond
[(= m 0) empty]
[else (cons (gyou m n) (hyou (- m 1) n))]))
3. 実行用ウインドウで以下を実行し、結果を確認する。
(hyou 3 4)
ヒント
gyouは横方向(列)の生成、hyouは縦方向(行)の生成を担当する。
出力される表の構造を確認する。(hyou 3 4)の結果は、3行4列のかけ算の表をリストのリストとして表現したものである。
出力が降順になっていることに注意する。これは再帰の構造に起因する。
例題8:ステップ実行
目的
gyou関数とhyou関数の実行過程をステップ実行で追跡し、入れ子構造のリストがどのように構築されるかを理解する。
手順
1. まずgyou関数のステップ実行を行う。定義用ウインドウに以下を入力し、Executeボタンを押す。
(define (gyou m n)
(cond
[(= n 1) (cons m empty)]
[else (cons (* m n) (gyou m (- n 1)))]))
(define (hyou m n)
(cond
[(= m 0) empty]
[else (cons (gyou m n) (hyou (- m 1) n))]))
(gyou 3 4)
2. Stepボタンを押し、(gyou 3 4)から(list 12 9 6 3)が得られる過程を観察する。
3. 次にhyou関数のステップ実行を行う。コードの最後の行を(hyou 5 5)に変更してExecuteボタンを押す。
4. Stepボタンを押し、hyouの再帰呼び出しの中でgyouが呼び出される様子を観察する。
ヒント
スライド68〜71の実行過程と照らし合わせながら進める。
hyouの各ステップで、gyouが完全に評価されてからhyouの次の再帰呼び出しが行われることを確認する。
第3部:課題演習
課題1
目的
cons形式でのリスト記述が、どのようなlist形式の結果になるかを確認する。
手順
1. 実行用ウインドウで以下を実行する。
(cons 1 (cons 2 (cons 3 empty)))
2. 実行結果を報告する。
ヒント
スライド7のconsの実行例を参照する。
cons形式とlist形式の対応関係を理解する。
課題2
目的
与えられた関数の実行過程を分析し、再帰の動作を説明する能力を養う。
手順(問題1)
1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
(define (insert n alon)
(cond
[(empty? alon) (cons n empty)]
[else (cond
[(<= (first alon) n) (cons n alon)]
[(> (first alon) n) (cons (first alon)
(insert n (rest alon)))])]))
(insert 4 (list 5 1))
2. Stepボタンを押してステップ実行を行い、実行過程を観察する。
3. (insert 4 (list 5 1))から(list 5 4 1)が得られる過程を数行で説明する。
手順(問題2)
1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す。
(define (relative-2-absolute alon)
(cond
[(empty? alon) empty]
[else (cons (first alon)
(add-to-each (first alon)
(relative-2-absolute (rest alon))))]))
(define (add-to-each n alon)
(cond
[(empty? alon) empty]
[else (cons (+ (first alon) n)
(add-to-each n (rest alon)))]))
(relative-2-absolute (list 1 2 3))
2. Stepボタンを押してステップ実行を行い、実行過程を観察する。
3. (relative-2-absolute (list 1 2 3))から(list 1 3 6)が得られる過程を数行で説明する。
ヒント
insert関数は、降順にソートされたリストに要素を適切な位置に挿入する関数である。条件分岐の各ケースが何を意味するか考える。
relative-2-absoluteは相対値のリストを絶対値のリストに変換する。例えば、移動距離のリスト(1 2 3)(1進む、2進む、3進む)を位置のリスト(1 3 6)(位置1、位置3、位置6)に変換する。
課題3
目的
例題2・4で学んだ再帰パターンを応用し、奇数の列を生成する関数を自力で作成する。
手順
1. 1番目の奇数は1、2番目の奇数は3、n番目の奇数は2n - 1であることを確認する。
2. スライド76に示されたテンプレートの空欄を埋めて、関数series-odd-listを完成させる。
(define (series-odd-list n)
(cond
[ ]
[ ]))
3. 以下を実行して動作を確認する。
(series-odd-list 4)
4. 結果が(list 7 5 3 1)となることを確認する。
ヒント
例題2の1list関数の構造を参考にする。終了条件と再帰ステップを考える。
n番目の奇数を計算する式を考え、consで追加する要素として使用する。
出力が降順になっていることに注意する。
課題4
目的
例題1の関数を基に、解の個数を返す関数を作成する。条件分岐の応用を練習する。
手順
1. 例題1の判別式Dと解の個数の関係を復習する(スライド16〜17)。
2. D > 0なら2、D = 0なら1、D < 0なら0を返す関数how-manyを作成する。
3. 以下を実行して動作を確認する。
(how-many 1 0 -1)
(how-many 1 2 1)
4. 結果がそれぞれ2と1になることを確認する。
ヒント
例題1で定義した関数Dをそのまま利用できる。
quadratic-rootsとhow-manyの違いは、出力が「解のリスト/数値/シンボル」か「解の個数(数値)」かの違いである。
課題5
目的
例題1の関数を拡張し、より多くの場合に対応できるようにする。条件分岐の設計を練習する。
手順
1. スライド78に示された3つの追加ケースを確認する。
・a = 0 かつ b = 0 かつ c = 0:すべてのxが解
・a = 0 かつ b = 0 かつ c ≠ 0:解なし
・a = 0 かつ b ≠ 0:x = -c/b
2. 例題1のquadratic-roots関数を修正し、a = 0の場合にも対応させる。
3. 各ケースのテストを行い、動作を確認する。
ヒント
condの条件を追加する順序が重要である。より特殊な条件(a = 0のケース)を先に判定する。
「すべてのxが解」の場合、どのような出力が適切か考える(例:シンボル'Allなど)。
課題6
目的
日付と曜日の情報から1週間分のカレンダー行を生成する。リスト生成と条件分岐を組み合わせた応用問題である。
アルゴリズムの概要
1週間分のカレンダー行を生成するには、以下の手順を踏む。
1. 与えられた日付dと曜日youbiから、その週の日曜日に対応する日付を計算する(d - youbi)
2. 日曜日(位置0)から土曜日(位置6)まで、7つの要素を持つリストを生成する
3. 各位置について、対応する日付が1以上かつ月の日数以下であれば日付を、そうでなければ'*を出力する
4. 月の日数を返す補助関数を用意する(うるう年も考慮)
手順
1. スライド79の仕様を確認する。曜日は0〜6の数値で表し、0が日曜、6が土曜である。
2. 与えられた日付dと曜日youbiから、その週の日曜日から土曜日までの7つの要素を持つリストを生成する関数を設計する。
3. 日付が月の範囲外になる部分は'*で埋める。
4. 以下のテストケースで動作を確認する。
・y = 2004, m = 10, d = 2, youbi = 6 → (list '* '* '* '* '* 1 2)
・y = 2004, m = 10, d = 31, youbi = 0 → (list 31 '* '* '* '* '* '*)
ヒント
まず、与えられた日付から「その週の日曜日の日付」を計算する方法を考える。
日曜日から土曜日まで順に7つの要素を持つリストを作成し、月の範囲外(1未満または月の日数超過)なら'*を入れる。
月の日数を返す補助関数があると便利である。
正解プログラム
;; うるう年判定
(define (leap-year? y)
(or (and (= (remainder y 4) 0)
(not (= (remainder y 100) 0)))
(= (remainder y 400) 0)))
;; 月の日数を返す
(define (days-in-month y m)
(cond
[(= m 2) (if (leap-year? y) 29 28)]
[(or (= m 4) (= m 6) (= m 9) (= m 11)) 30]
[else 31]))
;; 1週間分のリストを生成(補助関数)
(define (week-list y m start-day pos)
(cond
[(= pos 7) empty]
[else
(let ([day (+ start-day pos)])
(cons (if (and (>= day 1) (<= day (days-in-month y m)))
day
'*)
(week-list y m start-day (+ pos 1))))]))
;; 1週間分のリストを生成(メイン関数)
(define (one-week y m d youbi)
(week-list y m (- d youbi) 0))
課題7
目的
課題6を発展させ、1か月分のカレンダーを生成する。リストのリストを生成する手法(例題7で学習)を応用する。
アルゴリズムの概要
月のカレンダーを生成するには、以下の手順を踏む。
1. 月の初日の曜日を計算する(Zellerの公式を使用)
2. 月の初日から開始し、1週間ごとにリストを生成する
3. 各週について課題6の関数を呼び出し、1週間分のリストを取得する
4. 月の最終日を超えるまで、週のリストをconsで連結していく
5. 結果はリストのリスト(週のリストを要素とするリスト)となる
Zellerの公式は、年月日から曜日を計算する公式である。1月と2月は前年の13月、14月として扱う。
手順
1. 課題6で作成した1週間分の関数を活用する。
2. 月の初日から最終日まで、週ごとにリストを生成し、それらをリストのリストとしてまとめる。
3. スライド80の出力例を参考に、期待される形式を確認する。
ヒント
例題7のhyouとgyouの関係を参考にする。「1か月 = 複数の週」という構造を意識する。
月の初日の曜日を計算する関数が必要になる。Zellerの公式などを調べて実装するか、手動で入力する方式にする。
5週または6週になる月があることに注意する。
前提条件
以下の正解プログラムを実行するには、課題6の正解プログラム(leap-year?、days-in-month、week-list、one-week関数)が既に定義されている必要がある。課題6のコードを先に定義用ウインドウに入力してから、以下のコードを追加すること。
正解プログラム
;; Zellerの公式による曜日計算(0=日曜、1=月曜、...、6=土曜)
(define (zeller y m d)
(let* ([adjusted-m (if (< m 3) (+ m 12) m)]
[adjusted-y (if (< m 3) (- y 1) y)]
[q d]
[k (remainder adjusted-y 100)]
[j (quotient adjusted-y 100)]
[h (remainder (+ q
(quotient (* 13 (+ adjusted-m 1)) 5)
k
(quotient k 4)
(quotient j 4)
(* -2 j))
7)])
(remainder (+ h 6) 7)))
;; 月カレンダー生成(補助関数)
(define (month-calendar-helper y m current-day)
(cond
[(> current-day (days-in-month y m)) empty]
[else
(let* ([youbi (zeller y m current-day)]
[week (one-week y m current-day youbi)]
[next-day (+ current-day (- 7 youbi))])
(cons week (month-calendar-helper y m next-day)))]))
;; 月カレンダー生成(メイン関数)
(define (month-calendar y m)
(month-calendar-helper y m 1))
課題8
目的
課題7をさらに発展させ、1年分のカレンダーを生成する。リストのリストのリストという3重の入れ子構造を扱う。
アルゴリズムの概要
年のカレンダーを生成するには、以下の手順を踏む。
1. 1月から12月まで、各月について課題7の関数を呼び出す
2. 各月のカレンダー(リストのリスト)をconsで連結していく
3. 12月まで処理したらemptyを返して終了する
4. 結果はリストのリストのリスト(月のリストを要素とするリスト)となる
手順
1. 課題7で作成した月カレンダー関数を活用する。
2. 1月から12月まで、各月のカレンダーを生成し、それらをリストとしてまとめる。
3. 出力はリストのリストのリスト(年 → 月 → 週)となる。
ヒント
1から12までの数を順に処理し、各月のカレンダーをconsで連結していく。
うるう年の判定が必要である。うるう年は「4で割り切れ、かつ100で割り切れないか、400で割り切れる年」である。
前提条件
以下の正解プログラムを実行するには、課題6の正解プログラム(leap-year?、days-in-month、week-list、one-week関数)および課題7の正解プログラム(zeller、month-calendar-helper、month-calendar関数)が既に定義されている必要がある。課題6と課題7のコードを先に定義用ウインドウに入力してから、以下のコードを追加すること。
正解プログラム
;; 年カレンダー生成(補助関数)
(define (year-calendar-helper y m)
(cond
[(> m 12) empty]
[else (cons (month-calendar y m)
(year-calendar-helper y (+ m 1)))]))
;; 年カレンダー生成(メイン関数)
(define (year-calendar y)
(year-calendar-helper y 1))
補足:リストの連結
例題9として、2つのリストを連結する関数my-appendが紹介されている。標準関数appendの動作を自作することで、リスト操作の理解を深めることができる。
sp-8. プログラム設計法と種々のエラー
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/ZGQ1DK-2022-01-26-145646
演習パート(クリックして展開)
例題1:条件式
条件式(cond句)を用いて、複数の条件分岐を持つ関数を作成する方法を理解する。
手順
- DrSchemeを起動する。メニューから「Language → Choose Language → Intermediate Student」を選択し、Executeボタンを押す。
- スライド27のグラフを確認し、残高と利率の対応関係を把握する。残高が1000以下なら4%、5000以下なら4.5%、5000より多ければ5%である。
- 以下のプログラムを定義ウィンドウに入力する。
;; interest-rate: number -> number ;; to determine the interest rate ;; for the given amount (define (interest-rate amount) (cond [(<= amount 1000) 0.040] [(<= amount 5000) 0.045] [(> amount 5000) 0.050])) - Executeボタンを押してプログラムを読み込ませる。
- 対話ウィンドウで以下のテストケースを実行し、期待値と一致するか確認する。
(interest-rate 500)→ 期待値: 0.040(interest-rate 1000)→ 期待値: 0.040(interest-rate 1500)→ 期待値: 0.045(interest-rate 5000)→ 期待値: 0.045(interest-rate 10000)→ 期待値: 0.050
- 境界値(1000, 5000)での動作を確認し、条件式の評価順序を理解する。
- cond句は上から順に条件を評価し、最初に真となった条件の式を返す。このため、条件の記述順序が重要である。
- 境界値(1000や5000)がどちらの条件に該当するか、不等号の向き(<=, <, >, >=)に注意して確認すること。
- プログラム設計法のContract、Purpose、Example、Definition、Testの各段階を意識しながら、自分でも別の関数を設計してみるとよい。
課題1:関数fの実行結果
関数定義における引数の個数と、関数呼び出し時の引数の個数の不一致によって発生するエラーを理解する。
手順
- DrSchemeを起動し、Intermediate Studentモードに設定する。
- 以下の関数fの定義を入力する。
(define (f n) (* n 20)) - Executeボタンを押してプログラムを読み込ませる。
- 対話ウィンドウで
(f 5 20)を実行する。 - 表示されるエラーメッセージを確認し、その原因を考察する。
- 関数fの定義(引数の個数)と、呼び出し時に渡した引数の個数を比較する。
- 考察結果を文章でまとめる。
- 関数定義の
(define (f n) ...)において、パラメータnは1つである。 - 「期待される引数の個数」と「実際に渡された引数の個数」という観点で説明を構成するとよい。
課題2:エラーの原因説明
構文エラー(Syntax Errors)、実行エラー(Run-time Errors)の違いを理解し、各エラーの原因を正確に説明できるようになる。
手順
- DrSchemeを起動し、Intermediate Studentモードに設定する。
- 以下の6つのScheme式を、1つずつ入力して実行する。
- 各式について、以下の手順で分析する。
- エラーメッセージを確認する
- エラーの種類(構文エラーまたは実行エラー)を判定する
- エラーの原因を特定する
- 分析結果を、式ごとに以下の形式でまとめる。
- 式の内容
- エラーの種類
- 原因の説明
各式の分析観点
| 式番号 | 式 | エラー種類 | 分析の観点 |
|---|---|---|---|
| 1 | (10 + 20) |
構文エラー | Schemeにおける関数呼び出しの構文規則(前置記法)を確認する |
| 2 | (define (f y) (+ x 10))で(f 5)を実行 |
実行エラー | 関数本体で使用している変数と、パラメータの関係を確認する |
| 3 | (define (g x) + x 10) |
構文エラー | 関数本体の括弧の構造を確認する |
| 4 | (define h(x) (+ x 10)) |
構文エラー | define構文の正しい形式を確認する |
| 5 | (define (f n) (+ (/ n 3) 2))で(f 5 8)を実行 |
実行エラー | 関数定義の引数の個数と呼び出し時の引数の個数を比較する |
| 6 | (+ 5 (/ 1 0)) |
実行エラー | 数学的に許されない演算を確認する |
- 構文エラーはScheme言語の書式に合っていない場合に発生する。プログラムを実行する前の段階で検出される。
- 実行エラーは書式は正しいが、実行時に問題が発生する場合に起こる。
- 式2では、パラメータ名が「y」であるのに、本体で「x」を使用している点に注目する。
- 式4では、関数名と引数リストの間の空白と括弧の位置に注目する。
論理的エラーについて
論理的エラー(Logical Errors)は、構文エラーや実行エラーとは異なり、プログラムが正常に実行され結果を出力するが、その結果が誤っているという種類のエラーである。DrSchemeはこのエラーを自動的に検出できないため、プログラマ自身が発見しなければならない。
論理的エラーの例
例1:三角形の面積計算の誤り
(define (area-of-triangle b h)
(+ b h))
誤りの内容:底辺bと高さhを足し算している。三角形の面積は「底辺×高さ÷2」であるため、正しくは(/ (* b h) 2)または(* 0.5 (* b h))とすべきである。
例2:時給計算の誤り
(define (wage h)
(+ 12 h))
誤りの内容:時給12に労働時間hを足し算している。賃金は「時給×労働時間」であるため、正しくは(* 12 h)とすべきである。
例3:リングの面積計算で内外を逆にした誤り
(define (area-of-ring outer inner)
(- (area-of-disk inner) (area-of-disk outer)))
誤りの内容:内側の円の面積から外側の円の面積を引いている。外側が大きいため、結果が負の値になる。正しくは(- (area-of-disk outer) (area-of-disk inner))とすべきである。
論理的エラーへの対処方針
論理的エラーは構文的に正しく、実行も完了するため、発見が困難である。以下の方法で対処する。
- Exampleの活用:プログラム設計法のExample段階で、事前に期待される入出力を明確にしておき、実行結果と照合する。
- 境界値のテスト:特殊なケース(0、負数、最大値など)でテストを行い、異常な結果が出ないか確認する。
- 手計算との照合:小さな入力値で手計算した結果と、プログラムの出力を比較する。
補足:プログラム設計法(Design Recipe)の実践
本演習では直接の課題として指定されていないが、プログラム設計法を、例題1や自作の関数に適用して練習することを推奨する。
設計法の5段階は以下のとおりである。
- Contract:関数名、入力の型、出力の型を定義する
- Purpose:関数が何をするかを文章で記述する
- Example:具体的な入出力の例を示す
- Definition:関数本体を作成する
- Test:Exampleで示した例を実行し、期待値と一致するか確認する
sp-9. 高階関数
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/K1XRGK-2022-01-26-145619
演習パート(クリックして展開)
学習の到達目標
本演習を完了すると、以下のことができるようになる。
- 高階関数(関数を引数として受け取る関数)のプログラムを理解し実行できる
- reduce、series、taylor-seriesの各関数の動作を説明できる
- テーラー展開を用いて数学関数の近似値を計算できる
環境設定
DrSchemeを起動し、Language設定を「Advanced Student」に変更する。手順は以下のとおりである。
- DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
- Language → Choose Language → Advanced Student → OK
- Executeボタンを押す
Advanced Studentモードは高階関数の実行に必要である。
例題1:リストの総和
目的
高階関数reduce(関数を引数として受け取り、リストの要素を順次処理する関数)の動作を理解し、これを利用してリストの総和を求める関数list-sumを作成する。
手順
- 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
- reduce関数の定義
- list-sum関数の定義
- 実行用ウインドウで以下を実行する
(list-sum (list 1 2 3))(list-sum empty)
- 実行結果を確認し、reduceの動作を追跡する
コード
;; reduce: (number data -> data) data list -> data
;; (reduce g 0 (list 1 2 3)) = (g 1 (g 2 (g 3 0)))
(define (reduce op base L)
(cond
[(empty? L) base]
[else (op (first L) (reduce op base (rest L)))]))
;; list-sum: list -> number
;; total of a list
;; (list-sum (list 40 90 80)) = 210
(define (list-sum a-list)
(reduce + 0 a-list))
(reduce g 0 (list 1 2 3))を(g 1 (g 2 (g 3 0)))に展開する。list-sumではgが+(加算)、baseが0となるため、(+ 1 (+ 2 (+ 3 0)))すなわち6が得られる。置き換え過程を紙に書いて追跡すると理解が深まる。
例題2:xのn乗の近似値
目的
#iを付けた近似計算の動作を理解する。Schemeでは#iを数値に付けると、その計算は近似値(浮動小数点数)として処理される。
手順
- 実行用ウインドウで以下を実行する
(expt 11 200)(expt #i11 200)
- 両者の結果を比較する
#iなしの場合は有理数として厳密に計算されるため、巨大な整数が表示される。#iありの場合は近似値として計算され、#i1.899...e+208のような指数表記で表示される。e+208は10の208乗を意味する。
例題3:数列の和
目的
高階関数seriesを使用して、数列の和 Σf(k)(k=1からn)を計算する方法を理解する。
手順
- 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
- series関数の定義
- f2関数(k²を返す)とf3関数(k³を返す)の定義
- 実行用ウインドウで以下を実行する
(series 3 f2)→ 1² + 2² + 3² = 14(series 4 f3)→ 1³ + 2³ + 3³ + 4³ = 100
- 実行結果を確認する
コード
;; series: number (number -> number) -> number
;; (series f2 3) = (+ (f2 3) (+ (f2 2) (+ (f2 1))))
(define (series n f)
(cond
[(= n 1) (f 1)]
[else (+ (f n) (series (- n 1) f))]))
(define (f2 k) (* k k))
(define (f3 k) (* k k k))
(f 1)を返し、n>1のとき(+ (f n) (series (- n 1) f))を返す。置き換え過程を追跡して、再帰の展開を確認すること。
例題4:数列の和のリスト
目的
series-iter関数を使用して、数列の部分和のリストを作成する。これにより、級数の収束の様子を観察できる。
手順
- 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
- series関数の定義
- series-iter関数の定義
- f4関数(1/k²を返す)の定義
- 実行用ウインドウで以下を実行する
(series-iter #i100 f4)
- 結果のリストを確認し、Σ(1/k²)がπ²/6(約1.6449)に収束する様子を観察する
コード
;; series: number (number -> number) -> number
;; (series f2 3) = (+ (f2 3) (+ (f2 2) (+ (f2 1))))
(define (series n f)
(cond
[(= n 1) (f 1)]
[else (+ (f n) (series (- n 1) f))]))
;; series-iter : number (number -> number) -> list
(define (series-iter n f)
(cond
[(= n 0) empty]
[else (cons (series n f) (series-iter (- n 1) f))]))
(define (f4 k) (/ 1 (* k k)))
#iを付けることで近似値として計算され、収束の様子が見やすくなる。
例題5:べき級数
目的
高階関数taylor-seriesを使用して、べき級数 Σg(k)·xk(k=0からn)を計算する方法を理解する。
手順
- 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
- taylor-series関数の定義
- g1関数(kを返す)の定義
- 実行用ウインドウで以下を実行する
(taylor-series 2 3 g1)→ 0·2⁰ + 1·2¹ + 2·2² + 3·2³ = 0 + 2 + 8 + 24 = 34(taylor-series 2 4 g1)
- 実行結果を確認する
コード
;; taylor-series: number number (number -> number)
;; -> number
;; (taylor-series 2 3 g) =
;; (+ (* (g 3) (expt 2 3)
;; (+ (* (g 2) (expt 2 2))
;; (+ (* (g 1) (expt 2 1))
;; (g 0))))
(define (taylor-series x n g)
(cond
[(= n 0) (g 0)]
[else (+ (* (g n) (expt x n))
(taylor-series x (- n 1) g))]))
(define (g1 k) k)
例題6:指数関数のテーラー展開
目的
taylor-series関数を使用して、指数関数exのテーラー展開による近似値を計算する。
手順
- 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
- taylor-series関数の定義
- factorial関数(階乗を計算)の定義
- exp-term関数(1/k!を返す)の定義
- my-exp関数の定義
- 実行用ウインドウで以下を実行する
(my-exp 0),(my-exp 1),(my-exp 2),(my-exp 3)(my-exp #i1),(my-exp #i2),(my-exp #i3)
#iありとなしの結果を比較する
コード
;; taylor-series: number number (number -> number)
;; -> number
(define (taylor-series x n g)
(cond
[(= n 0) (g 0)]
[else (+ (* (g n) (expt x n))
(taylor-series x (- n 1) g))]))
(define (factorial k)
(cond
[(= k 0) 1]
[else (* k (factorial (- k 1)))]))
(define (exp-term k) (/ 1 (factorial k)))
(define (my-exp x)
(taylor-series x 20 exp-term))
#iなしの場合は分数として厳密に計算される。
例題7:cos関数のテーラー展開
目的
taylor-series関数を使用して、cos関数のテーラー展開による近似値を計算する。
手順
- 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
- taylor-series関数、factorial関数の定義
- cos-term関数の定義
- my-cos関数の定義
- 実行用ウインドウで以下を実行する
(my-cos #i0),(my-cos #i0.2),(my-cos #i0.4),(my-cos #i0.6),(my-cos #i0.8)
- 結果をScheme組み込みのcos関数と比較して確認する
コード
;; taylor-series: number number (number -> number)
;; -> number
(define (taylor-series x n g)
(cond
[(= n 0) (g 0)]
[else (+ (* (g n) (expt x n))
(taylor-series x (- n 1) g))]))
(define (factorial k)
(cond
[(= k 0) 1]
[else (* k (factorial (- k 1)))]))
(define (cos-term k)
(cond
[(odd? k) 0]
[else (cond
[(= (remainder k 4) 0) (/ 1 (factorial k))]
[else (/ -1 (factorial k))])]))
(define (my-cos x)
(taylor-series x 20 cos-term))
odd?(奇数判定)を使用して奇数次の項を0にし、remainder(剰余)を使用して符号を交互に変えている。
例題8:対数関数のテーラー展開
目的
taylor-series関数を使用して、対数関数log(1+x)のテーラー展開による近似値を計算する。
手順
- 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
- taylor-series関数の定義
- log-term関数の定義
- my-log関数の定義
- 実行用ウインドウで以下を実行する
(my-log #i1),(my-log #i1.2),(my-log #i1.4),(my-log #i1.6),(my-log #i1.8)
- 結果をScheme組み込みのlog関数と比較して確認する
コード
;; taylor-series: number number (number -> number)
;; -> number
(define (taylor-series x n g)
(cond
[(= n 0) (g 0)]
[else (+ (* (g n) (expt x n))
(taylor-series x (- n 1) g))]))
(define (log-term k)
(cond
[(= k 0) 0]
[else (/ (expt -1 (- k 1)) k)]))
(define (my-log x)
(taylor-series (- x 1) 20 log-term))
課題1:近似計算の理解
目的
#i記号と指数表記の意味を正確に理解し、近似計算と厳密計算の違いを説明できるようになる。
手順
#i9.313e+020の意味を説明する- 以下の3つを実行し、結果を比較する
(expt 12 100)(expt #i12 100)(expt 12 #i100)
- 各結果の違いを報告する
e+020は1020を意味する。#iを付けた数値は近似値として扱われ、計算結果も近似値となる。どちらか一方の引数に#iを付けても、結果は近似値となることを確認すること。
課題2:my-expの理解
目的
テーラー展開の部分和を段階的に計算することで、級数の収束過程を理解する。
手順
- 例題6のmy-exp関数を参考に、x=1のとき以下の各部分和の値を計算する
- 1 + x/1!
- 1 + x/1! + x²/2!
- 1 + x/1! + x²/2! + x³/3!
- 1 + x/1! + x²/2! + x³/3! + x⁴/4!
- 1 + x/1! + x²/2! + x³/3! + x⁴/4! + x⁵/5!
- 各値を報告する
課題3:数列の和の列1
目的
series-iter関数を使用して交代級数(正負の項が交互に現れる級数)の部分和のリストを作成する。
手順
- 数列 Σ((-1)n+1 / (2n-1))(n=1から∞)の一般項を計算する関数を定義する
- n=1: 1/1, n=2: -1/3, n=3: 1/5, n=4: -1/7, ...
- series-iterを使用して部分和のリストを作成する
#iありとなしの結果の違いを報告する
#iなしの場合は分数として厳密に計算され、#iありの場合は小数の近似値として計算される。
課題4:数列の和の列2
目的
series-iter関数を使用して階乗の逆数の和の部分和のリストを作成する。
手順
- 数列 Σ(1/n!)(n=1から∞)の一般項を計算する関数を定義する
- n=1: 1/1!, n=2: 1/2!, n=3: 1/3!, ...
- series-iterを使用して部分和のリストを作成する
#iありとなしの結果の違いを報告する
課題5:sin関数のテーラー展開
目的
例題7のcos関数と同様の方法で、sin関数のテーラー展開を実装する。
手順
- sin xのテーラー展開式を確認する
- sin x = x - x³/3! + x⁵/5! - x⁷/7! + ...
- sin x = Σ((-1)k · x2k+1 / (2k+1)!)(k=0から∞)
- sin-term関数を定義する(偶数次の項は0、奇数次の項のみ非零)
- my-sin関数を定義する
- 様々なxの値で実行し、Scheme組み込みのsin関数と比較して正しさを確認する
even?関数(偶数ならばtrue)を使用する。符号の決定にはremainderを使用し、4で割った余りで判定する方法がある。
課題6:tan⁻¹関数のテーラー展開
目的
tan⁻¹関数(逆正接関数、arctangent)のテーラー展開を実装する。
手順
- tan⁻¹ xのテーラー展開式を確認する
- tan⁻¹ x = x - x³/3 + x⁵/5 - x⁷/7 + ...
- tan⁻¹ x = Σ((-1)k · x2k+1 / (2k+1))(k=0から∞)
- atan-term関数を定義する(偶数次の項は0、奇数次の項のみ非零)
- my-atan関数を定義する
- 様々なxの値で実行し、Scheme組み込みのatan関数と比較して正しさを確認する
even?関数を使用する。
例題間の依存関係
本演習の例題は段階的に構成されている。以下に依存関係を示す。
| 例題 | 前提となる例題 | 説明 |
|---|---|---|
| 例題1 | なし | 高階関数reduceの基本 |
| 例題2 | なし | 近似計算の基本 |
| 例題3 | なし | 高階関数seriesの基本 |
| 例題4 | 例題3 | seriesを利用してseries-iterを構築 |
| 例題5 | なし | 高階関数taylor-seriesの基本 |
| 例題6 | 例題5 | taylor-seriesを利用してexを計算 |
| 例題7 | 例題5, 例題6 | taylor-seriesを利用してcos xを計算 |
| 例題8 | 例題5 | taylor-seriesを利用してlog xを計算 |
sp-10. 構造体
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/KV413Z-2022-01-26-145545
演習パート(クリックして展開)
演習環境の準備
DrSchemeを起動し、言語設定を「Intermediate Student」に変更する。設定手順は以下のとおりである。
- DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
- メニューから Language → Choose Language を選択する
- Intermediate Student を選択する
- Execute ボタンを押す
例題1:ball構造体
目的
構造体(structure:複数のデータをまとめて1つの型として扱うデータ構造)の定義方法を理解する。define-struct文を使用してball構造体を定義する。
手順
- 定義用ウインドウに以下のコードを入力する
(define-struct ball (x y delta-x delta-y)) - Executeボタンを押す
- 実行結果は表示されないが、これでball構造体が定義される
ヒント
define-structを実行すると、コンストラクタ(構造体を生成する関数)とセレクタ(属性値を取得する関数)が自動的に使用可能になる。ball構造体の場合、make-ball(コンストラクタ)とball-x, ball-y, ball-delta-x, ball-delta-y(セレクタ)が使えるようになる。
例題2:ballの原点からの距離
目的
セレクタを使って構造体から属性値を取り出し、計算に利用する方法を学ぶ。ball構造体のx, y座標から原点までの距離を計算する関数を作成する。
手順
- 定義用ウインドウに以下のコードを入力する
(define-struct ball (x y delta-x delta-y)) ;; distance-to-0: ball -> number ;; to compute the distance of a ball to the origin ;; (distance-to-0 (make-ball 3 4 0 0)) = 5 (define (sqr x) (* x x)) (define (distance-to-0 a-ball) (sqrt (+ (sqr (ball-x a-ball)) (sqr (ball-y a-ball))))) - Executeボタンを押す
- 実行用ウインドウで以下を入力し実行する
(distance-to-0 (make-ball 3 4 0 0)) - 結果として「5」が表示されることを確認する
ヒント
ball-xとball-yはセレクタであり、ball構造体から属性値を取り出す。(ball-x a-ball)はa-ballのx座標を返す。距離の計算には三平方の定理(ピタゴラスの定理)を使用している。
例題3:ステップ実行
目的
DrSchemeのstepper機能を使い、関数の実行過程を段階的に追跡することで、評価の順序と構造体のセレクタの動作を理解する。
手順
- 定義用ウインドウに以下のコードを入力する(例題2のコードに実行式を追加)
(define-struct ball (x y delta-x delta-y)) ;; distance-to-0: ball -> number ;; to compute the distance of a ball to the origin ;; (distance-to-0 (make-ball 3 4 0 0)) = 5 (define (sqr x) (* x x)) (define (distance-to-0 a-ball) (sqrt (+ (sqr (ball-x a-ball)) (sqr (ball-y a-ball))))) (distance-to-0 (make-ball 3 4 0 0)) - Stepボタンを押す
- Nextボタンを繰り返し押し、各ステップで式がどのように変換されるか観察する
- (make-ball 3 4 0 0)からball-xで3が取り出される過程、sqrで9が計算される過程などを確認する
ヒント
ステップ実行では、関数呼び出しが定義本体で置き換えられ、セレクタが具体的な値に置き換えられる様子が観察できる。最終的に(sqrt 25)が5に評価される過程まで追跡する。
例題4:AddressNote構造体
目的
name, age, addressの3つの属性を持つAddressNote構造体を定義する。異なる属性構成の構造体定義に慣れる。
手順
- 定義用ウインドウに以下のコードを入力する
(define-struct AddressNote (name age address)) - Executeボタンを押す
- この定義により、make-AddressNote(コンストラクタ)とAddressNote-name, AddressNote-age, AddressNote-address(セレクタ)が使用可能になる
ヒント
構造体名と属性名を組み合わせたセレクタ名(例:AddressNote-name)は自動的に生成される。属性の順序は定義時の順序と一致する。
例題5:住所録
目的
構造体のリストを処理し、各要素から特定の属性を抽出する関数を作成する。再帰処理とセレクタの組み合わせを学ぶ。
手順
- 定義用ウインドウに以下のコードを入力する
(define-struct AddressNote (name age address)) ;; select-name: a list of AddressNote -> a list of string ;; to select name from an AddressNote list (define (select-name a-list) (cond [(empty? a-list) empty] [else (cons (AddressNote-name (first a-list)) (select-name (rest a-list)))])) - Executeボタンを押す
- 実行用ウインドウで以下を入力し実行する
(select-name (list (make-AddressNote "Ken" 35 "Fukuoka") (make-AddressNote "Bill" 30 "Saga") (make-AddressNote "Mike" 28 "Nagasaki"))) - 結果として(list "Ken" "Bill" "Mike")が表示されることを確認する
ヒント
この関数はリストの各要素に対して再帰的に処理を行う。(first a-list)でリストの先頭要素(AddressNote構造体)を取得し、AddressNote-nameで名前を抽出する。(rest a-list)で残りのリストに対して同じ処理を繰り返す。
例題6:複素数の計算
目的
DrSchemeに組み込まれているrectangular構造体を使用し、複素数の四則演算を行う。make-rectangularによる複素数の表現方法を理解する。
手順
- 実行用ウインドウで以下の式を順番に実行する
(+ (make-rectangular 1 2) (make-rectangular 3 4)) (- (make-rectangular 5 7) (make-rectangular 3 4)) (* (make-rectangular 1 2) (make-rectangular 3 4)) (/ (make-rectangular -5 10) (make-rectangular 3 4)) - 各計算結果を確認する
ヒント
(make-rectangular x y)は実数部x、虚数部yの複素数を表す。(make-rectangular 3 4)は3+4iを意味する。(+ (1+2i) (3+4i))のように直接記述するとシンタックスエラーになるため、必ずmake-rectangularを使用する。
例題7:複素数のべき乗
目的
複素数のべき乗を計算する関数を作成する。expt関数が複素数にも適用できることを確認する。
手順
- 定義用ウインドウに以下のコードを入力する
(define (myexp theta n) (expt (make-rectangular (cos theta) (sin theta)) n)) - Executeボタンを押す
- 実行用ウインドウで以下を順番に実行する
(myexp 0.5236 1) (myexp 0.5236 2) (myexp 0.5236 3) - 結果に「#i」が付いていることを確認する(#iは近似値を意味する)
ヒント
この関数は(cosθ + i sinθ)nを計算する。θの単位はラジアンである。0.5236ラジアンは約30度に相当する。
課題1:distance-to-0の実行
目的
例題2で作成したdistance-to-0関数を異なる入力で実行し、動作を確認する。
手順
- 例題2のコードを定義用ウインドウに入力しExecuteする
- 実行用ウインドウで以下の3つの式を実行する
(distance-to-0 (make-ball 1 2 0 0)) (distance-to-0 (make-ball (- 5 3) (- 4 6) 0 0)) (distance-to-0 (make-ball (* 2 3) (* 4 5) 0 0)) - 各実行結果を記録し報告する
ヒント
2番目と3番目の式では、make-ballの引数に算術式が含まれている。まず算術式が評価され、その結果がx, yの値として使用される。
課題2:関数inの作成
目的
ball構造体のx座標が特定の範囲内にあるかを判定する関数を作成する。条件分岐と比較演算の組み合わせを練習する。
手順
- ball構造体を定義する
- 関数inを作成する。この関数は以下の仕様を満たす必要がある
- 入力:ball構造体
- 出力:xの値が0より大きく100より小さい場合はtrue、それ以外はfalse
- x = 0またはx = 100の場合はfalseを返す
- 作成した関数をテストし、結果を報告する
ヒント
スライドに示されたcond文の骨格を完成させる。ball-xセレクタでx座標を取得し、比較演算子(<, >)を使って範囲判定を行う。「0 < x < 100」を満たす条件をSchemeの論理式で表現する方法を考える。
課題3:年齢による分類
目的
AddressNote構造体の年齢属性に基づいて、成人か子供かを判定する関数を作成する。
手順
- AddressNote構造体を定義する
- 関数を作成する。この関数は以下の仕様を満たす必要がある
- 入力:AddressNote構造体
- 出力:ageが20以上なら'Adult、20未満なら'Child
- 作成した関数をテストし、結果を報告する
ヒント
AddressNote-ageセレクタで年齢を取得する。cond文または条件式を使って20との比較を行う。
課題4:関数check-by-ageの作成
目的
指定された年齢と一致する場合に名前を返す関数を作成する。2つの引数を取る関数の定義を練習する。
手順
- AddressNote構造体を定義する
- 関数check-by-ageを作成する。この関数は以下の仕様を満たす必要がある
- 入力:AddressNote構造体(a-person)と年齢(an-age)
- 出力:a-personの年齢がan-ageと一致すれば名前を返し、一致しなければ"none"を返す
- スライドに示された例で動作を確認し、結果を報告する
ヒント
関数定義で2つのパラメータを受け取る形式を使用する。年齢の比較には=演算子を用いる。
課題5:関数selection-by-ageの作成
目的
AddressNote構造体のリストから、指定年齢と一致する要素のみを抽出する関数を作成する。リストのフィルタリング処理を練習する。
手順
- AddressNote構造体を定義する
- 関数selection-by-ageを作成する。この関数は以下の仕様を満たす必要がある
- 入力:AddressNote構造体のリスト(a-list)と年齢(an-age)
- 出力:年齢がan-ageと一致するAddressNote構造体のリスト
- スライドに示された例で動作を確認し、結果を報告する
ヒント
例題5のselect-name関数の構造を参考にする。ただし、select-nameは全要素の名前を抽出するのに対し、この関数は条件に合う要素のみをリストに含める。条件判定の結果によってconsするかどうかを分岐させる。
課題6:関数sum-ageの作成(年齢の平均)
目的
AddressNote構造体のリストから年齢の平均値を求める関数を作成する。集計処理とリスト長の取得を組み合わせる。
手順
- AddressNote構造体を定義する
- 関数を作成する。この関数は以下の仕様を満たす必要がある
- 入力:AddressNote構造体のリスト
- 出力:年齢の平均値
- テストデータで動作を確認し、結果を報告する
ヒント
平均を求めるには、年齢の合計とリストの長さが必要である。合計を求める補助関数と、リストの長さを求める処理(lengthなど)を組み合わせて実装する。
補足:ドモアブルの定理
本演習は発展的な内容であり、さらに学習を深めたい受講者向けである。
目的
ドモアブルの定理((cosθ + i sinθ)n = cos nθ + i sin nθ)を計算で確認し、近似計算における誤差の蓄積を観察する。
手順
- 定義用ウインドウに以下のコードを入力する
(define (f1 theta n) (cond [(= n 0) empty] [else (cons (expt (make-rectangular (cos theta) (sin theta)) n) (f1 theta (- n 1)))])) (define (f2 theta n) (cond [(= n 0) empty] [else (cons (make-rectangular (cos (* n theta)) (sin (* n theta))) (f2 theta (- n 1)))])) - Executeボタンを押す
- 実行用ウインドウで以下を実行する
(f1 0.05 20) (f2 0.05 20) - 2つの結果を比較し、値が微妙に異なることを確認する
ヒント
f1は(cosθ + i sinθ)を計算してからn乗するため、近似誤差が累積する。f2は直接cos nθとsin nθを計算する。数学的には同じ値になるはずだが、計算結果にわずかな差異が生じる。これが数値計算における誤差の蓄積である。
sp-11. 構造体とグラフィックス
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/K6JV4Z-2022-01-26-145515
演習パート(クリックして展開)
事前準備
DrSchemeの起動と設定
DrScheme(現在はRacketとして知られる処理系の旧名称)を使用するための初期設定を行う。
- DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
- 言語設定を「Intermediate Student」に変更する(Language → Choose Language → Intermediate Student → Executeボタン)
- draw.ss teachpackをロードする(Language → Add Teachpack → htdpディレクトリ → draw.ss → Executeボタン)
例題1:簡単な絵を描く
目的:
DrSchemeの描画機能であるdraw.ss teachpackを使用し、線、矩形、円などの基本図形を描画する方法を習得する。
- DrSchemeの「実行用ウインドウ」(下部のインタラクションエリア)を使用する
(start 100 100)を入力して実行し、100×100ピクセルの描画用ウインドウを開く- 以下の描画命令を順に実行する
(draw-solid-line (make-posn 0 0) (make-posn 80 80) 'red)— 赤い線を描く(draw-solid-rect (make-posn 50 50) 50 20 'green)— 緑の塗りつぶし矩形を描く(draw-circle (make-posn 20 20) 20 'blue)— 青い円を描く(draw-solid-disk (make-posn 70 70) 10 'red)— 赤い塗りつぶし円を描く
(stop)を実行して描画用ウインドウを閉じる
make-posnは座標を表すposn構造体(position:位置を表す構造体)を生成する関数である。(make-posn x y)の形式でx座標とy座標を指定する- 色は
'red、'green、'blueなどのシンボル(クォート記号'に続く識別子)で指定する - 描画用ウインドウの座標系は、左上が原点(0, 0)で、x軸は右方向、y軸は下方向が正となる
例題2:ball構造体を描く
目的:
構造体(複数の属性をまとめたデータ型)とグラフィックス処理を組み合わせ、構造体のデータを用いて描画を行う方法を学ぶ。
- 「定義用ウインドウ」(上部のエディタエリア)に以下のコードを入力する
(define-struct ball (x y delta-x delta-y)) (define (draw-ball a-ball) (draw-solid-disk (make-posn (ball-x a-ball) (ball-y a-ball)) 5 'red)) - Executeボタンを押して定義を読み込む
- 「実行用ウインドウ」で以下を順に実行する
(start 100 100) (draw-ball (make-ball 10 10 0 0)) (stop)
define-structは構造体を定義する特殊形式である。(define-struct ball (x y delta-x delta-y))により、ball構造体が定義され、自動的に以下が生成される- コンストラクタ(構造体を生成する関数):
make-ball - セレクタ(属性を取り出す関数):
ball-x、ball-y、ball-delta-x、ball-delta-y
- コンストラクタ(構造体を生成する関数):
- ball構造体では、x, yが位置を、delta-x, delta-yが速度を表す
(make-ball 10 10 0 0)は、位置(10, 10)、速度(0, 0)のballを生成する
例題3:リストの変数定義
目的:
define を使用してリストを値とする変数を定義し、リスト操作関数の動作を確認する。
- 「定義用ウインドウ」に以下を入力し、Executeボタンを押す
(define A (list 15 8 6)) - 「実行用ウインドウ」で以下を順に実行する
A (first A) (rest A)
defineは変数を定義する特殊形式である。(define 変数名 値)の形式で使用するfirstはリストの先頭要素を返し、restはリストの先頭を除いた残りのリストを返すAと入力すると、変数Aの値が表示される
例題4:構造体のリスト
目的:
構造体のリストを定義し、リスト操作と構造体のセレクタを組み合わせて使用する方法を学ぶ。
- 「定義用ウインドウ」に以下を入力し、Executeボタンを押す
(define-struct address-record (name age address)) (define book (list (make-address-record "Mike" 10 "Fukuoka") (make-address-record "Bill" 20 "Saga") (make-address-record "Ken" 30 "Nagasaki"))) - 「実行用ウインドウ」で以下を順に実行する
book (first book) (address-record-address (first book))
(first book)はbookリストの最初の要素(Mikeのaddress-record)を返す(address-record-address (first book))は、最初の要素からaddress属性を取り出すため、"Fukuoka"が返される- 構造体のリストでは、リスト操作(first, rest)と構造体のセレクタを組み合わせて個々の属性にアクセスする
例題5:頂点のリストを値とする変数の定義
目的:
posn構造体(DrSchemeに組み込み済みの座標を表す構造体)のリストを定義し、図形の頂点データを表現する方法を学ぶ。
- 「定義用ウインドウ」に以下を入力し、Executeボタンを押す
(define P (cons (make-posn 0 0) (cons (make-posn 10 70) (cons (make-posn 100 30) empty)))) - 「実行用ウインドウ」で以下を順に実行する
P (first P) (rest P)
consはリストの先頭に要素を追加する関数である。(cons 要素 リスト)の形式で使用するemptyは空リストを表す- posn構造体はDrSchemeに組み込み済みのため、
define-structによる定義なしで使用できる (list (make-posn 0 0) (make-posn 10 70) (make-posn 100 30))と書いても同じリストが生成される
例題6:折れ線
目的:
再帰(関数が自分自身を呼び出す手法)を用いて、頂点のリストから折れ線を描画する関数を作成する。
- 「定義用ウインドウ」に以下を入力し、Executeボタンを押す
(define P (cons (make-posn 0 0) (cons (make-posn 10 70) (cons (make-posn 100 30) empty)))) (define (draw-polyline a-poly) (cond [(empty? (rest a-poly)) true] [else (and (draw-solid-line (first a-poly) (first (rest a-poly))) (draw-polyline (rest a-poly)))])) - 「実行用ウインドウ」で以下を順に実行する
(start 100 100) (draw-polyline P) (stop)
- 終了条件は
(empty? (rest a-poly))である。これは「リストに残り1点のみ」を意味し、これ以上線分を描けない状態を表す - 描画命令と他の命令を並べるときは
andでつなぐ必要がある。andを書き忘れるとエラーになる (first (rest a-poly))は「リストの2番目の要素」を取得する方法である- 再帰呼び出し
(draw-polyline (rest a-poly))により、リストの残りの部分について同様の処理を行う
例題7:多角形
目的:
例題6の折れ線描画を拡張し、終点と始点を結んで多角形を描画する関数を作成する。
- 「定義用ウインドウ」に例題6のコードに加えて以下を追加し、Executeボタンを押す
(define (last a-poly) (cond [(empty? (rest a-poly)) (first a-poly)] [else (last (rest a-poly))])) (define (draw-polygon a-poly) (draw-polyline (cons (last a-poly) a-poly))) - 「実行用ウインドウ」で以下を順に実行する
(start 100 100) (draw-polygon P) (stop)
last関数はリストの最後の要素を返す再帰関数であるdraw-polygonは、元のリストの先頭に最後の要素を追加してからdraw-polylineを呼び出す。これにより、最後の点から最初の点への線分も描画される- 例:リストが
(A B C)の場合、(cons (last a-poly) a-poly)により(C A B C)となり、C→A→B→Cの順に線が引かれる
課題1:描画命令の実行
目的:
描画関数のパラメータを変更し、異なるサイズ・位置での描画結果を確認する。
- 描画用ウインドウを開く:
(start 300 300) - 赤い線を描く:
(draw-solid-line (make-posn 100 100) (make-posn 200 200) 'red) - 赤い円を描く:
(draw-circle (make-posn 200 100) 50 'red) - 緑の塗りつぶし円を描く:
(draw-solid-disk (make-posn 100 200) 50 'green) - 描画用ウインドウを閉じる:
(stop)
- 例題1と比較して、ウインドウサイズが300×300に拡大されている点に注意する
- 各描画関数のパラメータ(座標、サイズ、色)と描画結果の関係を観察する
draw-circleは輪郭のみ、draw-solid-diskは塗りつぶしである点を確認する
課題2:ボールの速さを求める関数
目的:
ball構造体から速度成分を取り出し、数学的な計算を行う関数を作成する。
- 例題2のball構造体定義を使用する
- ball構造体から速さを計算する関数を作成する
- 作成した関数をテストし、結果を確認する
- ボールの速さは √(delta-x² + delta-y²) で計算される
- Schemeでは平方根は
sqrt関数、累乗はsqr関数(2乗)またはexpt関数(一般の累乗)を使用する - セレクタ
ball-delta-xとball-delta-yを使用して速度成分を取り出す - 関数の入力はball構造体、出力は数値(速さ)となる
課題3:境界判定関数
目的:
ball構造体の位置を評価し、条件分岐を用いて境界外かどうかを判定する関数を作成する。
- 例題2のball構造体定義を使用する
- ball構造体の位置が「x < 0 または y < 0 または x > 100 または y > 100」のときtrueを返す関数を作成する
- 境界内外の両方のケースでテストする
- Schemeでは論理和(または)は
or関数を使用する - 比較演算子として
<と>を使用する - 条件が1つでも成立すればtrueを返し、すべて成立しなければfalseを返す
課題4:2点間の距離を求める関数
目的:
点を表す構造体を自分で定義し、2点間の距離を計算する関数を作成する。
- x-y平面上の点を表す構造体を定義する
- 2点a, bを受け取り、その間の距離を返す関数
distanceを作成する - テスト用の点を作成し、関数の動作を確認する
- 2点間の距離は √((x₂-x₁)² + (y₂-y₁)²) で計算される
- 点を表す構造体には、少なくともxとyの2つの属性が必要である
- posn構造体を使用することも、独自の構造体を定義することも可能である
課題5:複数のballを描画
目的:
リストの再帰処理を用いて、複数のball構造体を描画する関数を作成する。
- 例題2の
draw-ball関数を参照する - ball構造体のリストを受け取り、すべてのballを描画する関数
draw-all-ballsを作成する - 複数のball構造体を含むリストを作成し、動作を確認する
- 例題6の
draw-polylineと同様の再帰パターンを使用する - 終了条件は「リストが空のとき」であり、
empty?で判定する - 各ballに対して
draw-ballを呼び出し、残りのリストに対して再帰呼び出しを行う - 描画命令を並べるときは
andでつなぐことを忘れない
課題6:複数のballを消す
目的:
課題5と同様のパターンで、複数のballを消去する関数を作成する。
clear-ball関数を参照する- ball構造体のリストを受け取り、すべてのballを消す関数
clear-all-ballsを作成する - 動作確認を行う(描画してから消去する)
clear-solid-diskはdraw-solid-diskで描いた円を消す関数である- 課題5の
draw-all-ballsとほぼ同じ構造になる clear-ballは色のパラメータを取らない点に注意する
補足:例題8:ballを1秒描く
目的:
タイマー機能を用いて、一定時間だけ図形を表示してから消去する処理を学ぶ。
- 「定義用ウインドウ」に以下を入力し、Executeボタンを押す
(define-struct ball (x y delta-x delta-y)) (define DELAY 1) (define (draw-and-clear a-ball) (and (draw-solid-disk (make-posn (ball-x a-ball) (ball-y a-ball)) 5 'red) (sleep-for-a-while DELAY) (clear-solid-disk (make-posn (ball-x a-ball) (ball-y a-ball)) 5 'red))) - 「実行用ウインドウ」で以下を順に実行する
(start 100 100) (draw-and-clear (make-ball 10 10 0 0)) (stop)
sleep-for-a-whileは指定した秒数だけ処理を停止する関数である- 変数
DELAYを使用することで、待機時間を容易に変更できる andで3つの処理(描画、待機、消去)を順番に実行している
補足:例題9:動くballを描く
目的:
再帰とタイマーを組み合わせて、ballが移動するアニメーションを作成する。
- 「定義用ウインドウ」に以下を入力し、Executeボタンを押す
(define-struct ball (x y delta-x delta-y)) (define DELAY 1) (define (draw-ball a-ball) (draw-solid-disk (make-posn (ball-x a-ball) (ball-y a-ball)) 5 'red)) (define (clear-ball a-ball) (clear-solid-disk (make-posn (ball-x a-ball) (ball-y a-ball)) 5)) (define (move-ball a-ball) (make-ball (+ (ball-x a-ball) (ball-delta-x a-ball)) (+ (ball-y a-ball) (ball-delta-y a-ball)) (ball-delta-x a-ball) (ball-delta-y a-ball))) (define (animation a-ball) (and (draw-ball a-ball) (sleep-for-a-while DELAY) (clear-ball a-ball) (animation (move-ball a-ball)))) - 「実行用ウインドウ」で以下を順に実行する
(start 100 100) (animation (make-ball 0 0 10 5)) (stop)
move-ballは現在位置に速度を加算して新しいball構造体を生成するanimationは自分自身を再帰的に呼び出すことで、無限にアニメーションを続ける- アニメーションを停止するには、描画用ウインドウを手動で閉じるか、DrSchemeを停止する
- 終了条件がないため、このままでは無限ループとなる
課題7:複数のballを動かす
目的:
リストの各要素に対して移動処理を適用する関数を作成する。
- 例題9の
move-ball関数を参照する - ball構造体のリストを受け取り、すべてのballを移動させた新しいリストを返す関数
move-all-ballsを作成する - 動作確認を行う
- この関数は描画ではなく、リストの変換を行う
- 入力:ball構造体のリスト、出力:移動後のball構造体のリスト
consを使用して、移動後のballを新しいリストに追加していく- 再帰の終了条件は「リストが空のとき」で、空リスト
emptyを返す
課題8:境界外判定とウインドウ制御
目的:
条件判定を組み込み、すべてのballが境界外になったらアニメーションを終了する機能を実装する。
- 課題3の境界判定関数を活用する
- 課題5の
draw-all-ballsを修正し、境界外のballは描画しないようにする - すべてのballが境界外にあるかを判定する関数を作成する
- すべてが境界外の場合、
(stop)で描画用ウインドウを閉じる
condを使用して、境界内のballのみ描画するよう条件分岐する- 「すべてのballが境界外」を判定するには、リスト内のすべての要素について境界判定を行い、論理積(and)を取る
- または、境界内のballが1つでも存在すればアニメーションを続行するという判定方法もある
課題9:複数ballのアニメーション
目的:
例題9を拡張し、複数のballが同時に動くアニメーションを作成する。
- 課題5(draw-all-balls)、課題6(clear-all-balls)、課題7(move-all-balls)を組み合わせる
- すべてのballが境界外になったらアニメーションが終了するようにする
- 複数のball構造体を含むリストを作成し、アニメーションの動作を確認する
- 例題9の
animation関数の構造を参考に、リストを処理するバージョンを作成する - 終了条件として課題8で作成した「すべて境界外」の判定を使用する
- 終了時には
trueを返すようにする
課題10:動的な大きさ・色の変化
目的:
アニメーション中にballの属性(大きさや色)を変化させる機能を追加する。
- 課題9のプログラムを基にする
- ball構造体に大きさや色の属性を追加することを検討する、または別の方法で大きさ・色を変化させる
- 移動するたびに属性が変化するようにプログラムを修正する
- 方法1:ball構造体に大きさ(size)や色(color)の属性を追加する
- 方法2:移動回数や位置に応じて大きさ・色を計算する
- 色をリストで用意し、順番に変化させる方法もある
- 大きさの変化には上限・下限を設けると見やすくなる
課題11:跳ね返り処理
目的:
境界に達したballが跳ね返る物理シミュレーションを実装する。
- 課題9のプログラムを基にする
- ballが境界に達したとき、速度の符号を反転させて跳ね返るようにする
- 跳ね返りの動作を確認する
- x座標が0未満または100超になったとき、delta-xの符号を反転する
- y座標についても同様にdelta-yの符号を反転する
- 境界判定と速度反転を行う新しい関数を作成するか、
move-ballを修正する - 反転後の新しいball構造体を
make-ballで生成する
sp-12. 再帰と繰り返しの回数
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/ZNV125-2022-01-26-145501
再帰的定義に基づいて階乗関数を作成し、再帰呼び出しの基本構造を理解する。 階乗の再帰的定義は「n = 0のとき1、n > 0のとき n × (n-1)!」である。プログラム中の DrSchemeのstepper機能を用いて、再帰呼び出しがどのように展開・収縮するかを視覚的に理解する。 式が膨張していく過程(基本的な計算式への展開)と、収縮していく過程(演算の実行)の2段階があることに注目する。この形式を線形再帰的プロセス(linear recursive process)と呼ぶ。再帰の呼び出し回数に比例して式が成長するため「線形」と名付けられている。 反復的プロセス(iterative process)による階乗計算を実装し、線形再帰的プロセスとの違いを理解する。 この方式では、productに部分積(計算途中の結果)、counterに現在の乗数を保持しながら計算を進める。線形再帰的プロセスのような式の伸び縮みがなく、各ステップで計算が実行される点が特徴である。終了条件は 反復的プロセスの実行過程をstepperで確認し、線形再帰的プロセスとの違いを明確にする。 線形再帰的プロセス(例題2)では式が膨張してから収縮したが、反復的プロセスでは式のサイズが一定のまま計算が進む。product、counter、maxの3つの値だけで計算状態が完全に表現されている点に注目する。 リスト処理における関数の繰り返し回数について考察する。 ユークリッドの互除法(Euclidean algorithm)を用いて最大公約数を求める関数を作成する。 ユークリッドの互除法は以下の原理に基づく。mとnの最大公約数を求めるとき、n = 0ならば答えはm、n ≠ 0ならば「nとmをnで割った余り」の最大公約数に等しい。 ユークリッドの互除法の実行過程をstepperで確認し、繰り返し回数を数える。 m=180、n=32のとき、 例題6〜7で学んだユークリッドの互除法の理解を確認し、異なる入力での実行過程を説明できるようになる。 例題7の リストを引数とする再帰関数の構造を理解し、空欄補充によってプログラムを完成させる。 リストが空かどうかの判定には 同じ問題を解く2つの異なるアルゴリズムの効率を比較し、アルゴリズムの選択が重要であることを理解する。 エラトステネスのふるい(Sieve of Eratosthenes)の原理を理解し、100以下の素数を求めるプログラムを作成する。 11の倍数を消す必要がない理由は、100以下の11の倍数で11より大きいもの(22, 33, ...)は、すでに2や3の倍数として消されているからである。したがって、√100 = 10までの素数の倍数を消せば十分である。プログラムの構造として、「リストの先頭の数の倍数を消し、残りのリストに対して同じ処理を繰り返す」という再帰的な考え方が有効である。 以下にエラトステネスのふるいを実装したプログラム例を示す。このプログラムは3つの関数から構成される。 上記のコードを定義用ウインドウに入力し、Executeボタンを押した後、実行用ウインドウで以下を実行する。 補足:この実装では、sieve関数がリストの先頭要素の倍数を消しながら再帰的に処理を進める。リストの先頭要素は必ず素数である(それより小さい数の倍数としてすでに消されているため)。この性質を利用して、先頭要素を素数リストに追加し、残りのリストに対して同じ処理を繰り返す。演習パート(クリックして展開)
例題1:階乗
目的
手順
;;! : number -> number
;;to compute n*(n-1)*...*2*1
(define (! n)
(cond
[(= n 0) 1]
[else (* n (! (- n 1)))]))(! 3) と (! 4) を実行し、結果を確認する。ヒント
(= n 0) が終了条件、(* n (! (- n 1))) が再帰呼び出しに対応する。終了条件がないと無限に再帰呼び出しが続くため、必ず終了条件を設けること。例題2:ステップ実行(階乗)
目的
手順
(! 3) を追加し、定義用ウインドウに入力する。
;;! : number -> number
;;to compute n*(n-1)*...*2*1
(define (! n)
(cond
[(= n 0) 1]
[else (* n (! (- n 1)))]))
(! 3)
(! 3) → (* 3 (! 2)) → (* 3 (* 2 (! 1))) → (* 3 (* 2 (* 1 (! 0))))(* 3 (* 2 (* 1 1))) → (* 3 (* 2 1)) → (* 3 2) → 6ヒント
(! 3) では関数 ! が4回繰り返して実行される。例題3:反復的プロセスでの階乗
目的
手順
;; ! : number -> number
;; to compute n*(n-1)*...*2*1
;; (! 4) = 24
(define (! n)
(factorial 1 1 n))
(define (factorial product counter max)
(cond
[(> counter max) product]
[else (factorial (* counter product)
(+ counter 1)
max)]))(! 4)、(! 10)、(! 20) を実行する。factorial 関数の3つの引数(product、counter、max)の役割を確認する。ヒント
counter > max のときで、そのときのproductが答えとなる。例題4:ステップ実行(反復的プロセス)
目的
手順
(! 4) を追加し、定義用ウインドウに入力する。
(factorial 1 1 4) → (factorial 1 2 4) → (factorial 2 3 4) → (factorial 6 4 4) → (factorial 24 5 4) → 24ヒント
(! 4) では factorial が5回繰り返して実行される。例題5:繰り返し回数
目的
手順
(define (square x) (* x x))
(define (total-square x)
(cond
[(empty? x) 0]
[else (+ (square (first x))
(total-square (rest x)))]))(total-square (list 10 20 30)) を実行し、結果が1400になることを確認する。square 関数が何回実行されるかを考える。ヒント
square 関数はリストの要素ごとに1回ずつ呼び出される。リストの要素数が3であれば、square は3回実行される。total-square 自体は要素数+1回(終了条件の判定を含む)呼び出される。例題6:最大公約数の計算
目的
手順
;; my-gcd: number number -> number
;; to find the greatest common divisor of n and m
;; Example: (my-gcd 180 32) = 4
(define (my-gcd m n)
(cond
[(= n 0) m]
[else (my-gcd n (remainder m n))]))(my-gcd 180 32) を実行し、結果が4になることを確認する。ヒント
remainder 関数は剰余(割り算の余り)を返す。終了条件は n = 0 のときで、そのときのmが最大公約数となる。例題7:ステップ実行(最大公約数)
目的
手順
(my-gcd 180 32) を追加し、定義用ウインドウに入力する。
(my-gcd 180 32) → (my-gcd 32 20) → (my-gcd 20 12) → (my-gcd 12 8) → (my-gcd 8 4) → (my-gcd 4 0) → 4my-gcd が何回呼び出されるかを数える。ヒント
my-gcd は6回繰り返して実行される。各ステップで余りがどのように減少していくかに注目する。ユークリッドの互除法は非常に効率的なアルゴリズムであり、入力値に対して繰り返し回数が少ない。課題1:my-gcdの実行過程
目的
手順
(my-gcd 210 66) をステップ実行する。ヒント
(my-gcd 180 32) の説明を参考にする。各ステップで「mをnで割った余り」を計算し、mとnの値がどう変わるかを追跡する。説明では、呼び出しの列((my-gcd 210 66) → (my-gcd ? ?) → ...)を示しながら記述するとよい。課題2:sum-coins関数
目的
手順
;; Contract: sum-coins : a-list-of-numbers a-list-of-numbers -> number
;; Example: (sum-coins (list 23 0 5 7) (list 1 5 10 25)) = 248
(define (sum-coins a b)
(cond
[( ) ]
[else (+ (* ( ) ( ))
( ( ) ( )))]))(sum-coins (list 23 0 5 7) (list 1 5 10 25)) の結果が248になることを確認する。ヒント
(= a empty) ではなく (empty? a) を使用すること。= は数値の比較用であり、リスト同士の比較には使えない。リストの先頭要素は first 関数、残りのリストは rest 関数で取得する。sum-coinsは2つのリストを同時に処理するため、両方のリストに対してfirstとrestを適用する必要がある。課題3:繰り返し回数の比較
目的
手順
my-gcd を用意する。gcd-structural を入力する。
(define (first-divisor n m i)
(cond
[(= i 1) 1]
[else (cond
[(and (= (remainder n i) 0)
(= (remainder m i) 0)) i]
[else (first-divisor n m (- i 1))])]))
(define (gcd-structural n m)
(first-divisor n m (min n m)))ヒント
gcd-structural の方式は「nとmの小さい方から1ずつ減らしながら、両方を割り切れる数を探す」というものである。この方式とユークリッドの互除法で、繰り返し回数がどの程度異なるかに注目する。特に入力値が大きい場合の違いを観察するとよい。考察では、なぜ一方が効率的なのかを、アルゴリズムの仕組みから説明することを試みる。課題4:エラトステネスのふるい
目的
手順
ヒント
正解例
;; range: number number -> list-of-numbers
;; 整数startからendまでのリストを生成する
;; Example: (range 2 10) = (list 2 3 4 5 6 7 8 9 10)
(define (range start end)
(cond
[(> start end) empty]
[else (cons start (range (+ start 1) end))]))
;; remove-multiples: number list-of-numbers -> list-of-numbers
;; リストからnの倍数(n自身を除く)を取り除く
;; Example: (remove-multiples 2 (list 2 3 4 5 6)) = (list 2 3 5)
(define (remove-multiples n lst)
(cond
[(empty? lst) empty]
[(and (> (first lst) n)
(= (remainder (first lst) n) 0))
(remove-multiples n (rest lst))]
[else (cons (first lst)
(remove-multiples n (rest lst)))]))
;; sieve: list-of-numbers -> list-of-numbers
;; エラトステネスのふるいを適用して素数のリストを返す
;; Example: (sieve (list 2 3 4 5 6 7 8 9 10)) = (list 2 3 5 7)
(define (sieve lst)
(cond
[(empty? lst) empty]
[else (cons (first lst)
(sieve (remove-multiples (first lst)
(rest lst))))]))
;; primes-up-to: number -> list-of-numbers
;; n以下の素数のリストを返す
;; Example: (primes-up-to 100) = (list 2 3 5 7 11 ... 97)
(define (primes-up-to n)
(sieve (range 2 n)))実行方法
(primes-up-to 100)実行結果
(list 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)各関数の説明
関数名
機能
終了条件
rangestartからendまでの整数のリストを生成する
start > end のとき空リストを返す
remove-multiplesリストからnの倍数(n自身を除く)を取り除く
リストが空のとき空リストを返す
sieveリストの先頭を素数として残し、その倍数を消す処理を繰り返す
リストが空のとき空リストを返す
primes-up-to2からnまでのリストにふるいを適用する
(sieveに委譲)
sp-13. 数値微分と数値積分
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/KYMDLK-2022-01-26-145417
高階関数(関数を引数として受け取る関数)である d/dx を使用して、数値微分の基本的な使い方を習得する。 以下のコードを「定義用ウインドウ」に入力する。 d/dx 関数は、中心差分公式 (f(x+h) - f(x-h)) / (2h) を用いて接線の傾きを近似している。cos 関数は Scheme の組み込み関数として使用できる。f'(x) = cos x - x sin x であるから、理論値と比較することで近似の精度を確認できる。 刻み幅 h を変化させたときの近似精度の変化を観察し、数値微分における h の選択が結果に与える影響を理解する。 課題1と同じコードを使用する。 h を小さくすると一般に誤差は減少するが、h が極端に小さいと丸め誤差(コンピュータの数値表現の限界による誤差)の影響が現れる場合がある。グラフ作成には表計算ソフトウェア等を使用するとよい。 台形則(trapezoidal rule)を用いた数値積分の実行方法を習得し、積分の近似値と理論値を比較する。 以下のコードを「定義用ウインドウ」に入力する。 trapezoid 関数の引数は順に「関数 f」「積分区間の下端 a」「積分区間の上端 b」「分割数 n」である。分割数 n を大きくすると一般に精度は向上するが、計算時間も増加する。台形則の公式を確認しておくと、プログラムの動作が理解しやすい。 分割数 n を変化させたときの近似精度の変化を観察し、台形則の誤差が O(1/n²) であることを確認する。 演習3と同じコードを使用する。 対数グラフ(両対数プロット)を使用すると、S_n - I ≈ c/n² の関係が直線として現れ、確認が容易になる。傾き -2 の直線になれば、誤差が n の2乗に反比例していることを示す。 d/dx 関数を利用したグラフィックスプログラムを実行し、関数のグラフと指定した点における接線を視覚的に確認する。 以下のコード全体を「定義用ウインドウ」に入力する。 プログラム中の定数 X0, Y0 は原点の位置、SX, SY は1グリッドのピクセルサイズを表す。PX は接線を描画する x 座標である。これらの値を変更することで、表示範囲や接線の位置を調整できる。プログラムの構造を理解するには、samples 関数がグラフ上の点列を生成し、draw-polyline 関数がそれらを線で結ぶという流れを把握するとよい。演習パート(クリックして展開)
課題1:接線の傾きの計算
目的
必要なソースコード
;; d/dx: (number->number) number number -> number
;; inclination of the tangent
;; Example: (d/dx f 3 0.0001)
;; = ((- (f 3.0001) (f 2.9999)) 0.0002)
(define (d/dx f x h)
(/ (- (f (+ x h)) (f (- x h))) (* 2 h)))
;; f2: number -> number
;; f(x) = x * cos(x)
(define (f2 x)
(* x (cos x)))手順
(d/dx f2 0 0.1)
(d/dx f2 0.2 0.1)
(d/dx f2 0.4 0.1)
(d/dx f2 0.6 0.1)ヒント
演習2:刻み幅と誤差の関係(数値微分)
目的
必要なソースコード
;; d/dx: (number->number) number number -> number
;; inclination of the tangent
(define (d/dx f x h)
(/ (- (f (+ x h)) (f (- x h))) (* 2 h)))
;; f2: number -> number
;; f(x) = x * cos(x)
(define (f2 x)
(* x (cos x)))手順
(d/dx f2 0.4 0.1)
(d/dx f2 0.4 0.01)
(d/dx f2 0.4 0.001)ヒント
演習3:台形則による数値積分
目的
必要なソースコード
;; trapezoid-iter: (number->number) number number number -> number
;; helper function for trapezoid
(define (trapezoid-iter f a h k)
(cond
[(= k 1) (f (+ a h))]
[else (+ (f (+ a (* h k)))
(trapezoid-iter f a h (- k 1)))]))
;; trapezoid: (number->number) number number number -> number
;; to compute the area under the graph of f between a and b
(define (trapezoid f a b n)
(* (/ (- b a) n)
(+ (trapezoid-iter f a (/ (- b a) n) (- n 1))
(/ (f a) 2)
(/ (f b) 2))))
;; f2: number -> number
;; f(x) = 1 / (1 + x)
(define (f2 x)
(/ 1 (+ 1 x)))手順
(trapezoid f2 0 1 1000)(log 2)ヒント
演習4:分割数と誤差の関係(数値積分)
目的
必要なソースコード
;; trapezoid-iter: (number->number) number number number -> number
;; helper function for trapezoid
(define (trapezoid-iter f a h k)
(cond
[(= k 1) (f (+ a h))]
[else (+ (f (+ a (* h k)))
(trapezoid-iter f a h (- k 1)))]))
;; trapezoid: (number->number) number number number -> number
;; to compute the area under the graph of f between a and b
(define (trapezoid f a b n)
(* (/ (- b a) n)
(+ (trapezoid-iter f a (/ (- b a) n) (- n 1))
(/ (f a) 2)
(/ (f b) 2))))
;; f2: number -> number
;; f(x) = 1 / (1 + x)
(define (f2 x)
(/ 1 (+ 1 x)))手順
(trapezoid f2 0 1 4)
(trapezoid f2 0 1 8)
(trapezoid f2 0 1 16)
(trapezoid f2 0 1 32)
(trapezoid f2 0 1 64)
(trapezoid f2 0 1 128)ヒント
演習4(グラフィックス):関数と接線のグラフ表示
目的
必要なソースコード
;; d/dx: (number->number) number number -> number
;; inclination of the tangent
;; Example: (d/dx f 3 0.0001)
;; = ((- (f 3.0001) (f 2.9999)) 0.0002)
(define (d/dx f x h)
(/ (- (f (+ x h)) (f (- x h))) (* 2 h)))
;; samples: (number->number) number number number -> list of 'posn' structure
(define (samples f a h k)
(cond
[(= k 1) (cons (make-posn (+ a h) (f (+ a h))) empty)]
[else (cons (make-posn (+ a (* h k)) (f (+ a (* h k))))
(samples f a h (- k 1)))]))
; window size
(define WX 500)
(define WY 500)
; grid color, axis color and graph color
(define GRID_COLOR 'blue)
(define AXIS_COLOR 'red)
(define GRAPH_COLOR 'red)
; draw-a-line
(define (sessen x px py d)
(+ (* d (- x px)) py))
(define (draw-a-sessen from to px py d x0 y0 sx sy)
(draw-solid-line
(make-posn (+ (* from sx) x0)
(+ (* (sessen from px py d) sy) y0))
(make-posn (+ (* to sx) x0)
(+ (* (sessen to px py d) sy) y0))
GRAPH_COLOR))
(define (draw-sessen f px x0 y0 sx sy)
(draw-a-sessen (/ (- x0) sx) (/ (- WX x0) sx) px (f px)
(d/dx f px 0.00001) x0 y0 sx sy))
; draw-polyline
(define (draw-polyline a-poly)
(cond
[(empty? (rest a-poly)) true]
[else (and (draw-solid-line (first a-poly)
(first (rest a-poly))
GRAPH_COLOR)
(draw-polyline (rest a-poly)))]))
; draw-h-lines
(define (draw-h-lines start skip stop width)
(cond
[(>= start stop)
(draw-solid-line (make-posn 0 stop)
(make-posn width stop)
GRID_COLOR)]
[else (and (draw-solid-line (make-posn 0 start)
(make-posn width start)
GRID_COLOR)
(draw-h-lines (+ start skip) skip stop width))]))
; draw-v-lines
(define (draw-v-lines start skip stop height)
(cond
[(>= start stop)
(draw-solid-line (make-posn stop 0)
(make-posn stop height)
GRID_COLOR)]
[else (and (draw-solid-line (make-posn start 0)
(make-posn start height)
GRID_COLOR)
(draw-v-lines (+ start skip) skip stop height))]))
; (x0, y0) is the origin. sx and sy represent one grid size
(define (draw-grid x0 y0 sx sy)
(and (draw-v-lines (- x0 (* (quotient x0 sx) sx)) sx
(+ (* (quotient (- WX x0) sx) sx) x0) WY)
(draw-h-lines (- y0 (* (quotient y0 sy) sy)) sy
(+ (* (quotient (- WY y0) sy) sy) y0) WX)
(draw-solid-line (make-posn 0 y0) (make-posn WX y0) AXIS_COLOR)
(draw-solid-line (make-posn x0 0) (make-posn x0 WY) AXIS_COLOR)))
; (x0, y0) is the origin. sx and sy represent one grid size
(define (scaling a-list x0 y0 sx sy)
(cond
[(empty? a-list) empty]
[else (cons (make-posn (+ (* (posn-x (first a-list)) sx) x0)
(+ (* (posn-y (first a-list)) sy) y0))
(scaling (rest a-list) x0 y0 sx sy))]))
;; f2: number -> number
;; この関数を別のものに変更して実行すること
(define (f2 x)
(- (* x x) 2))
; (X0, Y0) represents the origin of the graph.
; SX and SY represent the size of one grid
(define X0 (/ WX 2))
(define Y0 (/ WY 2))
(define SX 50)
(define SY 50)
(define PX 0.5)
(start WX WY)
(draw-grid X0 Y0 SX SY)
(draw-polyline (scaling (samples f2 (/ (- X0) SX) (/ 1 SX) WX) X0 Y0 SX SY))
(draw-sessen f2 PX X0 Y0 SX SY)手順
または
(define (f2 x) (sin x))(define (f2 x) (* x (cos x)))ヒント
sp-14. ニュートン法
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/KE8XJZ-2022-01-26-145344
ニュートン法(Newton法)のプログラムを実装し、非線形方程式 f(x) = x³ − 6x² + 11x − 6 = 0 の近似解を求める方法を理解する。 Newton法を応用して、任意の数aの立方根(∛a)を求めるプログラムを作成する。 f(x) = x² − 5 に対するニュートン法の収束過程を視覚的に理解し、反復公式の幾何学的意味を確認する。 重解を持つ方程式に対するニュートン法の振る舞いを観察し、初期近似値と収束先の関係を理解する。 ニュートン法が収束しない場合の条件を理解し、その原因をグラフを用いて説明できるようになる。 注意:atanの使用可否は処理系のバージョンによって異なる場合がある。エラーが発生した場合は、使用しているDrSchemeのドキュメントを確認すること。 区間二分法(half-interval method)の原理を理解し、f(x) = x² − 2 = 0 の解(√2)を求めるプログラムを実装する。 DrSchemeのステップ実行機能を用いて、区間二分法の処理過程を詳細に追跡し、再帰呼び出しの動作を理解する。演習パート(クリックして展開)
例題1:ニュートン法による非線形方程式の解
目的
手順
;; d/dx: (number->number) number number -> number
;; inclination of the tangent
(define (d/dx f x h)
(/ (- (f (+ x h)) (f (- x h))) (* 2 h)))
(define (is-good? f guess delta)
(< (abs (f guess)) delta))
(define (improve f guess)
(- guess (/ (f guess) (d/dx f guess 0.0001))))
;; newton: (number->number) number number number -> number
(define (newton f guess delta number)
(cond
[(or (is-good? f guess delta) (< number 0)) guess]
[else (newton f (improve f guess) delta (- number 1))]))
(define (f2 x)
(+ (* x x x) (* -6 x x) (* 11 x) -6))
(newton f2 #i0 0.00001 10000)
(newton f2 #i10 0.00001 10000)ヒント
#i は近似計算(浮動小数点数)を意味する。#i なしで実行すると有理数で計算され、結果が膨大な桁数になる場合がある課題1:立方根を求めるプログラム
目的
手順
;; 例:8の立方根を求める場合
(define (f-cube x)
(- (* x x x) 8))
(newton f-cube #i1 0.00001 10000)
;; -8の立方根を求める場合
(define (f-cube-neg x)
(- (* x x x) -8))
(newton f-cube-neg #i-1 0.00001 10000)
ヒント
(- (* x x x) a) と書ける課題2:ニュートン法での収束
目的
手順
(define (f x)
(- (* x x) 5))
(newton f #i1 0.00001 10000)
ヒント
課題3:(x − 1)⁴(x − 2) = 0 を解く
目的
手順
;; (x-1)^4(x-2) を展開すると x^5 - 6x^4 + 14x^3 - 16x^2 + 9x - 2
(define (f3 x)
(+ (* x x x x x) (* -6 x x x x) (* 14 x x x) (* -16 x x) (* 9 x) -2))
(newton f3 #i0 0.00001 10000)
(newton f3 #i0.5 0.00001 10000)
(newton f3 #i1.5 0.00001 10000)
(newton f3 #i3 0.00001 10000)
(newton f3 #i10 0.00001 10000)
ヒント
演習4:f(x) = tan⁻¹x + 0.3x を解く
目的
手順
(define (f4 x)
(+ (atan x) (* 0.3 x)))
(newton f4 #i0.1 0.00001 10000)
(newton f4 #i1 0.00001 10000)
(newton f4 #i5 0.00001 10000)
(newton f4 #i10 0.00001 10000)
ヒント
例題2:区間二分法による非線形方程式の解
目的
手順
(define (f x)
(- (* x x) 2))
(define (good-enough? a b)
(< (- b a) 0.000001))
(define (middle a b)
(/ (+ a b) 2))
(define (half-interval a b)
(cond
[(good-enough? a b) a]
[else
(cond
[(< (f (middle a b)) 0) (half-interval (middle a b) b)]
[(= (f (middle a b)) 0) (middle a b)]
[(> (f (middle a b)) 0) (half-interval a (middle a b))])]))
(half-interval 0 2) ;; 正しい解(約1.414)が得られる
(half-interval -2 0) ;; 正しくない結果になる
(half-interval 2 4) ;; 正しくない結果になる
ヒント
(half-interval -2 0) では、f(−2) = 2 > 0, f(0) = −2 < 0 であり、プログラムの想定(f(a) < 0 < f(b))と符号が逆(half-interval 2 4) では、f(2) = 2 > 0, f(4) = 14 > 0 であり、区間内に解が存在しない例題3:区間二分法での繰り返し処理
目的
手順
(half-interval 0 2) を追加)(define (f x)
(- (* x x) 2))
(define (good-enough? a b)
(< (- b a) 0.000001))
(define (middle a b)
(/ (+ a b) 2))
(define (half-interval a b)
(cond
[(good-enough? a b) a]
[else
(cond
[(< (f (middle a b)) 0) (half-interval (middle a b) b)]
[(= (f (middle a b)) 0) (middle a b)]
[(> (f (middle a b)) 0) (half-interval a (middle a b))])]))
(half-interval 0 2)
ヒント
sp-15. リスト処理とクイックソート
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/5MVX95-2022-01-26-145317
本演習ではDrSchemeを使用する。起動後、言語設定を「Intermediate Student」に変更すること。 DrSchemeのステッパ(Stepper)機能は、プログラムの実行過程を1ステップずつ確認できるデバッグツールである。再帰関数の動作を理解する際に有用である。 ソート済みリスト(降順に並んだリスト)に新しい要素を適切な位置に挿入する再帰関数の構造を理解する。 例題1のinsert関数を活用し、リスト全体を降順にソートするインサーションソート(挿入ソート)のアルゴリズムを実装する。 再帰関数の実行回数とデータサイズの関係を分析し、アルゴリズムの計算量(時間計算量)について理解する。 Schemeの組み込み関数appendの動作を確認し、複数のリストを連結する方法を理解する。 リストから条件を満たす要素のみを抽出するフィルタリング処理を実装する。ここではthreshold以上の要素を選択する。 例題5と対になる処理として、thresholdより小さい要素を選択する関数を実装する。 分割統治法(divide and conquer)に基づくクイックソートアルゴリズムを実装する。例題4-6で作成した関数を組み合わせて使用する。 数値リストではなく、構造体のリストに対してクイックソートを適用する方法を学ぶ。文字列比較関数を用いて名前順にソートする。 クイックソートの再帰的な実行過程を追跡し、分割統治法の動作原理を説明できるようになる。 例題8のプログラムを実行し、構造体リストのソート結果を確認する。 リストから特定の要素を検索する関数を作成する。さらに、ソート済みリストの性質を活用した効率的な検索を考える。 課題3-1:search関数 テスト例: 課題3-2:search-sorted関数(昇順リスト版) テスト例: 課題3-2:search-sorted関数(降順リスト版) テスト例:演習パート(クリックして展開)
環境設定
ステッパ機能について
ステッパを使用すると、再帰呼び出しがどのように展開され、どのように値が返されるかを視覚的に追跡できる。例題3でインサーションソートの繰り返し回数を数える際に活用する。
例題1:要素の挿入
目的
手順
;; insert: number list-of-numbers(sorted) -> list-of-numbers(sorted)
;; ソート済みリスト alon に数値 n を挿入し、ソート済みリストを返す
(define (insert n alon)
(cond
[(empty? alon) (cons n empty)]
[else (cond
[(<= (first alon) n) (cons n alon)]
[(> (first alon) n)
(cons (first alon)
(insert n (rest alon)))])]))
(insert 40 (list 80 21 10 7 5 4))
(list 80 40 21 10 7 5 4) となることを確認する。(insert 100 (list 80 21 10))
(insert 1 (list 80 21 10))
insert関数は3つの場合に分岐する。リストが空の場合、先頭要素がn以下の場合、先頭要素がnより大きい場合である。再帰呼び出しは3番目の場合にのみ発生する。
例題2:インサーションソート
目的
手順
;; sort: list-of-numbers -> list-of-numbers
;; リスト alon を降順にソートする
(define (sort alon)
(cond
[(empty? alon) empty]
[else (insert (first alon)
(sort (rest alon)))]))
;; insert: number list-of-numbers(sorted) -> list-of-numbers(sorted)
(define (insert n alon)
(cond
[(empty? alon) (cons n empty)]
[else (cond
[(<= (first alon) n) (cons n alon)]
[(> (first alon) n)
(cons (first alon)
(insert n (rest alon)))])]))
(sort (list 3 5 1 4))
(list 5 4 3 1) となることを確認する。(sort (list 10 2 8 4 6))
(sort (list 1))
(sort empty)
sort関数は「リストのrestをソートした結果に、first要素を挿入する」という再帰構造を持つ。空リストに到達したときが終了条件であり、そこから逆順に要素が挿入されていく。
例題3:インサーションソートでの繰り返し回数
目的
手順
(sort (list 80 30 50))
insert関数の呼び出し回数がデータの並び順に依存することが分かる。平均的な場合、計算量はO(n²)となる。
例題4:append
目的
手順
(append (list 1 2) (list 3 4))
(append (list 1 2) (list 3 4) (list 5 6))
(append (list 1 2) 3 (list 4 5))
appendはリスト同士の連結にのみ使用できる。単一の要素を追加する場合はconsを使用する。この関数は例題7のクイックソートで重要な役割を果たす。
例題5:大きな要素の選択
目的
手順
;; larger-items: list-of-numbers number -> list-of-numbers
;; alon から threshold 以上の数を選びリストを作る
(define (larger-items alon threshold)
(cond
[(empty? alon) empty]
[else (cond
[(>= (first alon) threshold)
(cons (first alon)
(larger-items (rest alon) threshold))]
[else (larger-items (rest alon) threshold)])]))
(larger-items (list 1 2 3 10 11 12 4 5 6) 6)
(list 10 11 12 6) となることを確認する。(larger-items (list 1 2 3 10 11 12 4 5 6) 10)
(larger-items (list 1 2 3 10 11 12 4 5 6) 1)
この関数は2つの分岐を持つ。条件を満たす要素は結果リストにconsで追加され、満たさない要素は無視される。どちらの場合も再帰呼び出しが行われる点に注意する。
例題6:小さな要素の選択
目的
手順
;; smaller-items: list-of-numbers number -> list-of-numbers
;; alon から threshold より小さな数を選びリストを作る
(define (smaller-items alon threshold)
(cond
[(empty? alon) empty]
[else (cond
[(< (first alon) threshold)
(cons (first alon)
(smaller-items (rest alon) threshold))]
[else (smaller-items (rest alon) threshold)])]))
(smaller-items (list 1 2 3 10 11 12 4 5 6) 6)
(list 1 2 3 4 5) となることを確認する。
larger-itemsとの違いは比較演算子のみである。>=が<に変わっている。この2つの関数を組み合わせることで、クイックソートにおけるピボットによる分割が実現される。
例題7:クイックソート
目的
手順
;; larger-items: list-of-numbers number -> list-of-numbers
(define (larger-items alon threshold)
(cond
[(empty? alon) empty]
[else (cond
[(>= (first alon) threshold)
(cons (first alon)
(larger-items (rest alon) threshold))]
[else (larger-items (rest alon) threshold)])]))
;; smaller-items: list-of-numbers number -> list-of-numbers
(define (smaller-items alon threshold)
(cond
[(empty? alon) empty]
[else (cond
[(< (first alon) threshold)
(cons (first alon)
(smaller-items (rest alon) threshold))]
[else (smaller-items (rest alon) threshold)])]))
;; quick-sort: list-of-numbers -> list-of-numbers
;; リストを昇順にソートする
(define (quick-sort alon)
(cond
[(empty? alon) empty]
[else (append
(quick-sort (smaller-items (rest alon) (first alon)))
(list (first alon))
(quick-sort (larger-items (rest alon) (first alon))))]))
(quick-sort (list 4 6 2))
(quick-sort (list 8 10 6 3 5))
(list 2 4 6)、(list 3 5 6 8 10) となることを確認する。
クイックソートの動作は以下の通りである。リストの先頭要素をピボット(pivot、基準値)として選択し、残りの要素をピボットより小さいグループと大きいグループに分割する。各グループを再帰的にソートし、最後にappendで「小さいグループ+ピボット+大きいグループ」の順に連結する。
例題8:クイックソート(構造体)
目的
手順
;; 構造体の定義
(define-struct address-record (name age address))
;; テストデータ
(define book
(list (make-address-record "Mike" 10 "Fukuoka")
(make-address-record "Bill" 20 "Saga")
(make-address-record "Ken" 30 "Nagasaki")))
;; larger-items: list-of-address-record string -> list-of-address-record
;; a-list から threshold 以上の名前を持つレコードを選びリストを作る
(define (larger-items a-list threshold)
(cond
[(empty? a-list) empty]
[else (cond
[(string>=? (address-record-name (first a-list)) threshold)
(cons (first a-list)
(larger-items (rest a-list) threshold))]
[else (larger-items (rest a-list) threshold)])]))
;; smaller-items: list-of-address-record string -> list-of-address-record
;; a-list から threshold より小さな名前を持つレコードを選びリストを作る
(define (smaller-items a-list threshold)
(cond
[(empty? a-list) empty]
[else (cond
[(string<? (address-record-name (first a-list)) threshold)
(cons (first a-list)
(smaller-items (rest a-list) threshold))]
[else (smaller-items (rest a-list) threshold)])]))
;; quick-sort: list-of-address-record -> list-of-address-record
;; 名前のアルファベット順にソートする
(define (quick-sort a-list)
(cond
[(empty? a-list) empty]
[else (append
(quick-sort (smaller-items (rest a-list)
(address-record-name (first a-list))))
(list (first a-list))
(quick-sort (larger-items (rest a-list)
(address-record-name (first a-list)))))]))
(quick-sort book)
数値の比較演算子(>=、<)が文字列比較関数(string>=?、string<?)に置き換わっている点に注目する。構造体のフィールドにアクセスするにはaddress-record-nameのようなアクセサ関数を使用する。
課題1:クイックソートの実行過程
目的
手順
(quick-sort (list 8 10 6 3 5)) の実行過程を分析する。
ピボットとして8が選ばれ、(list 10 6 3 5)が「8より小さい」と「8以上」に分割される。この過程を追跡することで、分割統治法の動作が理解できる。
課題2:住所録構造体のクイックソート
目的
手順
(quick-sort book) を実行する。
結果はaddress-record構造体のリストとして出力される。名前フィールドがアルファベット順(昇順)に並んでいることを確認する。
課題3:リスト検索プログラム
目的
課題3-1:基本的な検索
課題3-2:ソート済みリストの検索
課題3-1は例題5、6のフィルタリング関数と類似した構造で実装できる。課題3-2では、ソート順によって「これ以上検索しても見つからない」ことが判定できる場合がある。例えば昇順リストで先頭要素がnより大きければ、残りの要素はすべてnより大きいため検索を終了できる。
課題3 解答例
以下は解答例である。まずは自分で実装を試みてから参照すること。
;; search: number list-of-numbers -> boolean
;; 数 n がリスト a-list に存在するかどうかを判定する
;; 存在するときに限り true を返す
(define (search n a-list)
(cond
[(empty? a-list) false]
[(= (first a-list) n) true]
[else (search n (rest a-list))]))(search 5 (list 1 3 5 7 9)) ;; => true
(search 4 (list 1 3 5 7 9)) ;; => false
(search 1 (list 1 2 3)) ;; => true
(search 10 empty) ;; => false;; search-sorted: number list-of-numbers(sorted ascending) -> boolean
;; 昇順にソートされたリスト a-list から数 n を検索する
;; 存在するときに限り true を返す
;; ソート済みという性質を利用して効率的に検索を行う
(define (search-sorted n a-list)
(cond
[(empty? a-list) false]
[(= (first a-list) n) true]
[(> (first a-list) n) false] ;; 昇順なので、これ以降にnは存在しない
[else (search-sorted n (rest a-list))]))(search-sorted 5 (list 1 3 5 7 9)) ;; => true
(search-sorted 4 (list 1 3 5 7 9)) ;; => false(4より大きい5で打ち切り)
(search-sorted 10 (list 1 3 5 7 9)) ;; => false
(search-sorted 1 (list 1 3 5 7 9)) ;; => true;; search-sorted-desc: number list-of-numbers(sorted descending) -> boolean
;; 降順にソートされたリスト a-list から数 n を検索する
;; 存在するときに限り true を返す
(define (search-sorted-desc n a-list)
(cond
[(empty? a-list) false]
[(= (first a-list) n) true]
[(< (first a-list) n) false] ;; 降順なので、これ以降にnは存在しない
[else (search-sorted-desc n (rest a-list))]))(search-sorted-desc 5 (list 9 7 5 3 1)) ;; => true
(search-sorted-desc 6 (list 9 7 5 3 1)) ;; => false(6より小さい5で打ち切り)
(search-sorted-desc 0 (list 9 7 5 3 1)) ;; => false
(search-sorted-desc 9 (list 9 7 5 3 1)) ;; => true
search-sorted関数では、リストがソート済みであるという性質を利用している。昇順リストの場合、先頭要素が検索対象nより大きければ、残りの要素はすべてnより大きいため、検索を打ち切ってfalseを返すことができる。これにより、最悪の場合でもリスト全体を走査せずに済む場合がある。
sp-16. cons と種々のデータ構造
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/Z4JVLK-2022-01-26-145242
二分探索木は二分木の一種であり、データの配置に以下の規則がある。 この規則により、効率的なデータ探索が可能となる。演習パート(クリックして展開)
演習環境の準備
emptyが使用できないため、空リストは'()と記述する(list 15 8 6)は(15 8 6)と表示される例題1:ペア
(define x (cons 'apple 100))
(define y (cons 'orange 60))
(define z (cons 'banana 80))
x
(car x)
(cdr x)
(cons 'apple 100)は(apple . 100)と表示される(ドット対表記)例題2:リストの変数定義
(define A (cons 15 (cons 8 (cons 6 '()))))
A
(car A)
(cdr A)
(cdr A)の結果に対してさらにcar、cdrを適用し、リストの構造を探索する
'()は空リスト(empty list)を表す(car A)はリストの先頭要素(15)を返す(cdr A)はリストから先頭要素を除いた残り(8 6)を返す例題3:ペアから構成されたペア
(define a (cons (cons 1 2) (cons 3 4)))
a
(car a)
(cdr a)
(car a)は左側のペア(1 . 2)を返す(cdr a)は右側のペア(3 . 4)を返す((1 . 2) 3 . 4)と表示される例題4:carとcdrの組み合わせ
(define a (cons (cons 1 2) (cons 3 4)))
(car (car a))
(cdr (car a))
(car (cdr a))
(cdr (cdr a))
(caar a)
(cdar a)
(cadr a)
(cddr a)
(car (car a))と(caar a)は同じ結果を返すcaarは「carしてからcar」cdddr)例題5:consとlistの組み合わせ(1)
(define x (cons (list 1 2 3) (list 4 5 6)))
x
(car x)
(cdr x)
(car x)はリスト(1 2 3)を返す(cdr x)はリスト(4 5 6)を返す例題6:consとlistの組み合わせ(2)
(define x (list (cons 'x 'y) (cons 'a 'b) (cons 10 20)))
x
(car x)
(cadr x)
(caddr x)
例題7:listとlistの組み合わせ
(define x (list (list 1 2) (list 3 4)))
x
(car x)
(cadr x)
(car (car x))
(cadr (cadr x))
(car x)は最初のリスト(1 2)を返す例題8:二分探索木
(define-struct node (value left right))
(define tree
(make-node 35
(make-node 21
(make-node 13 false false)
false)
(make-node 46
(make-node 40 false false)
(make-node 61
false
(make-node 69 false false)))))
(node-value tree)
(node-left tree)
(node-right tree)
(node-value (node-left tree))
(define-struct node (value left right))により、make-node、node-value、node-left、node-rightが使用可能になるfalseは子が存在しないことを示す例題9:二分探索木による探索
(define (search x a-tree)
(cond
[(eq? a-tree false) false]
[(< x (node-value a-tree)) (search x (node-left a-tree))]
[(< (node-value a-tree) x) (search x (node-right a-tree))]
[else true]))
(search 40 tree)
(search 35 tree)
(search 100 tree)
(search 13 tree)
課題1
(list (cons 1 2) (cons 3 4))
(list (list 1 2) (list 3 4))
(car (list ...))
(cdr (list ...))
(cadr (list ...))
(cddr (list ...))
(list (cons 1 2) (cons 3 4))と(list (list 1 2) (list 3 4))の構造の違いに注目する報告用テンプレート
式
(car (list ...))
(cdr (list ...))
(cadr (list ...))
(cddr (list ...))
(list (cons 1 2) (cons 3 4))
(list (list 1 2) (list 3 4))
sp-17. フィボナッチ数
資料(スライド): [PDF], [パワーポイント], [HTML]
ドクセルの URL: https://www.docswell.com/s/6674398749/ZJ84EZ-2022-01-26-145136
生成的再帰(generative recursion)の一形態である木構造再帰プロセス(tree recursion)を用いて、フィボナッチ数を計算するプログラムの動作原理を理解する。 フィボナッチ数列の定義は f₀ = 0、f₁ = 1、fₙ = fₙ₋₁ + fₙ₋₂(n > 1)である。プログラム中の 反復的プロセス(iterative process)を用いてフィボナッチ数を計算する方法を学び、例題1の木構造再帰との違いを理解する。 反復的プロセスでは、変数a、bに「直前の2つのフィボナッチ数」を保持しながら、counterが0になるまで更新を繰り返す。この方式では同じ計算を重複して行うことがないため、例題1より効率的である。「a ← a + b、b ← a」という更新規則が核心部分である。 木構造再帰プロセスと反復的プロセスの計算効率の違いを、実際の実行回数を計測することで定量的に把握する。 stepperはプログラムの実行過程を1ステップずつ可視化する機能である。例題1では指数関数的に呼び出し回数が増加し、例題2では線形に増加する傾向が観察されるはずである。この違いがなぜ生じるかを、各プログラムの構造と関連付けて考えるとよい。「計算が冗長」という指摘と、自分の計測結果を照らし合わせること。 フィボナッチ数列は、0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... と続く数列である。各項は直前の2つの項の和として定義される。この数列は自然界の様々な現象(植物の葉の配置、貝殻の螺旋など)に現れることが知られている。 木構造再帰プロセスでは、関数が自身を複数回呼び出すため、呼び出しの構造が木のような形になる。一方、反復的プロセスでは、状態を変数に保持しながら順次更新するため、呼び出しは直線的になる。この構造の違いが計算効率の差を生む原因である。演習パート(クリックして展開)
例題1:フィボナッチ数(木構造再帰プロセス)
目的
手順
(define (fibo n)
(cond
[(= n 0) 0]
[(= n 1) 1]
[else (+ (fibo (- n 1)) (fibo (- n 2)))]))
(fibo 5)
(fibo 6)
(fibo 7)
(fibo 4)がどのように展開されて3という結果になるかを追跡するヒント
cond式は、この数学的定義をそのまま反映している。(fibo 4)の計算では(fibo 2)が2回、(fibo 1)が3回、(fibo 0)が2回呼び出される点に注目すると、木構造再帰の特徴である「同じ計算の重複」が理解しやすい。例題2:反復的プロセスでのフィボナッチ数
目的
手順
(define (fibo n)
(fibo-iterate 1 0 n))
(define (fibo-iterate a b counter)
(cond
[(= counter 0) b]
[else (fibo-iterate (+ a b) a (- counter 1))]))
(fibo 5)
(fibo 6)
(fibo 7)
(fibo 4)の計算過程でa、b、counterの値がどのように変化するかを追跡するヒント
課題1:性能比較
目的
手順
(fibo 6)を計算するためにfibo関数が何回呼び出されるかを数える
(fibo 6)の呼び出し構造を紙に描いて数えるとよい(fibo 6)を計算するためにfibo-iterate関数が何回呼び出されるかを数える
(fibo 3)、(fibo 4)、(fibo 5)、(fibo 6)を実行するヒント
補足説明
フィボナッチ数列について
木構造再帰と反復的プロセスの違い