Schemeで学ぶプログラミングの原理(授業資料)

本授業は、関数型プログラミング言語Schemeを用いて、プログラミングの基礎から応用までを体系的に学習する全17回の演習科目である。DrScheme(現Racket)を開発環境として使用し、各回で理論説明と実践演習を組み合わせた構成となっている。

カリキュラム構成

本授業は以下の4つのフェーズで構成される。

  1. 第1フェーズ:基礎(sp-1〜sp-4)

    授業全体の方針を示した後、式の記述、関数定義、条件分岐といったSchemeの基本文法を習得する。

  2. 第2フェーズ:データ構造とリスト処理(sp-5〜sp-7)

    リスト、シンボル、文字列といったデータ構造と、リストに対する繰り返し処理・生成方法を習得する。

  3. 第3フェーズ:設計と抽象化(sp-8〜sp-11)

    プログラム設計法、高階関数、構造体を学び、実践的なプログラム構築能力を養う。

  4. 第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を用いた演習で理解を定着させる。また、数値計算やソートアルゴリズムなど、計算機科学の古典的な題材を取り上げることで、プログラミングの基礎技術と計算機科学の基礎知識を同時に習得できる構成となっている。

教材の利用条件: クリエイティブコモンズ 表示-非営利-継承 4.0 国際ライセンス(CC BY-NC-SA 4.0)に基づき、著作者表示・非営利目的・同一ライセンスでの再配布を条件として自由に利用可能である。

sp-1. 全体計画と方針

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/ZR6Q25-2022-01-26-150009

演習パート(クリックして展開)

環境構築:DrRacketのインストール

Schemeプログラミングの演習では、DrRacket(ドクター・ラケット)という統合開発環境(IDE: Integrated Development Environment、プログラムの作成・実行・デバッグを一つのソフトウェアで行える環境)を使用する。DrRacketは元々DrSchemeという名称であり、Scheme言語の学習用に開発された。2010年にPLT SchemeがRacketに改名された際、DrSchemeもDrRacketに改名された。

ダウンロードとインストール

  1. 公式サイト https://racket-lang.org/download/ にアクセスする
  2. 自分のOS(オペレーティングシステム)に対応したインストーラをダウンロードする(ダウンロードページで「Platform」から選択する)
  3. ダウンロードしたファイルを実行し、画面の指示に従ってインストールする

OS別の起動方法

Windowsの場合、スタートメニューの「Racket」フォルダからDrRacketを起動する。スタートメニューで「DrRacket」と入力して検索することも可能である。

macOSの場合、Applicationsフォルダに配置したRacketフォルダ内のDrRacketアイコンをダブルクリックする。

Linux(Unix系OS)の場合、デスクトップ環境によってはDrRacketアイコンが作成される。コマンドラインからは、インストールディレクトリのbinサブディレクトリにあるdrracket実行ファイルを起動する。

Scheme言語の設定

DrRacketを起動した後、本講義で使用するScheme言語を設定する必要がある。

  1. DrRacketを起動する
  2. メニューから「Language」→「Choose Language...」を選択する
  3. 「How to Design Programs」のセクションから適切な言語レベルを選択する(講義の指示に従う)
  4. 「OK」をクリックし、「Run」ボタンを押して設定を反映する

動作確認

設定完了後、下部の対話エリア(Interactions)に以下を入力してEnterキーを押す。

(+ 1 2)

結果として「3」が表示されれば、環境構築は完了である。

推奨する参考資料

公式ドキュメントとして https://docs.racket-lang.org/getting-started/index.html(Getting Started)がある。初心者向けの導入として推奨される。

教科書として https://htdp.org/(How to Design Programs, Second Edition)がある。プログラム設計の体系的な方法論を学べる教科書であり、DrRacketとの連携を前提に書かれている。

日本語書籍としては、スライドに記載のとおり以下がある。

演習:パソコン演習の取り組み方を理解する

目的

Schemeプログラミングの演習に効果的に取り組むための学習サイクルを理解し、自律的な学習姿勢を身につける。

手順

  1. スライド3〜4を読み、本講義の目標を確認する。「プログラムの実行イメージを持つ」とは、プログラムの指示に従ってコンピュータがどのように振る舞うかを頭の中で描けることを指す。
  2. スライド5を読み、Schemeプログラミングで習得すべき6つのスキルを把握する。
    • 式、括弧の付け方、関数の書き方
    • 再帰(関数が自分自身を呼び出す手法)
    • リスト(複数のデータをまとめて扱うデータ構造)
    • 構造体(関連するデータを一つにまとめる仕組み)
    • プログラムの読解・作成能力
    • エラーの無いプログラムの作成能力
  3. スライド8を読み、学習の流れを理解する。
    • 前半:説明資料で基本事項を理解する
    • 中盤:パソコン演習で例題を実行し、理解を深める
    • 後半:課題を解きながらプログラムに慣れる
  4. スライド11を読み、演習への取り組み方5ステップを確認する。
    • 手本となるプログラムを良く読み、理解する
    • 手本をまねて、自分でプログラムを作ってみる
    • 動かしてみる
    • 動いたら、自分の納得のいくまで手を加える
    • 動かないようであれば、解決の糸口を自分で探してみる
  5. スライド12を読み、課題への取り組み方を確認する。自分の実力にあった問題を選択し、確実に理解しながら解くことが重要である。
  6. 上記「環境構築」の手順に従い、DrRacketをインストールして動作確認を行う。
  7. 本ガイドの「推奨する参考資料」に記載したURLにアクセスし、どのような情報が得られるか確認する。

ヒント

この段階で重要なのは、学習の全体像を把握することである。個々のスキル(再帰、リストなど)の詳細は後続の資料で学ぶため、ここでは用語の名前と概要を知っておく程度でよい。

スライド11の5ステップは、プログラミング学習における普遍的なアプローチである。特にステップ5「失敗を重ねながら、理解を深める」は、エラーを恐れずに試行錯誤することの重要性を示している。

DrRacketの言語設定で迷った場合は、「Beginning Student」から始めることを推奨する。この設定では、初学者が陥りやすいエラーに対して分かりやすいメッセージが表示される。

参照

自己確認チェックリスト

本資料の学習を終えた後、以下の項目を自己確認する。

sp-2. Scheme の式とプログラム

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/KW4X15-2022-01-26-145940

演習パート(クリックして展開)

現行環境(Racket / DrRacket)についての注記

スライドで使用されている「DrScheme」は、2010年に「DrRacket」へと名称変更された。PLT Scheme プロジェクトが Racket へとリブランディングされたことに伴う変更である。現在の最新バージョンは Racket 9.0(2025年11月リリース)であり、公式サイト https://racket-lang.org/ から無償でダウンロードできる。

主な差異と対応

スライドの記載 現行環境での対応
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

互換性

スライドに記載されている Scheme のプログラムおよび式は、DrRacket の Intermediate Student 言語設定においてそのまま動作する。define による関数定義、四則演算、sqrt、expt、remainder、quotient などの組み込み関数はすべて同様に使用できる。

演習を始める前の準備

DrRacket を起動し、言語設定を「Intermediate Student」に変更する。設定手順は以下のとおりである。

  1. DrRacket を起動する(Racket をインストール後、DrRacket アプリケーションを開く)
  2. メニューから Language → Choose Language を選択する
  3. 「Teaching Languages」を展開し、「How to Design Programs」内の「Intermediate Student」を選択する
  4. 「OK」をクリックし、Run ボタンを押す

DrRacket には2つのウインドウがある。定義用ウインドウは関数を記述する領域であり、実行用ウインドウは式を入力して実行結果を確認する領域である。

例題1:簡単な数式

目的

入れ子になった括弧を含むScheme式の書き方と実行方法を理解する。

手順

  1. DrRacket の実行用ウインドウに以下の式を入力する
    (* (+ 2 2) (/ (* (+ 3 5) (/ 30 10)) 2))
  2. Enter キーを押して実行する
  3. 実行結果が 48 と表示されることを確認する

ヒント

Scheme では演算子(+, -, *, /)を括弧の先頭に記述し、その後に被演算子を並べる。これを前置記法(prefix notation)と呼ぶ。数学の記法 (2+2)*... とは異なり、(* (+ 2 2) ...) のように記述する点に注意する。

よくある間違い
スペース(空白文字)の有無が重要である。(+2 2)(*(+ 2 2) ...) のように演算子と数値の間にスペースがない場合はエラーとなる。

例題2:円の面積

目的

define を用いた関数定義の方法と、定義した関数の呼び出し方を習得する。

手順

  1. 定義用ウインドウに以下のプログラムを入力する
    (define (area-of-disk r) (* 3.14 (* r r)))
  2. Run ボタンを押してプログラムをコンピュータに読み込ませる
  3. 実行用ウインドウに (area-of-disk 5) と入力し、Enter キーを押す
  4. 実行結果が 78.5 と表示されることを確認する
  5. 続けて (area-of-disk 10) を実行し、結果が 314 となることを確認する

ヒント

関数定義の構文は (define (関数名 パラメータ) 式) である。パラメータ(parameter)とは、関数が受け取る値に付ける名前である。この例では r がパラメータであり、関数呼び出し時に具体的な値(5や10)が渡される。

Run ボタンを押すと実行用ウインドウの内容はクリアされる。これは正常な動作である。

例題3:簡単なプログラム

目的

複数の関数を定義し、expt, max, remainder, quotient などの組み込み関数を活用する方法を学ぶ。

手順

  1. 定義用ウインドウに以下の4つの関数を入力する
    (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))
  2. Run ボタンを押す
  3. 実行用ウインドウで以下の式を順に実行し、結果を確認する
    • (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:ドルから円への変換

目的

実用的な単位変換を行う関数を自ら設計・実装する。

手順

  1. ドル d を入力として円を返す関数 d2y を設計する
  2. 1ドル = 120.53円 という換算レートを用いる
  3. 定義用ウインドウに関数を記述し、Run ボタンを押す
  4. 実行用ウインドウで任意のドル額を引数として関数を呼び出す
  5. 実行結果を報告する

ヒント

スライド65に「解答の例」として別の関数 foo が示されている。この構造を参考にするとよい。

よくある間違いを避けるためのチェックリスト
  • 全体を括弧で囲んでいるか(define ... ではなく (define ...)
  • 関数名の後にパラメータを記述しているか((d2y d) のように)
  • パラメータ名と式の中の変数名が一致しているか
  • 関数名にスペースを含めていないか(「d 2 y」ではなく「d2y」)

課題2:摂氏から華氏への変換

目的

与えられた数学的公式をSchemeの関数として実装する。

手順

  1. 変換式 c = 5 × (f - 32) / 9 を、摂氏 c から華氏 f を求める形に変形する
  2. 摂氏 c を入力として華氏を返す関数 c2f を定義する
  3. 定義用ウインドウに関数を記述し、Run ボタンを押す
  4. 実行用ウインドウで任意の摂氏温度を引数として関数を呼び出す
  5. 実行結果を報告する

ヒント

与えられた式は摂氏を求める式である。華氏を求めるには式を変形する必要がある。c = 5 × (f - 32) / 9 を f について解くと f = c × 9 / 5 + 32 となる。

課題3:元利の計算

目的

複利計算の公式を関数として実装し、長期間の運用結果をシミュレーションする。

手順

  1. 元利計算式「元利 = 元金 × (1 + 年利)年数」を確認する
  2. 元金、年利、年数の3つを入力として元利を返す関数 interest を定義する
  3. 定義用ウインドウに関数を記述し、Run ボタンを押す
  4. 元金1000円、年利2%(0.02)、年数50年で関数を呼び出す
  5. 実行結果を報告する

ヒント

べき乗の計算には expt を使用する。年利2%は小数で 0.02 と表現する。関数は3つのパラメータを受け取る必要がある(例:元金、年利、年数)。

課題4:Scheme 式

目的

様々な演算子を用いたScheme式の記述方法を習得する。

手順

  1. 以下の11種類の計算について、それぞれScheme式を記述する
  2. 実行用ウインドウで各式を実行する
  3. 実行結果を報告する
計算内容 参考となる演算子
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

ヒント

各演算子の使用例はスライド8-9, 12, 15に記載されている。負の数は数値の前にマイナス記号を付けて表現する(例:-5)。三角関数の引数はラジアン単位である。

sp-3. 関数の組み合わせ

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/5D2JD5-2022-01-26-145918

演習パート(クリックして展開)

演習環境の準備

DrSchemeの起動と設定

  1. 「プログラム」→「PLT Scheme」→「DrScheme」を選択し、DrSchemeを起動する
  2. 「Language」→「Choose Language」→「Intermediate Student」を選択する
  3. 「Execute」ボタンを押して設定を反映する

この設定により、ステップ実行機能(stepper)が使用可能になる。

例題1:実行結果に至る過程

目的

複合的な算術式がどのような順序で評価され、最終結果に至るかを理解する。括弧の内側が優先的に計算される規則を確認する。

手順

  1. DrSchemeの「定義用ウインドウ」(Definition Window:プログラムを記述する上部のウインドウ)に以下の式を入力する
    (* (+ 2 2) (/ (* (+ 3 5) (/ 30 10)) 2))
  2. 「Execute」ボタンを押す
  3. 「Step」ボタンを押してステップ実行を開始する
  4. 「Next」ボタンを押しながら、各ステップで何が計算されるかを観察する
  5. 最終結果「48」が得られることを確認する

ヒント

各ステップで「どの部分式が計算されたか」に注目する。計算順序は以下のようになる。

  • (+ 2 2) → 4
  • (+ 3 5) → 8
  • (/ 30 10) → 3
  • (* 8 3) → 24
  • (/ 24 2) → 12
  • (* 4 12) → 48

括弧の最も内側から順に評価されることを確認する。

例題2:関数のステップ実行

目的

関数呼び出し時に、関数本体がパラメータの実際の値で置き換わる過程を理解する。

手順

  1. 「定義用ウインドウ」に以下のプログラムを入力する
    (define (area-of-disk r)
      (* 3.14 (* r r)))
    
    (area-of-disk 5)
  2. 「Execute」ボタンを押す
  3. 「Step」ボタンを押してステップ実行を開始する
  4. 「Next」ボタンで各ステップを確認する

ヒント

(area-of-disk 5)(* 3.14 (* 5 5)) に置き換わる様子を観察する。このとき、関数定義内のパラメータ r がすべて引数 5 で置き換えられる。DrSchemeでは 3.14157/50 のように有理数(分数)で表示される場合があるが、計算結果は同じである。

例題3:2乗の和

目的

複数の関数を組み合わせてプログラムを構成する方法を学ぶ。一つの関数(sum-of-squares)が別の関数(sqr)を呼び出す構造を理解する。

手順

  1. 「定義用ウインドウ」に以下のプログラムを入力する
    (define (sqr x)
      (* x x))
    
    (define (sum-of-squares x y)
      (+ (sqr x) (sqr y)))
  2. 「Execute」ボタンを押す
  3. 「実行用ウインドウ」(Interaction Window:下部のウインドウ)で以下を入力し、Enterキーを押す
    (sum-of-squares 2 4)
  4. 結果「20」が表示されることを確認する
  5. 同様に (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)

目的

複数の関数が連携する場合の実行過程を追跡し、データの流れを理解する。

手順

  1. 例題3で入力したプログラムの末尾に以下を追加する
    (sum-of-squares 20 30)
  2. 「Execute」ボタンを押す
  3. 「Step」ボタンを押してステップ実行を開始する
  4. 各ステップで関数呼び出しがどのように展開されるか観察する

ヒント

実行過程は以下のようになる。

  • (sum-of-squares 20 30)(+ (sqr 20) (sqr 30))
  • (sqr 20)(* 20 20) → 400
  • (sqr 30)(* 30 30) → 900
  • (+ 400 900) → 1300

sqr 関数が呼び出されるたびに、その本体が展開される様子を確認する。

例題5:リングの面積

目的

実際の問題(リング状の図形の面積計算)を関数の組み合わせでモデル化する方法を学ぶ。

手順

  1. 「定義用ウインドウ」に以下のプログラムを入力する
    (define (area-of-disk r)
      (* 3.14 (* r r)))
    
    (define (area-of-ring outer inner)
      (- (area-of-disk outer) (area-of-disk inner)))
  2. 「Execute」ボタンを押す
  3. 「実行用ウインドウ」で以下を実行する
    (area-of-ring 5 3)
  4. 結果「50.24」が表示されることを確認する

ヒント

リングの面積は「外側の円の面積 − 内側の円の面積」として計算される。area-of-ring 関数は2つのパラメータ(outer:外径、inner:内径)を受け取り、area-of-disk 関数を2回呼び出して差を計算する。関数を分割することで、計算の意味が「外側 − 内側」であることが明確になる。

例題6:ステップ実行(area-of-ring)

目的

area-of-ring 関数の実行過程を追跡し、関数間のデータの流れを詳細に理解する。

手順

  1. 例題5で入力したプログラムの末尾に以下を追加する
    (area-of-ring 5 3)
  2. 「Execute」ボタンを押す
  3. 「Step」ボタンを押してステップ実行を開始する
  4. 各ステップを「Next」ボタンで確認する

ヒント

実行過程は以下のようになる。

  • (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:利益の計算

目的

複数の関数が相互に連携する実践的なプログラムを作成する。ビジネスの利益計算モデルを関数で表現する方法を学ぶ。

背景知識

このプログラムは劇場の利益を計算するモデルである。

手順

  1. 「定義用ウインドウ」に以下のプログラムを入力する
    (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))))
  2. 「Execute」ボタンを押す
  3. 「実行用ウインドウ」で以下を順に実行し、各結果を確認する
    (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 関数は収入から支出を減算する

関数間の依存関係として、profitrevenuecost を呼び出し、これらはさらに attendees を呼び出す構造になっている。

例題8:ステップ実行(profit)

目的

4つの関数が連携するプログラムの実行過程を追跡し、複雑な関数呼び出しの流れを理解する。

手順

  1. 例題7で入力したプログラムの末尾に以下を追加する
    (profit 3)
  2. 「Execute」ボタンを押す
  3. 「Step」ボタンを押してステップ実行を開始する
  4. 各ステップで関数がどのように展開されるか観察する

プログラム全体

例題7のプログラムに (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)

ヒント

実行過程は多くのステップを含むが、基本的な流れは以下のとおりである。

  • profitrevenuecost を呼び出す
  • revenueattendees を呼び出す
  • attendees の計算が完了すると revenue の計算が完了する
  • costattendees を呼び出す
  • 最終的に profit の減算が実行される

同じ attendees 関数が複数回呼び出される点に注目する。

課題1

目的

例題7で作成したプログラムを使い、異なるチケット代での利益を計算する。

手順

  1. 例題7のプログラムが「定義用ウインドウ」に入力されていることを確認する
  2. 「Execute」ボタンを押す
  3. 「実行用ウインドウ」で以下を順に実行する
    (profit 3)
    (profit 4)
    (profit 5)
  4. 各チケット代での利益を記録し、報告する

ヒント

チケット代を変えると観客数が変化し、それに伴い収入と支出も変化する。チケット代と利益の関係を考察すると、最適なチケット代を見つける手がかりになる。

課題2

目的

プログラムの一部を変更し、条件が変わった場合の結果を確認する。固定費がかからない場合の利益を計算する。

手順

  1. 例題7のプログラムにおいて、cost 関数を以下のように変更する
    (define (cost ticket-price)
      (+ 0 (* .04 (attendees ticket-price))))
    または
    (define (cost ticket-price)
      (* .04 (attendees ticket-price)))
  2. 「Execute」ボタンを押す
  3. 「実行用ウインドウ」で以下を順に実行する
    (profit 3)
    (profit 4)
    (profit 5)
  4. 各チケット代での利益を記録し、課題1の結果と比較して報告する

ヒント

cost 関数内の 180 が固定費を表している。この値を 0 に変更することで、固定費がかからない状況をシミュレートできる。課題1の結果と比較し、固定費が利益に与える影響を考察する。

sp-4. 条件式

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/ZX4EMZ-2022-01-26-145852

演習パート(クリックして展開)

例題1:条件式

目的

cond文(条件分岐を行う構文)を使って、入力値に応じて異なる結果を返す関数を作成する方法を学ぶ。

手順

  1. DrSchemeを起動し、Language → Choose Language → Intermediate Studentを選択してExecuteボタンを押す
  2. 定義用ウインドウに以下のコードを入力する
    ;; 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]))
  3. Executeボタンを押してプログラムを読み込ませる
  4. 実行用ウインドウで (interest-rate 1000) を入力し、結果を確認する
  5. 同様に (interest-rate 1500)(interest-rate 6000) を試し、条件によって結果が変わることを確認する

ヒント

  • cond文の条件は上から順に評価される。最初に成立した条件の値が返され、それ以降の条件は評価されない
  • 「≦」や「≧」の記号は使用できない。代わりに <=>= を使う
  • 字下げ(インデント)を適切に行うと、プログラムが読みやすくなる

例題2:ステップ実行(interest-rate)

目的

DrSchemeのstepper機能を使い、条件式がどのように評価されるかを段階的に理解する。

手順

  1. 例題1のコードの末尾に (interest-rate 1500) を追加する
  2. Executeボタンを押す
  3. Stepボタンをクリックしてステップ実行モードに入る
  4. Nextボタンを繰り返し押し、以下の評価過程を観察する
    • amountに1500が代入される
    • (<= 1500 1000)false と評価される
    • 最初の条件節が消える
    • (<= 1500 5000)true と評価される
    • 結果として 0.045 が得られる

ヒント

  • 各ステップで何が起きているかを声に出して説明すると理解が深まる
  • 条件が false になると、その節全体が消えることに注目する

例題3:月の日数

目的

論理演算or(「または」を表す演算)とelse句を組み合わせた条件式を作成する方法を学ぶ。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (define (easy-get-num-of-days month)
      (cond
        [(= month 2) 28]
        [(or (= month 4) (= month 6) (= month 9) (= month 11)) 30]
        [else 31]))
  2. Executeボタンを押す
  3. 実行用ウインドウで以下を順に実行し、結果を確認する
    • (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)

目的

or演算を含む条件式の評価過程を段階的に追跡し、論理演算の動作を理解する。

手順

  1. 例題3のコードの末尾に (easy-get-num-of-days 1) を追加する
  2. Executeボタンを押し、Stepボタンでステップ実行モードに入る
  3. Nextボタンを押しながら、以下を観察する
    • (= 1 2)false となり最初の節が消える
    • (or (= 1 4) (= 1 6) (= 1 9) (= 1 11)) の各部分が順に評価される
    • すべて false なので or全体が false となる
    • else節が選択され、31が返される

ヒント

  • or演算では、各条件が順に評価され、すべて false の場合にor全体が false となる
  • 逆に、1つでも true があれば、その時点でor全体が true となる

例題5:うるう年の判定

目的

複雑な条件(and, or, notの組み合わせ)を用いた判定関数を作成する方法を学ぶ。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (define (is-leap? year)
      (cond
        [(or (= (remainder year 400) 0)
             (and (not (= (remainder year 100) 0))
                  (= (remainder year 4) 0))) true]
        [else false]))
  2. Executeボタンを押す
  3. 実行用ウインドウで以下を実行し、結果を確認する
    • (is-leap? 2004) → true(4の倍数)
    • (is-leap? 2005) → false(4の倍数でない)
    • (is-leap? 1900) → false(100の倍数だが400の倍数でない)
    • (is-leap? 2000) → true(400の倍数)

ヒント

  • remainder(剰余演算)は割り算の余りを求める。(remainder 2004 4) は0を返す
  • うるう年の判定ルール:400の倍数、または「100の倍数でなく、かつ4の倍数」
  • 1900年と2000年の結果の違いに注目すると、条件の意味が理解しやすい

例題6:ステップ実行(is-leap?)

目的

and, or, notが組み合わさった複雑な条件式の評価順序を理解する。

手順

  1. 例題5のコードの末尾に (is-leap? 2004) を追加する
  2. Executeボタンを押し、Stepボタンでステップ実行モードに入る
  3. Nextボタンを押しながら、以下を観察する
    • (remainder 2004 400) → 4、(= 4 0) → false
    • or の最初の条件が false なので、次の条件(and部分)の評価に進む
    • (remainder 2004 100) → 4、(= 4 0) → false、(not false) → true
    • (remainder 2004 4) → 0、(= 0 0) → true
    • and全体が true、or全体が true となり、結果は true

ヒント

  • 複雑な式でも、内側から外側へ順に評価される
  • 各演算子の優先順位ではなく、括弧の構造に従って評価が進む

例題7:曜日を求める

目的

quotient(整数除算)とremainderを組み合わせたツエラーの公式を実装し、実用的な関数を作成する。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (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)]))
  2. Executeボタンを押す
  3. 実行用ウインドウで任意の日付を入力し、曜日を確認する
    • 結果の数値の意味:0=日曜、1=月曜、2=火曜、3=水曜、4=木曜、5=金曜、6=土曜

ヒント

  • ツエラーの公式では、1月と2月は前年の13月・14月として扱う。そのため関数zellarで場合分けしている
  • quotientは整数除算の商を返す。(quotient 7 3) は2を返す
  • 自分の誕生日や今日の日付で試してみると、結果を検証しやすい

課題1:重量に応じた料金計算

目的

cond文を使った条件分岐を自力で設計し、段階的な料金表を関数として実装する。

手順

  1. 以下の料金表を満たす関数を設計する
    • 50グラム以下 → 120
    • 75グラム以下 → 140
    • 100グラム以下 → 160
    • 150グラム以下 → 200
  2. 関数名を1単語で決める(例:decide_price)
  3. cond文を使って条件を上から順に記述する
  4. 複数の重量でテストし、正しい結果が得られることを確認する

ヒント

  • 関数名はスペースを含めない。decide price ではなく decide_price のように書く
  • は使えない。<=>= を使う
  • cond文の条件は上から順に判定されるため、条件の順序が重要である

課題2:うるう年を考慮した月の日数

目的

既存の関数(easy-get-num-of-daysとis-leap?)を組み合わせて、より汎用的な関数を作成する。

手順

  1. 例題3のeasy-get-num-of-daysと例題5のis-leap?を定義用ウインドウに記述する
  2. 年yと月mを引数に取る関数get-num-of-daysを新たに作成する
  3. 2月かつうるう年の場合は29を返し、それ以外はeasy-get-num-of-daysの結果を返すようにする
  4. 以下のケースでテストする
    • (get-num-of-days 2004 2) → 29(うるう年の2月)
    • (get-num-of-days 2005 2) → 28(平年の2月)
    • (get-num-of-days 2004 4) → 30(4月)

ヒント

  • 既存の関数を呼び出すには、その関数が定義されている必要がある
  • 条件として「2月である」かつ「うるう年である」を判定する

課題3:数式の計算

目的

quotientとremainderを組み合わせた数式を関数として実装する。

手順

  1. 以下の数式を計算する関数foo2を作成する
    • [(20x + 8) / 7] mod 10
    • [...] は小数点以下切り捨て(quotientで実現)
    • mod は剰余(remainderで実現)
  2. defineを使って関数を定義する
  3. いくつかの値でテストし、手計算の結果と一致することを確認する

ヒント

  • 式の構造を分解して考える:まず (20x + 8) / 7 の整数部分を求め、次にその結果を10で割った余りを求める
  • quotientとremainderの使い方はスライド69〜70を参照する

sp-5. リスト,シンボル,文字列

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/ZPW2EK-2022-01-26-145815

演習パート(クリックして展開)

環境設定

演習を始める前に、DrSchemeを起動し、言語設定を「Intermediate Student」に変更する。

  1. DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
  2. Language → Choose Language → Intermediate Student を選択する
  3. Executeボタンを押す

例題1:リストの式

目的

Schemeにおけるリストの基本的な記法を理解し、listキーワードを使ってリストを作成できるようになる。

手順

  1. スライド4を確認し、リストが「データの並び」であり「順序がある」ことを理解する
  2. スライド6を確認し、listキーワードの記法を確認する
  3. 実行用ウインドウに以下の式を入力し、Enterキーを押して実行する
    (list 15 8 6 32 23)
  4. 実行結果が (list 15 8 6 32 23) と表示されることを確認する

ヒント

listを使った式は、それ自体が1つの式として評価される。入力した式がそのまま表示されるのは、リストが正しく作成されたことを意味する。

例題2:リストのfirstとrest

目的

リストの先頭要素を取得するfirstと、先頭を除いた残りを取得するrestの動作を理解する。

手順

  1. スライド7を確認し、firstとrestの意味を理解する
  2. 実行用ウインドウで以下の式を順に実行する
    (first (list 15 8 6 32 23))
    (rest (list 15 8 6 32 23))
  3. firstの結果が 15(先頭要素)であることを確認する
  4. restの結果が (list 8 6 32 23)(先頭を除いた残り)であることを確認する

ヒント

restの結果もリストである。この性質により、restを繰り返し適用してリストの任意の位置にアクセスできる。スライド7の「rest の rest の rest の first は:32」という例を自分で実行して確認するとよい。

例題3:リストのfirstとrest(要素が1つの場合)

目的

要素が1つしかないリストに対するfirst, restの動作と、空リストemptyの概念を理解する。

手順

  1. スライド8を確認し、emptyの意味を理解する
  2. 実行用ウインドウで以下の式を順に実行する
    (first (list 15))
    (rest (list 15))
  3. firstの結果が 15 であることを確認する
  4. restの結果が empty であることを確認する

ヒント

空リストemptyに対してfirstやrestを実行すると実行エラーになる(スライド9参照)。リストを処理する関数を作成する際は、空リストの場合の処理を考慮する必要がある。

例題4:append

目的

複数のリストを連結するappend関数の使い方を理解し、リストでない値を渡した場合のエラーを確認する。

手順

  1. スライド17を確認し、appendの意味を理解する
  2. 実行用ウインドウで以下の式を順に実行する
    (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))
  3. 最初の2つは正常に連結されることを確認する
  4. 3つ目はエラーになることを確認し、その理由を考える

ヒント

appendはリスト同士を連結する関数である。引数としてリスト以外の値(例:数値3)を渡すと実行エラーになる。

例題5:リストの基本操作

目的

firstとrestを組み合わせてリストの特定位置の要素を取得する関数を定義できるようになる。

手順

  1. スライド47を確認し、「リストの3番目 = リストのrestのrestのfirst」という関係を理解する
  2. 定義用ウインドウに以下のプログラムを入力する
    (define (element3 a-list)
      (first (rest (rest a-list))))
  3. Executeボタンを押してプログラムを読み込ませる
  4. 実行用ウインドウで以下の式を実行する
    (element3 (list 1 2 3 4))
    (element3 (list 15 8 6 32 23))
  5. 結果がそれぞれ 36 であることを確認する
  6. 追加で (element3 (list 1 2)) を実行し、エラーになることを確認する

ヒント

スライド52〜54に示されているように、関数呼び出しは引数の値が関数本体の変数に代入されて評価される。element3関数は要素数が2以下のリストに対してはエラーになる。その理由をスライド57の評価過程を参考に考えるとよい。

例題6:シンボル

目的

シンボル(symbol)の概念を理解し、cond文を使って条件に応じたシンボルを返す関数を作成できるようになる。

手順

  1. スライド19を確認し、シンボルの記法('を先頭につける)を理解する
  2. 定義用ウインドウに以下のプログラムを入力する
    ;;judge: number -> symbol
    (define (judge x)
      (cond
        [(<= x 20) 'Cold]
        [(and (< 20 x) (<= x 30)) 'Warm]
        [(< 30 x) 'Hot]))
  3. Executeボタンを押す
  4. 実行用ウインドウで以下の式を実行する
    (judge 15)
    (judge 20)
    (judge 25)
  5. 結果がそれぞれ 'Cold'Cold'Warm であることを確認する

ヒント

cond文は上から順に条件を評価し、最初にtrueとなった条件に対応する値を返す。<= x 20 は「xが20以下」を意味し、境界値(この例では20)がどちらの条件に含まれるかを確認することが重要である。

例題7:数字かシンボルを出力

目的

条件に応じて異なる型(数値またはシンボル)の値を返す関数を作成できるようになる。cond文のelse節の使い方を理解する。

手順

  1. 定義用ウインドウに以下のプログラムを入力する
    (define (ast x)
      (cond
        [(> x 0) x]
        [else '*]))
  2. Executeボタンを押す
  3. 実行用ウインドウで以下の式を実行する
    (ast 10)
    (ast 0)
    (ast -10)
  4. 結果がそれぞれ 10'*'* であることを確認する

ヒント

elseはcond文において「それ以外のすべての場合」を表す。この関数では、xが正の場合はx自身を、0以下の場合はシンボル'*を返す。Schemeでは関数の戻り値の型が固定されている必要はない。

例題8:シンボル(シンボルの比較)

目的

symbol=?を使ったシンボルの比較方法を理解し、シンボルを入力として受け取りシンボルを返す関数を作成できるようになる。

手順

  1. スライド20を確認し、symbol=?の使い方を理解する
  2. 定義用ウインドウに以下のプログラムを入力する
    ;;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]))
  3. Executeボタンを押す
  4. 実行用ウインドウで以下の式を実行する
    (reply 'GoodMorning)
    (reply 'Hello)
  5. 最初の結果が 'Hi であることを確認する
  6. 2番目がエラーになることを確認し、その理由を考える

ヒント

スライド21、77に示されているように、シンボルの比較に=を使うのはよくある間違いである。数値の比較には=を、シンボルの比較にはsymbol=?を使用する。また、cond文のどの条件にも該当しない入力があった場合、エラーが発生する(スライド78参照)。

課題1:リスト操作の実行結果

目的

list, first, restの動作を様々なケースで確認し、実行結果を正確に予測できるようになる。

手順

  1. 以下の表の各セルについて、実行結果を予測する
  2. 実行用ウインドウで実際に実行し、予測と比較する
  3. エラーが発生する場合は「エラー」と記録する
(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:真夏日・冬日判定関数

目的

複数の引数を受け取り、複数の条件を判定して適切な文字列を返す関数を設計・実装できるようになる。

手順

  1. 以下の判定基準を確認する
    • "Tropical Day"(真夏日):1日の最高気温が30度以上
    • "Summer Day"(夏日):1日の最高気温が25度以上
    • "Frost Day"(冬日):1日の最低気温が0度未満
    • "Ice Day"(真冬日):1日の最高気温が0度未満
  2. 関数summer-winter-dayの仕様を考える
    • 入力:high(最高気温)、low(最低気温)
    • 出力:上記のいずれかの文字列
  3. 条件の優先順位を検討する(複数の条件に該当する場合どれを返すか)
  4. 定義用ウインドウでプログラムを作成する
  5. 様々な入力値で実行し、正しく判定されることを確認する

ヒント

例題6のjudge関数を参考にする。条件の判定順序が重要である点に注意すること。例えば、最高気温が35度の場合、"Summer Day"と"Tropical Day"の両方の条件を満たすが、cond文では上から順に判定されるため、条件の記述順序によって結果が変わる。また、同時に真夏日と冬日の条件を満たす場合など、境界的なケースについてどう扱うか考える必要がある。

課題3:日付存在判定関数

目的

複雑な条件分岐(月ごとの日数、閏年判定など)を含む関数を設計・実装できるようになる。

手順

  1. 日付の有効性判定に必要な条件を整理する
    • 月ごとの日数の違い(28日、30日、31日)
    • 閏年の判定(2月の日数が変わる)
    • 日が1以上であること
  2. 関数の仕様を考える
    • 入力:y(年)、m(月)、d(日)
    • 出力:日付が存在すればdを、存在しなければシンボル'*を返す
  3. 例題7のast関数の構造を参考に、条件分岐を設計する
  4. 定義用ウインドウでプログラムを作成する
  5. 以下のテストケースで動作を確認する
    • (関数名 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を以下の手順で設定する。

  1. DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
  2. 言語設定を「Intermediate Student」に変更する
    • Language → Choose Language → Intermediate Student を選択
    • Execute ボタンを押して設定を反映する

例題1:リストの総和

目的

再帰を用いてリストの全要素の総和を求める関数を作成し、再帰関数の基本構造を理解する。

手順

  1. 定義用ウインドウに以下のコードを入力する
(define (list-sum a-list)
  (cond
    [(empty? a-list) 0]
    [else (+ (first a-list) (list-sum (rest a-list)))]))
  1. Execute ボタンを押してプログラムを読み込む
  2. 実行用ウインドウで以下を実行し、結果を確認する
(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. 定義用ウインドウに例題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))
  1. Step ボタンを押してステップ実行を開始する
  2. Next ボタンを押しながら、各ステップで式がどのように変換されるかを観察する
  3. 以下の展開過程を実際の画面で確認する
    • (list-sum (list 1 2 3)) から (+ 1 (list-sum (list 2 3))) への展開
    • 最終的に (+ 1 (+ 2 (+ 3 0))) となり 6 が得られる過程

ヒント

ステップ実行では、関数呼び出しがその定義で置き換えられ、条件式が評価され、最終的に基本演算だけの式になる。この過程を理解することで、再帰の動作原理が明確になる。

例題3:平均点

目的

既存の関数を組み合わせて新しい関数を定義する方法を学ぶ。総和を求める関数と要素数を求める関数を利用して平均を計算する。

手順

  1. 定義用ウインドウに以下のコードを入力する
;; 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)))
  1. Execute ボタンを押す
  2. 実行用ウインドウで以下を実行する
(average (list 40 90 80))
(average (list 100 200 300 400 500))

ヒント

average関数自体は再帰を使用していない。list-sum(再帰関数)とlength(組み込み関数)の結果を除算しているだけである。このように、複雑な処理を単純な関数の組み合わせで実現できる点が関数型プログラミングの特徴である。

例題4:ステップ実行(average)

目的

複数の関数が組み合わされた場合の計算過程を追跡し、関数呼び出しの評価順序を理解する。

手順

  1. 定義用ウインドウに例題3のコードと実行文を入力する
;; (例題3のコードに加えて)
(average (list 40 90 80))
  1. Step ボタンとNext ボタンを使ってステップ実行する
  2. 以下の過程を確認する
    • (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)を返す再帰関数の書き方を学ぶ。

手順

  1. 定義用ウインドウに以下のコードを入力する
;; 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))])]))
  1. Execute ボタンを押す
  2. 実行用ウインドウで以下を実行する
(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が見つかった時点で探索を打ち切る)の仕組みを理解する。

手順

  1. 定義用ウインドウに例題5のコードと実行文を入力する
;; (例題5のコードに加えて)
(contains-5? (list 3 5 7 9))
  1. Step ボタンとNext ボタンを使ってステップ実行する
  2. 以下の過程を確認する
    • 先頭の3は5ではないので、rest である (list 5 7 9) を調べる
    • 先頭の5が見つかり、true が返される
    • (list 7 9) は調べられない

ヒント

スライド54-55に詳細な展開過程が記載されている。特に、(= 3 5) が false と評価され、再帰呼び出しに進む部分を注意深く追跡するとよい。

例題7:ベクトルの内積

目的

2つのリストを同時に処理する再帰関数を作成し、複数パラメータを扱う再帰の書き方を学ぶ。

手順

  1. 定義用ウインドウに以下のコードを入力する
;; 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)))]))
  1. Execute ボタンを押す
  2. 実行用ウインドウで以下を実行する
(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つのリストを同時に処理する再帰の計算過程を追跡し、両方のリストが同時に縮小していく様子を確認する。

手順

  1. 定義用ウインドウに例題7のコードと実行文を入力する
;; (例題7のコードに加えて)
(product (list 1 2 3) (list 4 5 6))
  1. Step ボタンとNext ボタンを使ってステップ実行する
  2. 以下の過程を確認する
    • (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 の計算過程の説明

目的

再帰関数の動作を言葉で説明できるようになり、計算過程の理解を深める。

手順

  1. DrScheme で例題2のステップ実行を再度行う
  2. (list-sum (list 1 2 3)) から 6 が得られる過程を観察する
  3. 数行程度(3〜5行)で計算過程を説明する文章を作成する

ヒント

説明には以下の要素を含めるとよい。最初の式がどのように展開されるか、再帰呼び出しが何回行われるか、終了条件に到達したときに何が起こるか、最終的にどのように値が計算されるか。スライド24の概略図を参考にするとよい。

課題2:実行エラーの解決

目的

stepper を使ってエラーの原因を特定する方法を習得し、デバッグ(プログラムの誤りを発見・修正すること)の技術を身につける。

手順

  1. 定義用ウインドウに以下のコードを入力する
(define (list-length a-list)
  (cond
    [(empty? a-list) 0]
    [else (+ 1 (list-length (rest a-list)))]))
(list-length 100)
  1. Execute ボタンを押す(赤い文字でエラーメッセージが表示される)
  2. Step ボタンを押してステップ実行を開始する
  3. エラーが発生する箇所まで Next ボタンで進む
  4. エラーの原因を特定し、報告を作成する

ヒント

このコードの関数定義自体には問題がない。呼び出し方に注目すること。list-length はリストを受け取る関数として定義されているが、呼び出し時に渡されている引数は何か。

課題3:シンボル出現の判定プログラム

目的

contains-5? を一般化し、任意のシンボルを検索できる関数を作成する。

手順

  1. contains-5? の構造を確認する(スライド45)
  2. 以下の仕様を満たす関数 contains? を設計する
    • 入力:シンボルのリスト a-list、検索対象のシンボル a-symbol
    • 出力:a-list が a-symbol を含むとき true、含まないとき false
  3. 設計した関数を実装し、テストする

ヒント

スライド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、真偽値を返す関数)を活用した判定処理を実装する。

手順

  1. contains-5? の構造を確認する
  2. 「先頭要素が5か」を判定している部分を「先頭要素が偶数か」に変更する方針で設計する
  3. 偶数の判定には組み込み関数 even? を使用する
  4. 設計した関数を実装し、様々なリストでテストする

ヒント

(even? 4) は true を返し、(even? 3) は false を返す。contains-5? で (= (first a-list) 5) と書いていた部分を、even? を使った式に置き換えればよい。

課題5:Horner法による多項式計算

目的

数学的なアルゴリズムをプログラムとして実装する経験を積む。Horner法(多項式を効率的に計算する手法)を用いた再帰関数を作成する。

手順

  1. スライド79-80でHorner法のアルゴリズムを理解する
  2. 多項式 f(x) = a₀ + a₁x + a₂x² + ... + aₙxⁿ をHorner法で書き換えると f(x) = a₀ + x(a₁ + x(a₂ + ... + x(aₙ₋₁ + xaₙ)...)) となることを確認する
  3. 係数のリスト(a₀から aₙ の順)と x の値を受け取り、f(x) を計算する関数を設計する
  4. 設計した関数を実装し、テストする

ヒント

計算の順序に注目すること。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 を与えた場合、以下のように計算が進む。

  1. (horner (list 3 6 5) 2)(+ 3 (* 2 (horner (list 6 5) 2))) に展開される
  2. (horner (list 6 5) 2)(+ 6 (* 2 (horner (list 5) 2))) に展開される
  3. (horner (list 5) 2)(+ 5 (* 2 (horner empty 2))) に展開される
  4. (horner empty 2) は終了条件により 0 を返す
  5. 逆順に計算すると、(+ 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 を与えた場合の計算過程は以下のとおりである。

  1. (horner-acc (list 3 6 5) 2 0):acc = 0 + 3×0 ではなく、acc = 0、first = 3 より、新しい acc = 3 + 0×2 = 3
  2. (horner-acc (list 6 5) 2 3):新しい acc = 6 + 3×2 = 12
  3. (horner-acc (list 5) 2 12):新しい acc = 5 + 12×2 = 29
  4. (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句)を用いて、複数の条件分岐を持つ関数を作成する方法を理解する。

手順

  1. DrSchemeを起動する。メニューから「Language → Choose Language → Intermediate Student」を選択し、Executeボタンを押す。
  2. スライド27のグラフを確認し、残高と利率の対応関係を把握する。残高が1000以下なら4%、5000以下なら4.5%、5000より多ければ5%である。
  3. 以下のプログラムを定義ウィンドウに入力する。
    ;; 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]))
  4. Executeボタンを押してプログラムを読み込ませる。
  5. 対話ウィンドウで以下のテストケースを実行し、期待値と一致するか確認する。
    • (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
  6. 境界値(1000, 5000)での動作を確認し、条件式の評価順序を理解する。
ヒント
  • cond句は上から順に条件を評価し、最初に真となった条件の式を返す。このため、条件の記述順序が重要である。
  • 境界値(1000や5000)がどちらの条件に該当するか、不等号の向き(<=, <, >, >=)に注意して確認すること。
  • プログラム設計法のContract、Purpose、Example、Definition、Testの各段階を意識しながら、自分でも別の関数を設計してみるとよい。

課題1:関数fの実行結果

目的
関数定義における引数の個数と、関数呼び出し時の引数の個数の不一致によって発生するエラーを理解する。

手順

  1. DrSchemeを起動し、Intermediate Studentモードに設定する。
  2. 以下の関数fの定義を入力する。
    (define (f n)
      (* n 20))
  3. Executeボタンを押してプログラムを読み込ませる。
  4. 対話ウィンドウで(f 5 20)を実行する。
  5. 表示されるエラーメッセージを確認し、その原因を考察する。
  6. 関数fの定義(引数の個数)と、呼び出し時に渡した引数の個数を比較する。
  7. 考察結果を文章でまとめる。
ヒント
  • 関数定義の(define (f n) ...)において、パラメータnは1つである。
  • 「期待される引数の個数」と「実際に渡された引数の個数」という観点で説明を構成するとよい。

課題2:エラーの原因説明

目的
構文エラー(Syntax Errors)、実行エラー(Run-time Errors)の違いを理解し、各エラーの原因を正確に説明できるようになる。

手順

  1. DrSchemeを起動し、Intermediate Studentモードに設定する。
  2. 以下の6つのScheme式を、1つずつ入力して実行する。
  3. 各式について、以下の手順で分析する。
    • エラーメッセージを確認する
    • エラーの種類(構文エラーまたは実行エラー)を判定する
    • エラーの原因を特定する
  4. 分析結果を、式ごとに以下の形式でまとめる。
    • 式の内容
    • エラーの種類
    • 原因の説明

各式の分析観点

式番号 エラー種類 分析の観点
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))とすべきである。

論理的エラーへの対処方針

論理的エラーは構文的に正しく、実行も完了するため、発見が困難である。以下の方法で対処する。

  1. Exampleの活用:プログラム設計法のExample段階で、事前に期待される入出力を明確にしておき、実行結果と照合する。
  2. 境界値のテスト:特殊なケース(0、負数、最大値など)でテストを行い、異常な結果が出ないか確認する。
  3. 手計算との照合:小さな入力値で手計算した結果と、プログラムの出力を比較する。

補足:プログラム設計法(Design Recipe)の実践

本演習では直接の課題として指定されていないが、プログラム設計法を、例題1や自作の関数に適用して練習することを推奨する。

設計法の5段階は以下のとおりである。

  1. Contract:関数名、入力の型、出力の型を定義する
  2. Purpose:関数が何をするかを文章で記述する
  3. Example:具体的な入出力の例を示す
  4. Definition:関数本体を作成する
  5. Test:Exampleで示した例を実行し、期待値と一致するか確認する

sp-9. 高階関数

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/K1XRGK-2022-01-26-145619

演習パート(クリックして展開)

学習の到達目標

本演習を完了すると、以下のことができるようになる。

環境設定

DrSchemeを起動し、Language設定を「Advanced Student」に変更する。手順は以下のとおりである。

  1. DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
  2. Language → Choose Language → Advanced Student → OK
  3. Executeボタンを押す

Advanced Studentモードは高階関数の実行に必要である。

注意:Intermediate Studentモードでは高階関数を実行できない。必ずAdvanced Studentモードに設定すること。

例題1:リストの総和

目的

高階関数reduce(関数を引数として受け取り、リストの要素を順次処理する関数)の動作を理解し、これを利用してリストの総和を求める関数list-sumを作成する。

手順

  1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
    • reduce関数の定義
    • list-sum関数の定義
  2. 実行用ウインドウで以下を実行する
    • (list-sum (list 1 2 3))
    • (list-sum empty)
  3. 実行結果を確認し、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関数は(reduce g 0 (list 1 2 3))(g 1 (g 2 (g 3 0)))に展開する。list-sumではg+(加算)、base0となるため、(+ 1 (+ 2 (+ 3 0)))すなわち6が得られる。置き換え過程を紙に書いて追跡すると理解が深まる。

例題2:xのn乗の近似値

目的

#iを付けた近似計算の動作を理解する。Schemeでは#iを数値に付けると、その計算は近似値(浮動小数点数)として処理される。

手順

  1. 実行用ウインドウで以下を実行する
    • (expt 11 200)
    • (expt #i11 200)
  2. 両者の結果を比較する
ヒント:#iなしの場合は有理数として厳密に計算されるため、巨大な整数が表示される。#iありの場合は近似値として計算され、#i1.899...e+208のような指数表記で表示される。e+208は10の208乗を意味する。

例題3:数列の和

目的

高階関数seriesを使用して、数列の和 Σf(k)(k=1からn)を計算する方法を理解する。

手順

  1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
    • series関数の定義
    • f2関数(k²を返す)とf3関数(k³を返す)の定義
  2. 実行用ウインドウで以下を実行する
    • (series 3 f2) → 1² + 2² + 3² = 14
    • (series 4 f3) → 1³ + 2³ + 3³ + 4³ = 100
  3. 実行結果を確認する

コード

;; 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))
ヒント:seriesは再帰的に定義されている。n=1のとき(f 1)を返し、n>1のとき(+ (f n) (series (- n 1) f))を返す。置き換え過程を追跡して、再帰の展開を確認すること。

例題4:数列の和のリスト

目的

series-iter関数を使用して、数列の部分和のリストを作成する。これにより、級数の収束の様子を観察できる。

手順

  1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
    • series関数の定義
    • series-iter関数の定義
    • f4関数(1/k²を返す)の定義
  2. 実行用ウインドウで以下を実行する
    • (series-iter #i100 f4)
  3. 結果のリストを確認し、Σ(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)))
ヒント:series-iterはn項までの和、n-1項までの和、...、1項までの和からなるリストを返す。リストの先頭が最も多くの項を含む和である。#iを付けることで近似値として計算され、収束の様子が見やすくなる。

例題5:べき級数

目的

高階関数taylor-seriesを使用して、べき級数 Σg(k)·xk(k=0からn)を計算する方法を理解する。

手順

  1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
    • taylor-series関数の定義
    • g1関数(kを返す)の定義
  2. 実行用ウインドウで以下を実行する
    • (taylor-series 2 3 g1) → 0·2⁰ + 1·2¹ + 2·2² + 3·2³ = 0 + 2 + 8 + 24 = 34
    • (taylor-series 2 4 g1)
  3. 実行結果を確認する

コード

;; 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)
ヒント:taylor-seriesはべき級数を計算する汎用関数である。係数を決める関数gを変えることで、様々なテーラー展開に応用できる。置き換え過程を追跡すること。

例題6:指数関数のテーラー展開

目的

taylor-series関数を使用して、指数関数exのテーラー展開による近似値を計算する。

手順

  1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
    • taylor-series関数の定義
    • factorial関数(階乗を計算)の定義
    • exp-term関数(1/k!を返す)の定義
    • my-exp関数の定義
  2. 実行用ウインドウで以下を実行する
    • (my-exp 0), (my-exp 1), (my-exp 2), (my-exp 3)
    • (my-exp #i1), (my-exp #i2), (my-exp #i3)
  3. #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))
ヒント:exのテーラー展開は ex = Σ(xk/k!)(k=0から∞)である。exp-term関数は係数1/k!を返す。my-expは20項までの部分和を計算するため、x21/21!以降の項が誤差となる。#iなしの場合は分数として厳密に計算される。

例題7:cos関数のテーラー展開

目的

taylor-series関数を使用して、cos関数のテーラー展開による近似値を計算する。

手順

  1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
    • taylor-series関数、factorial関数の定義
    • cos-term関数の定義
    • my-cos関数の定義
  2. 実行用ウインドウで以下を実行する
    • (my-cos #i0), (my-cos #i0.2), (my-cos #i0.4), (my-cos #i0.6), (my-cos #i0.8)
  3. 結果を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))
ヒント:cos xのテーラー展開は cos x = Σ((-1)k · x2k / (2k)!)(k=0から∞)である。奇数次の項は0である。cos-term関数ではodd?(奇数判定)を使用して奇数次の項を0にし、remainder(剰余)を使用して符号を交互に変えている。

例題8:対数関数のテーラー展開

目的

taylor-series関数を使用して、対数関数log(1+x)のテーラー展開による近似値を計算する。

手順

  1. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
    • taylor-series関数の定義
    • log-term関数の定義
    • my-log関数の定義
  2. 実行用ウインドウで以下を実行する
    • (my-log #i1), (my-log #i1.2), (my-log #i1.4), (my-log #i1.6), (my-log #i1.8)
  3. 結果を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))
ヒント:log(1+x)のテーラー展開は log(1+x) = Σ((-1)k+1 · xk / k)(k=1から∞)である。収束半径は1であるため、|x| ≤ 1の範囲でのみ有効である。my-log関数では引数xに対してlog(x)を計算するため、内部でx-1をtaylor-seriesに渡している。

課題1:近似計算の理解

目的

#i記号と指数表記の意味を正確に理解し、近似計算と厳密計算の違いを説明できるようになる。

手順

  1. #i9.313e+020の意味を説明する
  2. 以下の3つを実行し、結果を比較する
    • (expt 12 100)
    • (expt #i12 100)
    • (expt 12 #i100)
  3. 各結果の違いを報告する
ヒント:e+020は1020を意味する。#iを付けた数値は近似値として扱われ、計算結果も近似値となる。どちらか一方の引数に#iを付けても、結果は近似値となることを確認すること。

課題2:my-expの理解

目的

テーラー展開の部分和を段階的に計算することで、級数の収束過程を理解する。

手順

  1. 例題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!
  2. 各値を報告する
ヒント:my-exp関数のtaylor-seriesの第2引数(項数)を1, 2, 3, 4, 5と変えて実行することで、各部分和を計算できる。eの真の値(約2.71828)にどのように近づくかを観察すること。

課題3:数列の和の列1

目的

series-iter関数を使用して交代級数(正負の項が交互に現れる級数)の部分和のリストを作成する。

手順

  1. 数列 Σ((-1)n+1 / (2n-1))(n=1から∞)の一般項を計算する関数を定義する
    • n=1: 1/1, n=2: -1/3, n=3: 1/5, n=4: -1/7, ...
  2. series-iterを使用して部分和のリストを作成する
  3. #iありとなしの結果の違いを報告する
ヒント:この級数はπ/4(約0.7854)に収束する(ライプニッツの公式)。#iなしの場合は分数として厳密に計算され、#iありの場合は小数の近似値として計算される。

課題4:数列の和の列2

目的

series-iter関数を使用して階乗の逆数の和の部分和のリストを作成する。

手順

  1. 数列 Σ(1/n!)(n=1から∞)の一般項を計算する関数を定義する
    • n=1: 1/1!, n=2: 1/2!, n=3: 1/3!, ...
  2. series-iterを使用して部分和のリストを作成する
  3. #iありとなしの結果の違いを報告する
ヒント:この級数はe-1(約1.71828)に収束する。factorial関数(例題6で定義)を利用すること。

課題5:sin関数のテーラー展開

目的

例題7のcos関数と同様の方法で、sin関数のテーラー展開を実装する。

手順

  1. sin xのテーラー展開式を確認する
    • sin x = x - x³/3! + x⁵/5! - x⁷/7! + ...
    • sin x = Σ((-1)k · x2k+1 / (2k+1)!)(k=0から∞)
  2. sin-term関数を定義する(偶数次の項は0、奇数次の項のみ非零)
  3. my-sin関数を定義する
  4. 様々なxの値で実行し、Scheme組み込みのsin関数と比較して正しさを確認する
ヒント:cos-term関数(例題7)では奇数次の項が0であったが、sin-term関数では偶数次の項が0となる。even?関数(偶数ならばtrue)を使用する。符号の決定にはremainderを使用し、4で割った余りで判定する方法がある。

課題6:tan⁻¹関数のテーラー展開

目的

tan⁻¹関数(逆正接関数、arctangent)のテーラー展開を実装する。

手順

  1. tan⁻¹ xのテーラー展開式を確認する
    • tan⁻¹ x = x - x³/3 + x⁵/5 - x⁷/7 + ...
    • tan⁻¹ x = Σ((-1)k · x2k+1 / (2k+1))(k=0から∞)
  2. atan-term関数を定義する(偶数次の項は0、奇数次の項のみ非零)
  3. my-atan関数を定義する
  4. 様々なxの値で実行し、Scheme組み込みのatan関数と比較して正しさを確認する
ヒント:sin関数との違いは、分母が(2k+1)!ではなく(2k+1)である点である。階乗を使用しないため、sin-termよりも単純な定義となる。収束半径は1であるため、|x| ≤ 1の範囲で実行すること。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」に変更する。設定手順は以下のとおりである。

  1. DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
  2. メニューから Language → Choose Language を選択する
  3. Intermediate Student を選択する
  4. Execute ボタンを押す

例題1:ball構造体

目的

構造体(structure:複数のデータをまとめて1つの型として扱うデータ構造)の定義方法を理解する。define-struct文を使用してball構造体を定義する。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (define-struct ball (x y delta-x delta-y))
  2. Executeボタンを押す
  3. 実行結果は表示されないが、これでball構造体が定義される

ヒント

define-structを実行すると、コンストラクタ(構造体を生成する関数)とセレクタ(属性値を取得する関数)が自動的に使用可能になる。ball構造体の場合、make-ball(コンストラクタ)とball-x, ball-y, ball-delta-x, ball-delta-y(セレクタ)が使えるようになる。

例題2:ballの原点からの距離

目的

セレクタを使って構造体から属性値を取り出し、計算に利用する方法を学ぶ。ball構造体のx, y座標から原点までの距離を計算する関数を作成する。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (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)))))
  2. Executeボタンを押す
  3. 実行用ウインドウで以下を入力し実行する
    (distance-to-0 (make-ball 3 4 0 0))
  4. 結果として「5」が表示されることを確認する

ヒント

ball-xとball-yはセレクタであり、ball構造体から属性値を取り出す。(ball-x a-ball)はa-ballのx座標を返す。距離の計算には三平方の定理(ピタゴラスの定理)を使用している。

例題3:ステップ実行

目的

DrSchemeのstepper機能を使い、関数の実行過程を段階的に追跡することで、評価の順序と構造体のセレクタの動作を理解する。

手順

  1. 定義用ウインドウに以下のコードを入力する(例題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))
  2. Stepボタンを押す
  3. Nextボタンを繰り返し押し、各ステップで式がどのように変換されるか観察する
  4. (make-ball 3 4 0 0)からball-xで3が取り出される過程、sqrで9が計算される過程などを確認する

ヒント

ステップ実行では、関数呼び出しが定義本体で置き換えられ、セレクタが具体的な値に置き換えられる様子が観察できる。最終的に(sqrt 25)が5に評価される過程まで追跡する。

例題4:AddressNote構造体

目的

name, age, addressの3つの属性を持つAddressNote構造体を定義する。異なる属性構成の構造体定義に慣れる。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (define-struct AddressNote (name age address))
  2. Executeボタンを押す
  3. この定義により、make-AddressNote(コンストラクタ)とAddressNote-name, AddressNote-age, AddressNote-address(セレクタ)が使用可能になる

ヒント

構造体名と属性名を組み合わせたセレクタ名(例:AddressNote-name)は自動的に生成される。属性の順序は定義時の順序と一致する。

例題5:住所録

目的

構造体のリストを処理し、各要素から特定の属性を抽出する関数を作成する。再帰処理とセレクタの組み合わせを学ぶ。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (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)))]))
  2. Executeボタンを押す
  3. 実行用ウインドウで以下を入力し実行する
    (select-name (list (make-AddressNote "Ken" 35 "Fukuoka")
                       (make-AddressNote "Bill" 30 "Saga")
                       (make-AddressNote "Mike" 28 "Nagasaki")))
  4. 結果として(list "Ken" "Bill" "Mike")が表示されることを確認する

ヒント

この関数はリストの各要素に対して再帰的に処理を行う。(first a-list)でリストの先頭要素(AddressNote構造体)を取得し、AddressNote-nameで名前を抽出する。(rest a-list)で残りのリストに対して同じ処理を繰り返す。

例題6:複素数の計算

目的

DrSchemeに組み込まれているrectangular構造体を使用し、複素数の四則演算を行う。make-rectangularによる複素数の表現方法を理解する。

手順

  1. 実行用ウインドウで以下の式を順番に実行する
    (+ (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))
  2. 各計算結果を確認する

ヒント

(make-rectangular x y)は実数部x、虚数部yの複素数を表す。(make-rectangular 3 4)は3+4iを意味する。(+ (1+2i) (3+4i))のように直接記述するとシンタックスエラーになるため、必ずmake-rectangularを使用する。

例題7:複素数のべき乗

目的

複素数のべき乗を計算する関数を作成する。expt関数が複素数にも適用できることを確認する。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (define (myexp theta n)
      (expt (make-rectangular (cos theta) (sin theta)) n))
  2. Executeボタンを押す
  3. 実行用ウインドウで以下を順番に実行する
    (myexp 0.5236 1)
    (myexp 0.5236 2)
    (myexp 0.5236 3)
  4. 結果に「#i」が付いていることを確認する(#iは近似値を意味する)

ヒント

この関数は(cosθ + i sinθ)nを計算する。θの単位はラジアンである。0.5236ラジアンは約30度に相当する。

課題1:distance-to-0の実行

目的

例題2で作成したdistance-to-0関数を異なる入力で実行し、動作を確認する。

手順

  1. 例題2のコードを定義用ウインドウに入力しExecuteする
  2. 実行用ウインドウで以下の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))
  3. 各実行結果を記録し報告する

ヒント

2番目と3番目の式では、make-ballの引数に算術式が含まれている。まず算術式が評価され、その結果がx, yの値として使用される。

課題2:関数inの作成

目的

ball構造体のx座標が特定の範囲内にあるかを判定する関数を作成する。条件分岐と比較演算の組み合わせを練習する。

手順

  1. ball構造体を定義する
  2. 関数inを作成する。この関数は以下の仕様を満たす必要がある
    • 入力:ball構造体
    • 出力:xの値が0より大きく100より小さい場合はtrue、それ以外はfalse
    • x = 0またはx = 100の場合はfalseを返す
  3. 作成した関数をテストし、結果を報告する

ヒント

スライドに示されたcond文の骨格を完成させる。ball-xセレクタでx座標を取得し、比較演算子(<, >)を使って範囲判定を行う。「0 < x < 100」を満たす条件をSchemeの論理式で表現する方法を考える。

課題3:年齢による分類

目的

AddressNote構造体の年齢属性に基づいて、成人か子供かを判定する関数を作成する。

手順

  1. AddressNote構造体を定義する
  2. 関数を作成する。この関数は以下の仕様を満たす必要がある
    • 入力:AddressNote構造体
    • 出力:ageが20以上なら'Adult、20未満なら'Child
  3. 作成した関数をテストし、結果を報告する

ヒント

AddressNote-ageセレクタで年齢を取得する。cond文または条件式を使って20との比較を行う。

課題4:関数check-by-ageの作成

目的

指定された年齢と一致する場合に名前を返す関数を作成する。2つの引数を取る関数の定義を練習する。

手順

  1. AddressNote構造体を定義する
  2. 関数check-by-ageを作成する。この関数は以下の仕様を満たす必要がある
    • 入力:AddressNote構造体(a-person)と年齢(an-age)
    • 出力:a-personの年齢がan-ageと一致すれば名前を返し、一致しなければ"none"を返す
  3. スライドに示された例で動作を確認し、結果を報告する

ヒント

関数定義で2つのパラメータを受け取る形式を使用する。年齢の比較には=演算子を用いる。

課題5:関数selection-by-ageの作成

目的

AddressNote構造体のリストから、指定年齢と一致する要素のみを抽出する関数を作成する。リストのフィルタリング処理を練習する。

手順

  1. AddressNote構造体を定義する
  2. 関数selection-by-ageを作成する。この関数は以下の仕様を満たす必要がある
    • 入力:AddressNote構造体のリスト(a-list)と年齢(an-age)
    • 出力:年齢がan-ageと一致するAddressNote構造体のリスト
  3. スライドに示された例で動作を確認し、結果を報告する

ヒント

例題5のselect-name関数の構造を参考にする。ただし、select-nameは全要素の名前を抽出するのに対し、この関数は条件に合う要素のみをリストに含める。条件判定の結果によってconsするかどうかを分岐させる。

課題6:関数sum-ageの作成(年齢の平均)

目的

AddressNote構造体のリストから年齢の平均値を求める関数を作成する。集計処理とリスト長の取得を組み合わせる。

手順

  1. AddressNote構造体を定義する
  2. 関数を作成する。この関数は以下の仕様を満たす必要がある
    • 入力:AddressNote構造体のリスト
    • 出力:年齢の平均値
  3. テストデータで動作を確認し、結果を報告する

ヒント

平均を求めるには、年齢の合計とリストの長さが必要である。合計を求める補助関数と、リストの長さを求める処理(lengthなど)を組み合わせて実装する。

補足:ドモアブルの定理

本演習は発展的な内容であり、さらに学習を深めたい受講者向けである。

目的

ドモアブルの定理((cosθ + i sinθ)n = cos nθ + i sin nθ)を計算で確認し、近似計算における誤差の蓄積を観察する。

手順

  1. 定義用ウインドウに以下のコードを入力する
    (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)))]))
  2. Executeボタンを押す
  3. 実行用ウインドウで以下を実行する
    (f1 0.05 20)
    (f2 0.05 20)
  4. 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として知られる処理系の旧名称)を使用するための初期設定を行う。

手順:
  1. DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
  2. 言語設定を「Intermediate Student」に変更する(Language → Choose Language → Intermediate Student → Executeボタン)
  3. draw.ss teachpackをロードする(Language → Add Teachpack → htdpディレクトリ → draw.ss → Executeボタン)

例題1:簡単な絵を描く

目的:

DrSchemeの描画機能であるdraw.ss teachpackを使用し、線、矩形、円などの基本図形を描画する方法を習得する。

手順:
  1. DrSchemeの「実行用ウインドウ」(下部のインタラクションエリア)を使用する
  2. (start 100 100) を入力して実行し、100×100ピクセルの描画用ウインドウを開く
  3. 以下の描画命令を順に実行する
    • (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) — 赤い塗りつぶし円を描く
  4. (stop) を実行して描画用ウインドウを閉じる
ヒント:
  • make-posn は座標を表すposn構造体(position:位置を表す構造体)を生成する関数である。(make-posn x y) の形式でx座標とy座標を指定する
  • 色は 'red'green'blue などのシンボル(クォート記号 ' に続く識別子)で指定する
  • 描画用ウインドウの座標系は、左上が原点(0, 0)で、x軸は右方向、y軸は下方向が正となる

例題2:ball構造体を描く

目的:

構造体(複数の属性をまとめたデータ型)とグラフィックス処理を組み合わせ、構造体のデータを用いて描画を行う方法を学ぶ。

手順:
  1. 「定義用ウインドウ」(上部のエディタエリア)に以下のコードを入力する
    (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))
  2. Executeボタンを押して定義を読み込む
  3. 「実行用ウインドウ」で以下を順に実行する
    (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-xball-yball-delta-xball-delta-y
  • ball構造体では、x, yが位置を、delta-x, delta-yが速度を表す
  • (make-ball 10 10 0 0) は、位置(10, 10)、速度(0, 0)のballを生成する

例題3:リストの変数定義

目的:

define を使用してリストを値とする変数を定義し、リスト操作関数の動作を確認する。

手順:
  1. 「定義用ウインドウ」に以下を入力し、Executeボタンを押す
    (define A (list 15 8 6))
  2. 「実行用ウインドウ」で以下を順に実行する
    A
    (first A)
    (rest A)
ヒント:
  • define は変数を定義する特殊形式である。(define 変数名 値) の形式で使用する
  • first はリストの先頭要素を返し、rest はリストの先頭を除いた残りのリストを返す
  • A と入力すると、変数Aの値が表示される

例題4:構造体のリスト

目的:

構造体のリストを定義し、リスト操作と構造体のセレクタを組み合わせて使用する方法を学ぶ。

手順:
  1. 「定義用ウインドウ」に以下を入力し、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")))
  2. 「実行用ウインドウ」で以下を順に実行する
    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に組み込み済みの座標を表す構造体)のリストを定義し、図形の頂点データを表現する方法を学ぶ。

手順:
  1. 「定義用ウインドウ」に以下を入力し、Executeボタンを押す
    (define P
      (cons (make-posn 0 0)
            (cons (make-posn 10 70)
                  (cons (make-posn 100 30) empty))))
  2. 「実行用ウインドウ」で以下を順に実行する
    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:折れ線

目的:

再帰(関数が自分自身を呼び出す手法)を用いて、頂点のリストから折れ線を描画する関数を作成する。

手順:
  1. 「定義用ウインドウ」に以下を入力し、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)))]))
  2. 「実行用ウインドウ」で以下を順に実行する
    (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の折れ線描画を拡張し、終点と始点を結んで多角形を描画する関数を作成する。

手順:
  1. 「定義用ウインドウ」に例題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)))
  2. 「実行用ウインドウ」で以下を順に実行する
    (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:描画命令の実行

目的:

描画関数のパラメータを変更し、異なるサイズ・位置での描画結果を確認する。

手順:
  1. 描画用ウインドウを開く:(start 300 300)
  2. 赤い線を描く:(draw-solid-line (make-posn 100 100) (make-posn 200 200) 'red)
  3. 赤い円を描く:(draw-circle (make-posn 200 100) 50 'red)
  4. 緑の塗りつぶし円を描く:(draw-solid-disk (make-posn 100 200) 50 'green)
  5. 描画用ウインドウを閉じる:(stop)
ヒント:
  • 例題1と比較して、ウインドウサイズが300×300に拡大されている点に注意する
  • 各描画関数のパラメータ(座標、サイズ、色)と描画結果の関係を観察する
  • draw-circle は輪郭のみ、draw-solid-disk は塗りつぶしである点を確認する

課題2:ボールの速さを求める関数

目的:

ball構造体から速度成分を取り出し、数学的な計算を行う関数を作成する。

手順:
  1. 例題2のball構造体定義を使用する
  2. ball構造体から速さを計算する関数を作成する
  3. 作成した関数をテストし、結果を確認する
ヒント:
  • ボールの速さは √(delta-x² + delta-y²) で計算される
  • Schemeでは平方根は sqrt 関数、累乗は sqr 関数(2乗)または expt 関数(一般の累乗)を使用する
  • セレクタ ball-delta-xball-delta-y を使用して速度成分を取り出す
  • 関数の入力はball構造体、出力は数値(速さ)となる

課題3:境界判定関数

目的:

ball構造体の位置を評価し、条件分岐を用いて境界外かどうかを判定する関数を作成する。

手順:
  1. 例題2のball構造体定義を使用する
  2. ball構造体の位置が「x < 0 または y < 0 または x > 100 または y > 100」のときtrueを返す関数を作成する
  3. 境界内外の両方のケースでテストする
ヒント:
  • Schemeでは論理和(または)は or 関数を使用する
  • 比較演算子として <> を使用する
  • 条件が1つでも成立すればtrueを返し、すべて成立しなければfalseを返す

課題4:2点間の距離を求める関数

目的:

点を表す構造体を自分で定義し、2点間の距離を計算する関数を作成する。

手順:
  1. x-y平面上の点を表す構造体を定義する
  2. 2点a, bを受け取り、その間の距離を返す関数 distance を作成する
  3. テスト用の点を作成し、関数の動作を確認する
ヒント:
  • 2点間の距離は √((x₂-x₁)² + (y₂-y₁)²) で計算される
  • 点を表す構造体には、少なくともxとyの2つの属性が必要である
  • posn構造体を使用することも、独自の構造体を定義することも可能である

課題5:複数のballを描画

目的:

リストの再帰処理を用いて、複数のball構造体を描画する関数を作成する。

手順:
  1. 例題2の draw-ball 関数を参照する
  2. ball構造体のリストを受け取り、すべてのballを描画する関数 draw-all-balls を作成する
  3. 複数のball構造体を含むリストを作成し、動作を確認する
ヒント:
  • 例題6の draw-polyline と同様の再帰パターンを使用する
  • 終了条件は「リストが空のとき」であり、empty? で判定する
  • 各ballに対して draw-ball を呼び出し、残りのリストに対して再帰呼び出しを行う
  • 描画命令を並べるときは and でつなぐことを忘れない

課題6:複数のballを消す

目的:

課題5と同様のパターンで、複数のballを消去する関数を作成する。

手順:
  1. clear-ball 関数を参照する
  2. ball構造体のリストを受け取り、すべてのballを消す関数 clear-all-balls を作成する
  3. 動作確認を行う(描画してから消去する)
ヒント:
  • clear-solid-diskdraw-solid-disk で描いた円を消す関数である
  • 課題5の draw-all-balls とほぼ同じ構造になる
  • clear-ball は色のパラメータを取らない点に注意する

補足:例題8:ballを1秒描く

目的:

タイマー機能を用いて、一定時間だけ図形を表示してから消去する処理を学ぶ。

手順:
  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)))
  2. 「実行用ウインドウ」で以下を順に実行する
    (start 100 100)
    (draw-and-clear (make-ball 10 10 0 0))
    (stop)
ヒント:
  • sleep-for-a-while は指定した秒数だけ処理を停止する関数である
  • 変数 DELAY を使用することで、待機時間を容易に変更できる
  • and で3つの処理(描画、待機、消去)を順番に実行している

補足:例題9:動くballを描く

目的:

再帰とタイマーを組み合わせて、ballが移動するアニメーションを作成する。

手順:
  1. 「定義用ウインドウ」に以下を入力し、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))))
  2. 「実行用ウインドウ」で以下を順に実行する
    (start 100 100)
    (animation (make-ball 0 0 10 5))
    (stop)
ヒント:
  • move-ball は現在位置に速度を加算して新しいball構造体を生成する
  • animation は自分自身を再帰的に呼び出すことで、無限にアニメーションを続ける
  • アニメーションを停止するには、描画用ウインドウを手動で閉じるか、DrSchemeを停止する
  • 終了条件がないため、このままでは無限ループとなる

課題7:複数のballを動かす

目的:

リストの各要素に対して移動処理を適用する関数を作成する。

手順:
  1. 例題9の move-ball 関数を参照する
  2. ball構造体のリストを受け取り、すべてのballを移動させた新しいリストを返す関数 move-all-balls を作成する
  3. 動作確認を行う
ヒント:
  • この関数は描画ではなく、リストの変換を行う
  • 入力:ball構造体のリスト、出力:移動後のball構造体のリスト
  • cons を使用して、移動後のballを新しいリストに追加していく
  • 再帰の終了条件は「リストが空のとき」で、空リスト empty を返す

課題8:境界外判定とウインドウ制御

目的:

条件判定を組み込み、すべてのballが境界外になったらアニメーションを終了する機能を実装する。

手順:
  1. 課題3の境界判定関数を活用する
  2. 課題5の draw-all-balls を修正し、境界外のballは描画しないようにする
  3. すべてのballが境界外にあるかを判定する関数を作成する
  4. すべてが境界外の場合、(stop) で描画用ウインドウを閉じる
ヒント:
  • cond を使用して、境界内のballのみ描画するよう条件分岐する
  • 「すべてのballが境界外」を判定するには、リスト内のすべての要素について境界判定を行い、論理積(and)を取る
  • または、境界内のballが1つでも存在すればアニメーションを続行するという判定方法もある

課題9:複数ballのアニメーション

目的:

例題9を拡張し、複数のballが同時に動くアニメーションを作成する。

手順:
  1. 課題5(draw-all-balls)、課題6(clear-all-balls)、課題7(move-all-balls)を組み合わせる
  2. すべてのballが境界外になったらアニメーションが終了するようにする
  3. 複数のball構造体を含むリストを作成し、アニメーションの動作を確認する
ヒント:
  • 例題9の animation 関数の構造を参考に、リストを処理するバージョンを作成する
  • 終了条件として課題8で作成した「すべて境界外」の判定を使用する
  • 終了時には true を返すようにする

課題10:動的な大きさ・色の変化

目的:

アニメーション中にballの属性(大きさや色)を変化させる機能を追加する。

手順:
  1. 課題9のプログラムを基にする
  2. ball構造体に大きさや色の属性を追加することを検討する、または別の方法で大きさ・色を変化させる
  3. 移動するたびに属性が変化するようにプログラムを修正する
ヒント:
  • 方法1:ball構造体に大きさ(size)や色(color)の属性を追加する
  • 方法2:移動回数や位置に応じて大きさ・色を計算する
  • 色をリストで用意し、順番に変化させる方法もある
  • 大きさの変化には上限・下限を設けると見やすくなる

課題11:跳ね返り処理

目的:

境界に達したballが跳ね返る物理シミュレーションを実装する。

手順:
  1. 課題9のプログラムを基にする
  2. ballが境界に達したとき、速度の符号を反転させて跳ね返るようにする
  3. 跳ね返りの動作を確認する
ヒント:
  • 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

演習パート(クリックして展開)

例題1:階乗

目的

再帰的定義に基づいて階乗関数を作成し、再帰呼び出しの基本構造を理解する。

手順

  1. DrSchemeを起動し、Intermediate Studentモードに設定する。
  2. 定義用ウインドウに以下のコードを入力する。
    ;;! : number -> number
    ;;to compute n*(n-1)*...*2*1
    (define (! n)
      (cond
        [(= n 0) 1]
        [else (* n (! (- n 1)))]))
  3. Executeボタンを押してプログラムを読み込む。
  4. 実行用ウインドウで (! 3)(! 4) を実行し、結果を確認する。
  5. 関数定義の構造を確認する。終了条件(n = 0のとき1を返す)と再帰呼び出し(n × (n-1)!を計算)の2つの部分に注目する。

ヒント

階乗の再帰的定義は「n = 0のとき1、n > 0のとき n × (n-1)!」である。プログラム中の (= n 0) が終了条件、(* n (! (- n 1))) が再帰呼び出しに対応する。終了条件がないと無限に再帰呼び出しが続くため、必ず終了条件を設けること。

例題2:ステップ実行(階乗)

目的

DrSchemeのstepper機能を用いて、再帰呼び出しがどのように展開・収縮するかを視覚的に理解する。

手順

  1. 例題1のコードに (! 3) を追加し、定義用ウインドウに入力する。
    ;;! : number -> number
    ;;to compute n*(n-1)*...*2*1
    (define (! n)
      (cond
        [(= n 0) 1]
        [else (* n (! (- n 1)))]))
    (! 3)
  2. Executeボタンを押す。
  3. Stepボタンを押してステップ実行を開始する。
  4. Nextボタンを押しながら、各ステップでの式の変化を観察する。
  5. 以下の展開過程を確認する。
    • (! 3)(* 3 (! 2))(* 3 (* 2 (! 1)))(* 3 (* 2 (* 1 (! 0))))
    • (* 3 (* 2 (* 1 1)))(* 3 (* 2 1))(* 3 2)6

ヒント

式が膨張していく過程(基本的な計算式への展開)と、収縮していく過程(演算の実行)の2段階があることに注目する。この形式を線形再帰的プロセス(linear recursive process)と呼ぶ。再帰の呼び出し回数に比例して式が成長するため「線形」と名付けられている。(! 3) では関数 ! が4回繰り返して実行される。

例題3:反復的プロセスでの階乗

目的

反復的プロセス(iterative process)による階乗計算を実装し、線形再帰的プロセスとの違いを理解する。

手順

  1. 定義用ウインドウに以下のコードを入力する。
    ;; ! : 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)]))
  2. Executeボタンを押す。
  3. 実行用ウインドウで (! 4)(! 10)(! 20) を実行する。
  4. factorial 関数の3つの引数(product、counter、max)の役割を確認する。

ヒント

この方式では、productに部分積(計算途中の結果)、counterに現在の乗数を保持しながら計算を進める。線形再帰的プロセスのような式の伸び縮みがなく、各ステップで計算が実行される点が特徴である。終了条件は counter > max のときで、そのときのproductが答えとなる。

例題4:ステップ実行(反復的プロセス)

目的

反復的プロセスの実行過程をstepperで確認し、線形再帰的プロセスとの違いを明確にする。

手順

  1. 例題3のコードに (! 4) を追加し、定義用ウインドウに入力する。
  2. Executeボタンを押す。
  3. Stepボタン、Nextボタンを使ってステップ実行を行う。
  4. 以下の過程を確認する。
    • (factorial 1 1 4)(factorial 1 2 4)(factorial 2 3 4)(factorial 6 4 4)(factorial 24 5 4)24
  5. 各ステップでproductとcounterの値がどう変化するかを記録する。

ヒント

線形再帰的プロセス(例題2)では式が膨張してから収縮したが、反復的プロセスでは式のサイズが一定のまま計算が進む。product、counter、maxの3つの値だけで計算状態が完全に表現されている点に注目する。(! 4) では factorial が5回繰り返して実行される。

例題5:繰り返し回数

目的

リスト処理における関数の繰り返し回数について考察する。

手順

  1. 以下のコードを定義用ウインドウに入力する。
    (define (square x) (* x x))
    (define (total-square x)
      (cond
        [(empty? x) 0]
        [else (+ (square (first x))
                 (total-square (rest x)))]))
  2. Executeボタンを押す。
  3. (total-square (list 10 20 30)) を実行し、結果が1400になることを確認する。
  4. square 関数が何回実行されるかを考える。

ヒント

square 関数はリストの要素ごとに1回ずつ呼び出される。リストの要素数が3であれば、square は3回実行される。total-square 自体は要素数+1回(終了条件の判定を含む)呼び出される。

例題6:最大公約数の計算

目的

ユークリッドの互除法(Euclidean algorithm)を用いて最大公約数を求める関数を作成する。

手順

  1. 定義用ウインドウに以下のコードを入力する。
    ;; 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))]))
  2. Executeボタンを押す。
  3. 実行用ウインドウで (my-gcd 180 32) を実行し、結果が4になることを確認する。

ヒント

ユークリッドの互除法は以下の原理に基づく。mとnの最大公約数を求めるとき、n = 0ならば答えはm、n ≠ 0ならば「nとmをnで割った余り」の最大公約数に等しい。remainder 関数は剰余(割り算の余り)を返す。終了条件は n = 0 のときで、そのときのmが最大公約数となる。

例題7:ステップ実行(最大公約数)

目的

ユークリッドの互除法の実行過程をstepperで確認し、繰り返し回数を数える。

手順

  1. 例題6のコードに (my-gcd 180 32) を追加し、定義用ウインドウに入力する。
  2. Executeボタンを押す。
  3. Stepボタン、Nextボタンを使ってステップ実行を行う。
  4. 以下の過程を確認する。
    • (my-gcd 180 32)(my-gcd 32 20)(my-gcd 20 12)(my-gcd 12 8)(my-gcd 8 4)(my-gcd 4 0)4
  5. my-gcd が何回呼び出されるかを数える。

ヒント

m=180、n=32のとき、my-gcd は6回繰り返して実行される。各ステップで余りがどのように減少していくかに注目する。ユークリッドの互除法は非常に効率的なアルゴリズムであり、入力値に対して繰り返し回数が少ない。

課題1:my-gcdの実行過程

目的

例題6〜7で学んだユークリッドの互除法の理解を確認し、異なる入力での実行過程を説明できるようになる。

手順

  1. 例題7と同様の手順で、(my-gcd 210 66) をステップ実行する。
  2. 各ステップでmとnの値がどう変化するかを記録する。
  3. 最終的に6が得られる過程を数行程度で説明文にまとめる。

ヒント

例題7の (my-gcd 180 32) の説明を参考にする。各ステップで「mをnで割った余り」を計算し、mとnの値がどう変わるかを追跡する。説明では、呼び出しの列((my-gcd 210 66)(my-gcd ? ?) → ...)を示しながら記述するとよい。

課題2:sum-coins関数

目的

リストを引数とする再帰関数の構造を理解し、空欄補充によってプログラムを完成させる。

手順

  1. 以下のコードの空欄を埋める。
    ;; 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 (+ (*  (   ) (   ))
                 (   (   ) (   )))]))
  2. 完成したコードをDrSchemeで実行する。
  3. (sum-coins (list 23 0 5 7) (list 1 5 10 25)) の結果が248になることを確認する。
  4. 別の入力値でも動作を確認する。

ヒント

リストが空かどうかの判定には (= a empty) ではなく (empty? a) を使用すること。= は数値の比較用であり、リスト同士の比較には使えない。リストの先頭要素は first 関数、残りのリストは rest 関数で取得する。sum-coinsは2つのリストを同時に処理するため、両方のリストに対してfirstとrestを適用する必要がある。

課題3:繰り返し回数の比較

目的

同じ問題を解く2つの異なるアルゴリズムの効率を比較し、アルゴリズムの選択が重要であることを理解する。

手順

  1. 例題6のユークリッドの互除法による my-gcd を用意する。
  2. 別方式の 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)))
  3. 同じ入力(例:180と32、210と66など)で両方の関数を実行する。
  4. stepperを使って、それぞれの関数が何回繰り返されるかを数える。
  5. 繰り返し回数の違いについて、自分なりの考察をまとめる。

ヒント

gcd-structural の方式は「nとmの小さい方から1ずつ減らしながら、両方を割り切れる数を探す」というものである。この方式とユークリッドの互除法で、繰り返し回数がどの程度異なるかに注目する。特に入力値が大きい場合の違いを観察するとよい。考察では、なぜ一方が効率的なのかを、アルゴリズムの仕組みから説明することを試みる。

課題4:エラトステネスのふるい

目的

エラトステネスのふるい(Sieve of Eratosthenes)の原理を理解し、100以下の素数を求めるプログラムを作成する。

手順

  1. エラトステネスのふるいの原理を理解する。
    • 2から始めて、2の倍数(4, 6, 8, ...)を消す
    • 次に残っている数(3)の倍数を消す
    • 次に残っている数(5)の倍数を消す
    • これを√100 = 10を超えるまで繰り返す
  2. 以下の機能を持つ関数を設計する。
    • 2からnまでの整数のリストを生成する関数
    • リストからある数の倍数を取り除く関数
    • ふるいの処理を繰り返す関数
  3. 各関数を実装し、テストする。
  4. 最終的に100以下の素数のリストが得られることを確認する。

ヒント

11の倍数を消す必要がない理由は、100以下の11の倍数で11より大きいもの(22, 33, ...)は、すでに2や3の倍数として消されているからである。したがって、√100 = 10までの素数の倍数を消せば十分である。プログラムの構造として、「リストの先頭の数の倍数を消し、残りのリストに対して同じ処理を繰り返す」という再帰的な考え方が有効である。

正解例

以下にエラトステネスのふるいを実装したプログラム例を示す。このプログラムは3つの関数から構成される。

;; 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)))

実行方法

上記のコードを定義用ウインドウに入力し、Executeボタンを押した後、実行用ウインドウで以下を実行する。

(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)

各関数の説明

関数名 機能 終了条件
range startからendまでの整数のリストを生成する start > end のとき空リストを返す
remove-multiples リストからnの倍数(n自身を除く)を取り除く リストが空のとき空リストを返す
sieve リストの先頭を素数として残し、その倍数を消す処理を繰り返す リストが空のとき空リストを返す
primes-up-to 2からnまでのリストにふるいを適用する (sieveに委譲)

補足:この実装では、sieve関数がリストの先頭要素の倍数を消しながら再帰的に処理を進める。リストの先頭要素は必ず素数である(それより小さい数の倍数としてすでに消されているため)。この性質を利用して、先頭要素を素数リストに追加し、残りのリストに対して同じ処理を繰り返す。

sp-13. 数値微分と数値積分

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/KYMDLK-2022-01-26-145417

演習パート(クリックして展開)

課題1:接線の傾きの計算

目的

高階関数(関数を引数として受け取る関数)である d/dx を使用して、数値微分の基本的な使い方を習得する。

必要なソースコード

以下のコードを「定義用ウインドウ」に入力する。

;; 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)))

手順

  1. 上記のコードを「定義用ウインドウ」に入力し、Execute ボタンを押す。
  2. 「実行用ウインドウ」で以下の4つの式を順に実行する。
    (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)
  3. 各実行結果を記録する。

ヒント

d/dx 関数は、中心差分公式 (f(x+h) - f(x-h)) / (2h) を用いて接線の傾きを近似している。cos 関数は Scheme の組み込み関数として使用できる。f'(x) = cos x - x sin x であるから、理論値と比較することで近似の精度を確認できる。

演習2:刻み幅と誤差の関係(数値微分)

目的

刻み幅 h を変化させたときの近似精度の変化を観察し、数値微分における h の選択が結果に与える影響を理解する。

必要なソースコード

課題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)))

;; f2: number -> number
;; f(x) = x * cos(x)
(define (f2 x)
  (* x (cos x)))

手順

  1. 上記のコードを「定義用ウインドウ」に入力し、Execute ボタンを押す。
  2. 特定の x の値(例:x = 0.4)に対して、以下の3つの h の値で d/dx を実行する。
    (d/dx f2 0.4 0.1)
    (d/dx f2 0.4 0.01)
    (d/dx f2 0.4 0.001)
  3. 各結果を記録し、理論値 f'(0.4) = cos(0.4) - 0.4 × sin(0.4) と比較して誤差を計算する。
  4. h の値を横軸、誤差の絶対値を縦軸としたグラフを作成する。

ヒント

h を小さくすると一般に誤差は減少するが、h が極端に小さいと丸め誤差(コンピュータの数値表現の限界による誤差)の影響が現れる場合がある。グラフ作成には表計算ソフトウェア等を使用するとよい。

演習3:台形則による数値積分

目的

台形則(trapezoidal rule)を用いた数値積分の実行方法を習得し、積分の近似値と理論値を比較する。

必要なソースコード

以下のコードを「定義用ウインドウ」に入力する。

;; 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)))

手順

  1. 上記のコードを「定義用ウインドウ」に入力し、Execute ボタンを押す。
  2. 「実行用ウインドウ」で以下を実行する。
    (trapezoid f2 0 1 1000)
  3. 比較のため、以下を実行して log 2 の値を確認する。
    (log 2)
  4. 両者の差を計算し、近似の精度を確認する。

ヒント

trapezoid 関数の引数は順に「関数 f」「積分区間の下端 a」「積分区間の上端 b」「分割数 n」である。分割数 n を大きくすると一般に精度は向上するが、計算時間も増加する。台形則の公式を確認しておくと、プログラムの動作が理解しやすい。

演習4:分割数と誤差の関係(数値積分)

目的

分割数 n を変化させたときの近似精度の変化を観察し、台形則の誤差が O(1/n²) であることを確認する。

必要なソースコード

演習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)))

手順

  1. 上記のコードを「定義用ウインドウ」に入力し、Execute ボタンを押す。
  2. 以下の6通りの分割数で trapezoid を実行する。
    (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)
  3. 各結果と (log 2) の値との差(誤差)を計算する。
  4. 分割数 n を横軸、誤差の絶対値を縦軸としたグラフを作成する。
  5. 誤差が c/n²(c は定数)の関係に従っているか確認する。n を2倍にしたとき誤差が約1/4になっていれば、この関係が成り立っている。

ヒント

対数グラフ(両対数プロット)を使用すると、S_n - I ≈ c/n² の関係が直線として現れ、確認が容易になる。傾き -2 の直線になれば、誤差が n の2乗に反比例していることを示す。

演習4(グラフィックス):関数と接線のグラフ表示

目的

d/dx 関数を利用したグラフィックスプログラムを実行し、関数のグラフと指定した点における接線を視覚的に確認する。

必要なソースコード

以下のコード全体を「定義用ウインドウ」に入力する。

;; 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)

手順

  1. 上記のコード全体を「定義用ウインドウ」に入力する。
  2. Execute ボタンを押してプログラムを実行する。グラフウインドウが開き、f2(x) = x² - 2 のグラフと x = 0.5 における接線が表示される。
  3. f2 の定義を別の関数に変更する。例えば以下のいずれかに変更する。
    (define (f2 x) (sin x))
    または
    (define (f2 x) (* x (cos x)))
  4. 再度 Execute ボタンを押し、変更後のグラフと接線を確認する。
  5. 必要に応じて PX の値(接線を描く x 座標)を変更し、異なる点での接線を観察する。

ヒント

プログラム中の定数 X0, Y0 は原点の位置、SX, SY は1グリッドのピクセルサイズを表す。PX は接線を描画する x 座標である。これらの値を変更することで、表示範囲や接線の位置を調整できる。プログラムの構造を理解するには、samples 関数がグラフ上の点列を生成し、draw-polyline 関数がそれらを線で結ぶという流れを把握するとよい。

sp-14. ニュートン法

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/KE8XJZ-2022-01-26-145344

演習パート(クリックして展開)

例題1:ニュートン法による非線形方程式の解

目的

ニュートン法(Newton法)のプログラムを実装し、非線形方程式 f(x) = x³ − 6x² + 11x − 6 = 0 の近似解を求める方法を理解する。

手順

  1. DrSchemeを起動し、言語設定を「Intermediate Student」に変更する(Language → Choose Language → Intermediate Student → Executeボタン)
  2. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
;; 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))
  1. 実行用ウインドウで以下を実行する
(newton f2 #i0 0.00001 10000)
  1. 結果として約1に近い値(#i0.9999987646860998)が得られることを確認する
  2. 初期値を変えて以下を実行し、異なる解(約3)が得られることを確認する
(newton f2 #i10 0.00001 10000)

ヒント

  • #i は近似計算(浮動小数点数)を意味する。#i なしで実行すると有理数で計算され、結果が膨大な桁数になる場合がある
  • 初期近似値によって収束する解が変わる。f(x) = x³ − 6x² + 11x − 6 = (x − 1)(x − 2)(x − 3) であり、解は1, 2, 3の3つ存在する
  • newtonは高階関数(関数を引数として受け取る関数)である

課題1:立方根を求めるプログラム

目的

Newton法を応用して、任意の数aの立方根(∛a)を求めるプログラムを作成する。

手順

  1. 立方根を求める問題を方程式として定式化する:x³ − a = 0 の解xが ∛a である
  2. Schemeで関数fを定義する。aを具体的な数値(例:8)に置き換えて記述する
;; 例:8の立方根を求める場合
(define (f-cube x)
  (- (* x x x) 8))
  1. newton関数を用いて解を求める
(newton f-cube #i1 0.00001 10000)
  1. 負の数(例:a = −8)に対しても実行し、負の立方根(−2)が正しく求まることを確認する
;; -8の立方根を求める場合
(define (f-cube-neg x)
  (- (* x x x) -8))

(newton f-cube-neg #i-1 0.00001 10000)
  1. deltaの値を0.1, 0.01, 0.001, 0.00001などに変えて実行し、得られる解の精度の変化を記録する

ヒント

  • 多項式の記述方法として、x³ − a は (- (* x x x) a) と書ける
  • 負の数の立方根は実数として存在する(虚数にはならない)
  • deltaを大きくすると計算は速いが精度が低い。deltaを小さくすると精度は上がるが計算回数が増える

課題2:ニュートン法での収束

目的

f(x) = x² − 5 に対するニュートン法の収束過程を視覚的に理解し、反復公式の幾何学的意味を確認する。

手順

  1. f(x) = x² − 5 のグラフを手書きで描く(放物線、x軸との交点は ±√5 ≈ ±2.236)
  2. Schemeで関数fを定義する
(define (f x)
  (- (* x x) 5))
  1. 初期値 x₀ = 1 としてニュートン法を実行し、x₁, x₂, x₃ の値を記録する
(newton f #i1 0.00001 10000)
  1. プログラムの途中経過から x₁, x₂, x₃ の値を読み取る
  2. グラフ上に点 (x₀, f(x₀)) をプロットし、その点での接線を描く
  3. 接線とx軸の交点が x₁ であることを確認し、同様に x₂, x₃ まで図示する

ヒント

  • x₀ = 1 から始めると、x₁ = 3.0, x₂ ≈ 2.333, x₃ ≈ 2.238 と √5 に収束していく
  • グラフを描く際、y軸のスケールに注意する。f(1) = −4, f(3) = 4 程度の範囲を含めると見やすい
  • 接線の傾きは f'(x) = 2x で計算できる

課題3:(x − 1)⁴(x − 2) = 0 を解く

目的

重解を持つ方程式に対するニュートン法の振る舞いを観察し、初期近似値と収束先の関係を理解する。

手順

  1. 関数 f(x) = (x − 1)⁴(x − 2) をSchemeで定義する
;; (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))
  1. 様々な初期近似値(例:0, 0.5, 1.5, 3, 10)でnewtonを実行する
(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)
  1. 各初期値に対して得られた解を記録し、どの解(1または2)に収束したかを整理する
  2. 解1は4重解、解2は単解であることに注意し、収束の速さの違いを観察する

ヒント

  • 多項式の展開:(x − 1)⁴(x − 2) = x⁵ − 6x⁴ + 14x³ − 16x² + 9x − 2
  • 重解の近くでは導関数も0に近づくため、収束が遅くなる傾向がある
  • 初期値が1と2の中間付近(例:1.5)では、どちらに収束するか予測しにくい

演習4:f(x) = tan⁻¹x + 0.3x を解く

目的

ニュートン法が収束しない場合の条件を理解し、その原因をグラフを用いて説明できるようになる。

手順

  1. 関数 f(x) = tan⁻¹x + 0.3x をSchemeで定義する(tan⁻¹ は atan を使用)
(define (f4 x)
  (+ (atan x) (* 0.3 x)))
  1. 様々な初期近似値で実行し、収束する場合と収束しない場合を記録する
(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)
  1. f(x) のグラフを描き、解の位置を確認する
  2. 収束しない初期値について、接線とx軸の交点を順次図示し、発散の様子を視覚化する
  3. f'(x) = 1/(1 + x²) + 0.3 を計算し、導関数の符号変化と収束失敗の関係を考察する

ヒント

  • f'(x) = 0 となる点では接線が水平になり、x軸との交点が存在しない(または無限遠に発散する)
  • tan⁻¹x は −π/2 から π/2 の範囲の値を取る。f(x) = 0 の解は x = 0 のみ
  • 初期値が解から離れすぎると、反復が振動したり発散したりする可能性がある
  • 収束しない理由として、f'(x) の符号変化や変曲点の存在が考えられる

注意:atanの使用可否は処理系のバージョンによって異なる場合がある。エラーが発生した場合は、使用しているDrSchemeのドキュメントを確認すること。

例題2:区間二分法による非線形方程式の解

目的

区間二分法(half-interval method)の原理を理解し、f(x) = x² − 2 = 0 の解(√2)を求めるプログラムを実装する。

手順

  1. 区間二分法の原理を確認する:f(a) < 0 かつ f(b) > 0 ならば、[a, b] の間に解が存在する
  2. 定義用ウインドウに以下のコードを入力し、Executeボタンを押す
(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))])]))
  1. 実行用ウインドウで以下の3つを実行し、結果を比較する
(half-interval 0 2)    ;; 正しい解(約1.414)が得られる
(half-interval -2 0)   ;; 正しくない結果になる
(half-interval 2 4)    ;; 正しくない結果になる
  1. 正しい解が得られない場合の原因を考察する

ヒント

  • 区間二分法が正しく動作する条件:f(a) と f(b) の符号が異なること
  • (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:区間二分法での繰り返し処理

目的

DrSchemeのステップ実行機能を用いて、区間二分法の処理過程を詳細に追跡し、再帰呼び出しの動作を理解する。

手順

  1. 定義用ウインドウに以下のコードを入力する(例題2のコードに (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)
  1. ExecuteボタンではなくStepボタンを押す
  2. Nextボタンを繰り返し押し、各ステップでの変数の値を確認する
  3. 以下の過程を追跡する
    • (half-interval 0 2) → (half-interval 1 2) → (half-interval 1 3/2) → ...
  4. a と b の値が徐々に近づき、区間が縮小していく様子を観察する

ヒント

  • ステップ実行では、式の評価順序が視覚的に確認できる
  • middle が計算されるたびに区間が半分になることを確認する
  • 有理数(分数)で計算されるため、1, 3/2, 5/4 などの値が表示される

sp-15. リスト処理とクイックソート

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/5MVX95-2022-01-26-145317

演習パート(クリックして展開)

環境設定

本演習ではDrSchemeを使用する。起動後、言語設定を「Intermediate Student」に変更すること。

設定手順
  1. DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
  2. メニューから Language → Choose Language を選択する
  3. 「Intermediate Student」を選択する
  4. Executeボタンを押す

ステッパ機能について

DrSchemeのステッパ(Stepper)機能は、プログラムの実行過程を1ステップずつ確認できるデバッグツールである。再帰関数の動作を理解する際に有用である。

ステッパの使用手順
  1. 定義用ウインドウにプログラムを入力する
  2. 実行用ウインドウに評価したい式を入力する
  3. メニューから Language → Choose Language を選択し、「Intermediate Student with lambda」または「Intermediate Student」が選択されていることを確認する
  4. Stepボタン(または Step >> ボタン)を押す
  5. ステッパウインドウが開き、式の評価過程が表示される
  6. 「Step」ボタンを押すたびに、評価が1ステップ進む
  7. 各ステップで、どの部分式が評価され、どのような値に置き換わるかを観察する
ヒント
ステッパを使用すると、再帰呼び出しがどのように展開され、どのように値が返されるかを視覚的に追跡できる。例題3でインサーションソートの繰り返し回数を数える際に活用する。

例題1:要素の挿入

目的

ソート済みリスト(降順に並んだリスト)に新しい要素を適切な位置に挿入する再帰関数の構造を理解する。

手順

  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)))])]))
  1. Executeボタンを押して定義を読み込む。
  2. 実行用ウインドウで以下を実行する。
(insert 40 (list 80 21 10 7 5 4))
  1. 出力結果が (list 80 40 21 10 7 5 4) となることを確認する。
  2. 異なる値でも試し、挿入位置の変化を観察する。
(insert 100 (list 80 21 10))
(insert 1 (list 80 21 10))
ヒント
insert関数は3つの場合に分岐する。リストが空の場合、先頭要素がn以下の場合、先頭要素がnより大きい場合である。再帰呼び出しは3番目の場合にのみ発生する。

例題2:インサーションソート

目的

例題1のinsert関数を活用し、リスト全体を降順にソートするインサーションソート(挿入ソート)のアルゴリズムを実装する。

手順

  1. 定義用ウインドウに、例題1のinsert関数に加えて以下のコードを入力する。
;; 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)))])]))
  1. Executeボタンを押す。
  2. 実行用ウインドウで以下を実行する。
(sort (list 3 5 1 4))
  1. 出力結果が (list 5 4 3 1) となることを確認する。
  2. 要素数の異なるリストでも実行し、動作を確認する。
(sort (list 10 2 8 4 6))
(sort (list 1))
(sort empty)
ヒント
sort関数は「リストのrestをソートした結果に、first要素を挿入する」という再帰構造を持つ。空リストに到達したときが終了条件であり、そこから逆順に要素が挿入されていく。

例題3:インサーションソートでの繰り返し回数

目的

再帰関数の実行回数とデータサイズの関係を分析し、アルゴリズムの計算量(時間計算量)について理解する。

手順

  1. 例題2のプログラムを定義用ウインドウに入力し、Executeボタンを押す。
  2. 実行用ウインドウに以下の式を入力する。
(sort (list 80 30 50))
  1. Stepボタンを押してステッパを起動する。
  2. ステッパウインドウで「Step」ボタンを繰り返し押し、以下の点を観察する。
    • sort関数が何回呼び出されるか
    • insert関数が何回呼び出されるか
    • 各insert呼び出しの中で、再帰が何回発生するか
  3. リストの要素数をnとしたとき、以下の回数を計算で確認する。
    • sort関数の実行回数:n+1回
    • insert関数の実行回数:最小n回、最大n(n+1)/2回、平均n²/4+3n/4回
  4. nが大きくなったとき、計算時間がn²に比例することを理解する。
ヒント
insert関数の呼び出し回数がデータの並び順に依存することが分かる。平均的な場合、計算量はO(n²)となる。

例題4:append

目的

Schemeの組み込み関数appendの動作を確認し、複数のリストを連結する方法を理解する。

手順

  1. 実行用ウインドウで以下の式を順に実行する。
(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))
  1. 各実行結果を確認する。
    • 1番目:2つのリストが連結される
    • 2番目:3つのリストが連結される
    • 3番目:エラーが発生する(リストでない要素は連結できない)
ヒント
appendはリスト同士の連結にのみ使用できる。単一の要素を追加する場合はconsを使用する。この関数は例題7のクイックソートで重要な役割を果たす。

例題5:大きな要素の選択

目的

リストから条件を満たす要素のみを抽出するフィルタリング処理を実装する。ここではthreshold以上の要素を選択する。

手順

  1. 定義用ウインドウに以下のコードを入力する。
;; 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)])]))
  1. Executeボタンを押す。
  2. 実行用ウインドウで以下を実行する。
(larger-items (list 1 2 3 10 11 12 4 5 6) 6)
  1. 出力結果が (list 10 11 12 6) となることを確認する。
  2. 異なるthreshold値でも試してみる。
(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:小さな要素の選択

目的

例題5と対になる処理として、thresholdより小さい要素を選択する関数を実装する。

手順

  1. 定義用ウインドウに以下のコードを入力する。
;; 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)])]))
  1. Executeボタンを押す。
  2. 実行用ウインドウで以下を実行する。
(smaller-items (list 1 2 3 10 11 12 4 5 6) 6)
  1. 出力結果が (list 1 2 3 4 5) となることを確認する。
ヒント
larger-itemsとの違いは比較演算子のみである。>=<に変わっている。この2つの関数を組み合わせることで、クイックソートにおけるピボットによる分割が実現される。

例題7:クイックソート

目的

分割統治法(divide and conquer)に基づくクイックソートアルゴリズムを実装する。例題4-6で作成した関数を組み合わせて使用する。

手順

  1. 定義用ウインドウに、例題5のlarger-items、例題6のsmaller-itemsに加えて以下のコードを入力する。
;; 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))))]))
  1. Executeボタンを押す。
  2. 実行用ウインドウで以下を実行する。
(quick-sort (list 4 6 2))
(quick-sort (list 8 10 6 3 5))
  1. それぞれ (list 2 4 6)(list 3 5 6 8 10) となることを確認する。
ヒント
クイックソートの動作は以下の通りである。リストの先頭要素をピボット(pivot、基準値)として選択し、残りの要素をピボットより小さいグループと大きいグループに分割する。各グループを再帰的にソートし、最後にappendで「小さいグループ+ピボット+大きいグループ」の順に連結する。

例題8:クイックソート(構造体)

目的

数値リストではなく、構造体のリストに対してクイックソートを適用する方法を学ぶ。文字列比較関数を用いて名前順にソートする。

手順

  1. 定義用ウインドウに以下のコードを入力する。
;; 構造体の定義
(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)))))]))
  1. Executeボタンを押す。
  2. 実行用ウインドウで以下を実行する。
(quick-sort book)
  1. 名前のアルファベット順(Bill → Ken → Mike)にソートされた結果を確認する。
ヒント
数値の比較演算子(>=<)が文字列比較関数(string>=?string<?)に置き換わっている点に注目する。構造体のフィールドにアクセスするにはaddress-record-nameのようなアクセサ関数を使用する。

課題1:クイックソートの実行過程

目的

クイックソートの再帰的な実行過程を追跡し、分割統治法の動作原理を説明できるようになる。

手順

  1. (quick-sort (list 8 10 6 3 5)) の実行過程を分析する。
  2. 以下の観点で説明をまとめる。
    • 最初のピボットは何か
    • ピボットによってリストがどのように分割されるか
    • 分割された各部分がどのように再帰的に処理されるか
    • 最終的にどのように結果が組み立てられるか
  3. 数行程度で説明を記述する。
ヒント
ピボットとして8が選ばれ、(list 10 6 3 5)が「8より小さい」と「8以上」に分割される。この過程を追跡することで、分割統治法の動作が理解できる。

課題2:住所録構造体のクイックソート

目的

例題8のプログラムを実行し、構造体リストのソート結果を確認する。

手順

  1. 例題8のコードを定義用ウインドウに入力し、Executeボタンを押す。
  2. 実行用ウインドウで (quick-sort book) を実行する。
  3. 実行結果を報告する。
ヒント
結果はaddress-record構造体のリストとして出力される。名前フィールドがアルファベット順(昇順)に並んでいることを確認する。

課題3:リスト検索プログラム

目的

リストから特定の要素を検索する関数を作成する。さらに、ソート済みリストの性質を活用した効率的な検索を考える。

課題3-1:基本的な検索

  1. 数nがリストa-listに存在するかを判定する関数searchを設計する。
  2. 以下の構造を参考に実装する。
    • 終了条件:リストが空のとき → false
    • 先頭要素がnと等しいとき → true
    • それ以外 → restに対して再帰的に検索
  3. 実装した関数をテストし、結果を報告する。

課題3-2:ソート済みリストの検索

  1. ソート済みリスト(昇順または降順)に対する検索関数search-sortedを設計する。
  2. ソート済みという性質を利用して、不要な検索を打ち切る条件を追加する。
  3. 実装した関数をテストし、結果を報告する。
ヒント
課題3-1は例題5、6のフィルタリング関数と類似した構造で実装できる。課題3-2では、ソート順によって「これ以上検索しても見つからない」ことが判定できる場合がある。例えば昇順リストで先頭要素がnより大きければ、残りの要素はすべてnより大きいため検索を終了できる。

課題3 解答例

注意
以下は解答例である。まずは自分で実装を試みてから参照すること。

課題3-1:search関数

;; 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

課題3-2:search-sorted関数(昇順リスト版)

;; 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

課題3-2:search-sorted関数(降順リスト版)

;; 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

演習パート(クリックして展開)

演習環境の準備

目的:DrScheme環境をFull Schemeモードで起動し、演習を実行できる状態にする。
手順
  1. DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
  2. メニューから Language → Choose Language → Full Scheme → OK を選択する
  3. Executeボタンをクリックして設定を反映する
注意事項
  • Full Schemeモードではemptyが使用できないため、空リストは'()と記述する
  • リストの表示形式が変わり、(list 15 8 6)(15 8 6)と表示される

例題1:ペア

目的:consを用いてペア(pair)を生成し、car・cdrでペアの構成要素を取り出す方法を理解する。
手順
  1. 定義用ウインドウに以下のコードを入力する
(define x (cons 'apple 100))
(define y (cons 'orange 60))
(define z (cons 'banana 80))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を順に入力し、結果を確認する
x
(car x)
(cdr x)
  1. 変数y、zについても同様にcar、cdrを試す
ヒント
  • ペア(pair)とは2つの構成部分を持つデータ構造である
  • carはペアの1番目の要素、cdrは2番目の要素を返す
  • (cons 'apple 100)(apple . 100)と表示される(ドット対表記)

例題2:リストの変数定義

目的:consを入れ子にしてリストを構築し、リストがペアの連鎖として実装されていることを理解する。
手順
  1. 定義用ウインドウに以下のコードを入力する
(define A (cons 15 (cons 8 (cons 6 '()))))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を順に入力し、結果を確認する
A
(car A)
(cdr A)
  1. (cdr A)の結果に対してさらにcar、cdrを適用し、リストの構造を探索する
ヒント
  • '()は空リスト(empty list)を表す
  • リストは末尾が空リストであるペアの並びである
  • (car A)はリストの先頭要素(15)を返す
  • (cdr A)はリストから先頭要素を除いた残り(8 6)を返す
  • 箱とポインタ記法(box-and-pointer notation)を紙に描くと構造を理解しやすい

例題3:ペアから構成されたペア

目的:consを入れ子にして、ペアを要素とするペアを作成する方法を理解する。
手順
  1. 定義用ウインドウに以下のコードを入力する
(define a (cons (cons 1 2) (cons 3 4)))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を順に入力し、結果を確認する
a
(car a)
(cdr a)
  1. 表示結果を箱とポインタ記法で図示してみる
ヒント
  • (car a)は左側のペア(1 . 2)を返す
  • (cdr a)は右側のペア(3 . 4)を返す
  • 結果は((1 . 2) 3 . 4)と表示される

例題4:carとcdrの組み合わせ

目的:car・cdrを組み合わせて、入れ子構造の任意の要素にアクセスする方法を習得する。
手順
  1. 定義用ウインドウに以下のコードを入力する(例題3と同じ)
(define a (cons (cons 1 2) (cons 3 4)))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を順に入力し、結果を確認する
(car (car a))
(cdr (car a))
(car (cdr a))
(cdr (cdr a))
  1. 省略記法で同じ操作を試す
(caar a)
(cdar a)
(cadr a)
(cddr a)
  1. 各操作がどの要素(1, 2, 3, 4)を返すか確認する
ヒント
  • (car (car a))(caar a)は同じ結果を返す
  • 省略記法は内側から読む:caarは「carしてからcar」
  • car/cdrは最大4つまで組み合わせ可能(例:cdddr

例題5:consとlistの組み合わせ(1)

目的:consとlistを組み合わせて、リストを要素に持つペアを作成する。
手順
  1. 定義用ウインドウに以下のコードを入力する
(define x (cons (list 1 2 3) (list 4 5 6)))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を確認する
x
(car x)
(cdr x)
  1. 構造を確認する
ヒント
  • consは2つの引数からペアを作る
  • listは任意個数の引数からリストを作る
  • (car x)はリスト(1 2 3)を返す
  • (cdr x)はリスト(4 5 6)を返す

例題6:consとlistの組み合わせ(2)

目的:ペアを要素とするリストを作成する方法を理解する。
手順
  1. 定義用ウインドウに以下のコードを入力する
(define x (list (cons 'x 'y) (cons 'a 'b) (cons 10 20)))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を確認する
x
(car x)
(cadr x)
(caddr x)
  1. 各ペアの要素にアクセスする方法を考える
ヒント
  • listの各要素がcons(ペア)になっている
  • 連想リスト(association list)の基礎となる構造である

例題7:listとlistの組み合わせ

目的:リストのリスト(二次元構造)を作成し、要素へのアクセス方法を理解する。
手順
  1. 定義用ウインドウに以下のコードを入力する
(define x (list (list 1 2) (list 3 4)))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を確認する
x
(car x)
(cadr x)
(car (car x))
(cadr (cadr x))
  1. 各要素(1, 2, 3, 4)へのアクセス方法を整理する
ヒント
  • 二次元配列のような構造をリストのリストで表現できる
  • (car x)は最初のリスト(1 2)を返す

例題8:二分探索木

目的:define-structを用いて二分探索木(binary search tree)のデータ構造を定義し、具体的な木を構築する。
補足:二分探索木とは

二分探索木は二分木の一種であり、データの配置に以下の規則がある。

  • 左側のすべての子は親より小さい
  • 右側のすべての子は親より大きい

この規則により、効率的なデータ探索が可能となる。

手順
  1. 定義用ウインドウに以下のコードを入力する
(define-struct node (value left right))
  1. 続けて、二分探索木を構築するコードを入力する
(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)))))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を確認する
(node-value tree)
(node-left tree)
(node-right tree)
(node-value (node-left tree))
ヒント
  • define-structは新しいデータ型を定義する
  • (define-struct node (value left right))により、make-node、node-value、node-left、node-rightが使用可能になる
  • falseは子が存在しないことを示す
  • 二分探索木では、左の子は親より小さく、右の子は親より大きい

例題9:二分探索木による探索

目的:二分探索木を探索する再帰関数を作成し、動作を確認する。
手順
  1. 例題8のコードに続けて、探索関数を定義する
(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]))
  1. Executeボタンをクリックする
  2. 実行用ウインドウで以下を確認する
(search 40 tree)
(search 35 tree)
(search 100 tree)
(search 13 tree)
  1. 各探索がどのような経路をたどるか追跡する
ヒント
  • 探索キーが節点の値より小さければ左の部分木を探索する
  • 探索キーが節点の値より大きければ右の部分木を探索する
  • 一致すればtrue、末端(false)に到達すればfalseを返す

課題1

目的:list、cons、car、cdr、cadr、cddrの動作を実験的に確認し、結果を報告する。
手順
  1. DrSchemeをFull Schemeモードで起動する
  2. 以下の式を実行用ウインドウで入力し、結果を記録する
(list (cons 1 2) (cons 3 4))
(list (list 1 2) (list 3 4))
  1. 上記2つの結果それぞれについて、以下の操作を実行し結果を記録する
(car (list ...))
(cdr (list ...))
(cadr (list ...))
(cddr (list ...))
  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))

sp-17. フィボナッチ数

資料(スライド): [PDF], [パワーポイント], [HTML]

ドクセルの URL: https://www.docswell.com/s/6674398749/ZJ84EZ-2022-01-26-145136

演習パート(クリックして展開)

例題1:フィボナッチ数(木構造再帰プロセス)

目的

生成的再帰(generative recursion)の一形態である木構造再帰プロセス(tree recursion)を用いて、フィボナッチ数を計算するプログラムの動作原理を理解する。

手順

  1. DrSchemeを起動する(プログラム → PLT Scheme → DrScheme)
  2. 言語設定を「Intermediate Student」に変更する(Language → Choose Language → Intermediate Student → Executeボタン)
  3. 定義用ウインドウに以下のコードを入力する
(define (fibo n)
  (cond
    [(= n 0) 0]
    [(= n 1) 1]
    [else (+ (fibo (- n 1)) (fibo (- n 2)))]))
  1. Executeボタンを押して定義を読み込む
  2. 実行用ウインドウで以下を順に実行し、結果を確認する
(fibo 5)
(fibo 6)
(fibo 7)
  1. (fibo 4)がどのように展開されて3という結果になるかを追跡する
  2. 木構造図を見て、関数呼び出しがどのような構造で発生するかを確認する

ヒント

フィボナッチ数列の定義は f₀ = 0、f₁ = 1、fₙ = fₙ₋₁ + fₙ₋₂(n > 1)である。プログラム中のcond式は、この数学的定義をそのまま反映している。(fibo 4)の計算では(fibo 2)が2回、(fibo 1)が3回、(fibo 0)が2回呼び出される点に注目すると、木構造再帰の特徴である「同じ計算の重複」が理解しやすい。

例題2:反復的プロセスでのフィボナッチ数

目的

反復的プロセス(iterative process)を用いてフィボナッチ数を計算する方法を学び、例題1の木構造再帰との違いを理解する。

手順

  1. DrSchemeが起動していない場合は起動し、言語設定が「Intermediate Student」であることを確認する
  2. 定義用ウインドウに以下のコードを入力する(例題1のコードは削除または上書きしてよい)
(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))]))
  1. Executeボタンを押して定義を読み込む
  2. 実行用ウインドウで以下を順に実行し、例題1と同じ結果が得られることを確認する
(fibo 5)
(fibo 6)
(fibo 7)
  1. (fibo 4)の計算過程でa、b、counterの値がどのように変化するかを追跡する
  2. 例題1と例題2の計算過程を比較し、繰り返し回数の違いを観察する

ヒント

反復的プロセスでは、変数a、bに「直前の2つのフィボナッチ数」を保持しながら、counterが0になるまで更新を繰り返す。この方式では同じ計算を重複して行うことがないため、例題1より効率的である。「a ← a + b、b ← a」という更新規則が核心部分である。

課題1:性能比較

目的

木構造再帰プロセスと反復的プロセスの計算効率の違いを、実際の実行回数を計測することで定量的に把握する。

手順

  1. 例題1のプログラムについて、(fibo 6)を計算するためにfibo関数が何回呼び出されるかを数える
    • 木構造図を参考に、(fibo 6)の呼び出し構造を紙に描いて数えるとよい
  2. 例題2のプログラムについて、(fibo 6)を計算するためにfibo-iterate関数が何回呼び出されるかを数える
    • 計算過程を参考に、counterが0になるまでの呼び出し回数を追跡する
  3. DrSchemeのstepperを使用して、置き換えが起こった回数を計測する
    • 例題1と例題2それぞれについて、(fibo 3)(fibo 4)(fibo 5)(fibo 6)を実行する
    • 各実行において、最終結果が得られるまでに「next」ボタンを押した回数を記録する
  4. 計測結果を表にまとめ、nが増加したときの回数の増加パターンを考察する

ヒント

stepperはプログラムの実行過程を1ステップずつ可視化する機能である。例題1では指数関数的に呼び出し回数が増加し、例題2では線形に増加する傾向が観察されるはずである。この違いがなぜ生じるかを、各プログラムの構造と関連付けて考えるとよい。「計算が冗長」という指摘と、自分の計測結果を照らし合わせること。

補足説明

フィボナッチ数列について

フィボナッチ数列は、0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... と続く数列である。各項は直前の2つの項の和として定義される。この数列は自然界の様々な現象(植物の葉の配置、貝殻の螺旋など)に現れることが知られている。

木構造再帰と反復的プロセスの違い

木構造再帰プロセスでは、関数が自身を複数回呼び出すため、呼び出しの構造が木のような形になる。一方、反復的プロセスでは、状態を変数に保持しながら順次更新するため、呼び出しは直線的になる。この構造の違いが計算効率の差を生む原因である。