以下のedXコースSystematic Program Design - Part 1の日本語解説②。
①はこちら:
内容に問題があればご連絡ください。
HtDF - 関数デザインレシピについて
関数を書く際の手順となるテンプレート。これに沿って書くことで、可読性や変更の容易な良いコードとなる。
正確に言えば、HtDFは関数を書く際の段階を分割することで、複雑な関数を定義する際の助けとなる。一方で、シンプルなプログラムで使用すると、かえって手順が増えて煩雑になってしまうこともある。
以下の各段階では、各段階が次の段階を書くにあたってのヘルプになっていることに注目。しかし、HtDFはガチガチのウォーターフォールではなく、場合によっては前の段階が不明なために、わかりやすい後の段階を先に記述したり、後になって前の部分を修正したりもしてよい。
- Signature, purpose and stub.
- Define examples, wrap each in check-expect.
- Template and inventory.
- Code the function body.
- Test and debug until correct
ここでの例には、以下のdouble-starter.rkt
を使用する。
問題: 数値を受け取り、その2倍の数値を生成する関数を設計します。
関数をdouble
として呼び出します。 HtDFレシピに従い、コメント化されたstubとtemplateを残します。
1. Signature, purpose, stub
Signature: Type … —> Type
関数が受け取る値のタイプと、返す値のタイプを宣言(タイプ名は常にキャピタライズされる)。これは可能な限り詳細な方がよく、例えばある関数がFloat(小数)を扱わないなら、Number
とするよりもNatural
の方が相応しい。
;; Number -> Number
Purpose: 関数の処理目的
関数が何の値を受け取るかという点から、何を生成するか1行で説明する。上記のsignature以上の説明をしている必要がある。例えばこの例ではconsume number and produce number
だとsignatureの記述と同等の説明になるため、内容不足である。
;; produce 2 times the given number
また、Booleanを扱う場合には、どのような場合にtrue/falseとなるのかが明示されている方がよい。
;; produce true if img is tall (height > width)
Stub: 関数の設計図
これから書く関数の設計図のようなもの。後々コメントアウトするか削除する。stubはfunction definitionであり、以下の要素を持つ:
- 正しい関数名
- 正しい数の引数
- 正しいタイプのダミー結果(タイプが合っていればなんでもよい)
(define (double n) 0) ;this is the stub
2. ExamplesとTests
動作の例であり、関数の役割を理解する助けとなる。
多くの場合(check-expect (function-name argument) expected-expression)
で囲むことで、完成した関数の単体テストとしても機能する。
(check-expect (double 3) 6)
(check-expect (double 4.2) 8.4)
既に作成したstubとcheck-expectが揃った状態でDrRacketをRunすると、テスト結果は失敗してもテスト自体はrunするはずである。この時点でRunに失敗するなどのエラーがでないかどうかを確認する。 この確認ができた時点で、stubはコメントアウトしてよい。
いくつテストするべき?
今回の例では、Number
がInteger
ではなくあらゆる数値であることを示すために、小数値も例に使用している。
また条件分岐がある場合、そのすべてのケースがテストされるべきである。RacketでRunすれば、評価されなかった部分のコードがハイライトされる。すべてのコードがテストされ、このようなハイライトの部分が残らないようなテストを行うべきである。
その際、boundary conditionやcorner caseとも呼ばれる、条件が変化するしきいとなる値にも注意(例えばpurposeがproduce true if img is tall (height > width)であれば、height = width
となる場合のこと)。
条件を思いついた場合、即座にpurposeやテストに書き加えておく。上の図でハイライトされているようにテストが行われていない箇所があれば、加えてテストする。
3. Inventory - Template & Constants
Templateは、正しい関数名と引数を持ち、body(関数の内容、以下の(... n)
部分)が関数の概形となる。body部分は後々詳細なものを扱うが、今のところはシンプルなものを使用している。なお…
の部分はまだ作業が必要な部分をさしている。
(define (double n) ;this is the template
(... n))
ここでbodyは「n(parameter)に対して…(何らかの処理)する」ということを説明している。 慣れるまでは、次のコーディングを始める際templateをコピペして、もとのこちらをコメントアウトして指針として残しておいてもよい。
4. Cody body
関数bodyをコーディングする。具体例であるExamplesを元に記述するとやりやすい場合が多い。
(define (double n)
(* 2 n))
5. Test and debug
コードをrunし、すべてのテストにpassするまで修正する。エラーが発生した場合は、以下の部分をチェックしてみよう:
- function definitionが間違っていないか
- テスト(check-expect)の結果が間違っていないか
- 両方とも間違っていないか
Ex: double-starter.rtk
問題: 数値を受け取り、その2倍の数値を生成する関数を設計します。関数名はdouble
とする。HtDFレシピに従い、コメント化されたstubとtemplateを残します。
;; Number -> Number
;; produce 2 times the given number
;; (define (double n) 0) ;this is the stub
(check-expect (double 3) 6)
(check-expect (double 4.2) 8.4)
;; (define (double n)
;; (... n))
(define (double n)
(* 2 n))
Ex: yell-starter.rkt
問題:「hello」などの文字列を受け取り、「!」を追加する、yell
という関数をHtDFを用いて設計します。「hello!」のような文字列を生成します。コメント化されたstubとtemplateを残します。
;; String -> String
;; add "!" to the end of s
(check-expect (yell "hello") "hello!")
(check-expect (yell "bye") (string-append "bye" "!"))
;(define (yell s) ;stub
; "a")
;(define (yell s) ;template
; (... s))
(define (yell s)
(string-append s "!"))
Ex: area-starter.rkt
問題: 正方形の一辺の長さを受け取り、正方形の面積を求めるarea
という関数をHtDFを用いて設計してください。コメント化されたstubとtemplateを残します。
;; Natural -> Natural
;; produce area of square with side length s
(check-expect (area 2) (* 2 2))
(check-expect (area 4) (* 4 4))
;(define (area s) ; stub
; 2)
;(define (area s) ; template
; (... s))
(define (area s)
(* s s))
Ex: image-area-starter.rkt
問題: 画像を受け取り、その画像の面積を生成するimage-area
と呼ばれる関数を設計します。面積は画像の幅と高さを乗算するだけで十分です。HtDFレシピに従い、コメント化されたstubとtemplateを残します。
(require 2htdp/image)
;; Image -> Natural
;; produce the area of the consumed image (width * height)
(check-expect (image-area (rectangle 3 4 "solid" "blue")) (* 3 4))
;(define (image-area img) 0) ;stub
;(define (image-area img) ;template
; (... img))
(define (image-area img)
(* (image-width img)
(image-height img)))
Ex: tall-starter.rkt
問題: 画像を使用し、画像が縦長かどうかを判断する関数を設計します。HtDFレシピに従い、コメント化されたstubとtemplateを残します。
(require 2htdp/image)
;; Image -> Boolean
Booleanを出力する場合は、purposeで何がtrueで何がfalseなのかを明示しておく。
;; produce true if img is tall (height > width)
(check-expect (tall? (rectangle 20 40 "solid" "red")) true)
(check-expect (tall? (rectangle 40 20 "solid" "red")) false)
(check-expect (tall? (square 40 "solid" "red")) false)
DrRacketではBooleanを出力するfunctionは~?
の形で表記されることが多いため、ここではそれに倣っている。
;(define (tall? img) ; stub
; false)
;(define (tall? img) ; template
; (... img))
(define (tall? img)
(> (image-height img) (image-width img)))
※なお最後のcode bodyは以下でも同じだが、出力がtrue/falseならif構文を使う必要はない。
(define (tall? img)
(if (< (image-width img)
(image-height img))
true
false))
Problem Bank
summon-starter.rkt 難易度★☆☆
魔法を生成する機能を設計します。例えば:
(summon "Firebolt") should produce "accio Firebolt"
(summon "portkey") should produce "accio portkey"
(summon "broom") should produce "accio broom"
HtDFレシピに従い、コメント化されたstubとtemplateを残します。
召喚魔法の背景については、http://harrypotter.wikia.com/wiki/Summoning_Charm を参照してください。
答え
;; String -> String
;; prepend "accio " to the start of s
(check-expect (summon "Firebolt") "accio Firebolt")
(check-expect (summon "portkey") (string-append "accio " "portkey"))
;(define (summon s) ;stub
; "")
;(define (summon s) ;template
; (... s))
(define (summon s)
(string-append "accio " s))
less-than-five-starter.rkt 難易度★☆☆
文字列を受け取り、その長さが5未満かどうかを判断する関数を設計します。HtDFレシピに従い、コメント化されたstubとtemplateを残します。
答え
;; String -> Boolean
;; produce true if length of s is less than 5
(check-expect (less-than-5? "") true)
(check-expect (less-than-5? "five") true)
(check-expect (less-than-5? "12345") false)
(check-expect (less-than-5? "eighty") false)
;(define (less-than-5? s) ;stub
; true)
;(define (less-than-5? s) ;template
; (... s))
(define (less-than-5? s)
(< (string-length s) 5))
boxify-starter.rkt 難易度★★☆
関数の設計方法(HtDF)レシピを使用して、画像の周囲にボックスを配置するように見える関数を設計します。これを行うには、画像より大きい"outline"
長方形を作成し、オーバーレイを使用してそれを画像の上に置きます。
例えば:
(boxify (ellipse 60 30 "solid" "red"))
からは以下の画像が生成されるはずです。
私たちがデザインと言うとき、それはレシピに従うことを意味することを忘れないでください。コメント化されたstubとtemplateを残します。
答え
(require 2htdp/image)
;; Image -> Image
;; Puts a box around given image. Box is 2 pixels wider and taller than given image.
;; NOTE: A solution that follows the recipe but makes the box the same width and height
;; is also good. It just doesn't look quite as nice.
(check-expect (boxify (circle 10 "solid" "red"))
(overlay (rectangle 22 22 "outline" "black")
(circle 10 "solid" "red")))
(check-expect (boxify (star 40 "solid" "gray"))
(overlay (rectangle 67 64 "outline" "black")
(star 40 "solid" "gray")))
;(define (boxify i) (circle 2 "solid" "green"))
#;
(define (boxify i)
(... i))
(define (boxify i)
(overlay (rectangle (+ (image-width i) 2)
(+ (image-height i) 2)
"outline"
"black")
i))
pluralize-stubs-starter.rkt 難易度★☆☆
あなたは関数の設計に取り組んでおり、signatureとpurposeを完了しました。
;; String -> String
;; pluralizes str by appending "s" to the end
以下のsignatureとpurposeと一致する、bodyが異なる3つのstubを作成します。
3つのstubをコメントボックスに記入してください。
答え
(define (pluralize str)
str)
(define (pluralize str)
"x")
(define (pluralize str)
"")
blue-triangle-starter.rkt 難易度★☆☆
数値を受け取り、そのサイズの青い実線の三角形を生成する関数を設計します。
関数の設計方法(HtDF)レシピを使用する必要があり、完全な設計にはsignature、purpose、コメントアウトされたstub、example/test、コメントアウトされたtemplate、完成した関数が含まれている必要があります。
答え
(require 2htdp/image)
;; Natural -> Image
;; Given a number, produce a blue solid triangle of that size.
(check-expect (blue-triangle 7) (triangle 7 "solid" "blue"))
(check-expect (blue-triangle 50) (triangle 50 "solid" "blue"))
(check-expect (blue-triangle 100) (triangle 100 "solid" "blue"))
;(define (blue-triangle n) empty-image) ; stub
#;
(define (blue-triangle n)
(... n))
(define (blue-triangle n)
(triangle n "solid" "blue"))
double-error-starter.rkt 難易度★☆☆
この関数設計には複数の問題がある可能性があります。以下の関数設計の実行時に発生するエラーを解決するために必要な最小限の変更を加えます。
;; Number -> Number
;; doubles n
(check-expect (double 0) 0)
(check-expect (double 4) 8)
(check-expect (double 3.3) (* 2 3.3))
(check-expect (double -1) -2)
#;
(define (double n) 0) ; stub
(define (double n)
(* (2 n)))
答え
;; Number -> Number
;; doubles n
(check-expect (double 0) 0)
(check-expect (double 4) 8)
(check-expect (double 3.3) (* 2 3.3))
(check-expect (double -1) -2)
#;
(define (double n) 0) ; stub
(define (double n)
(* 2 n))
最後の行の(2 n)
の()
は不要
make-box-starter.rkt 難易度★☆☆
いろいろな色のボックスを作ってみるのもいいかもしれません。
How to Design Functions(HtDF)レシピを使用して、色を受け取り、その色の10x10の正方形を作成する関数を設計します。HtDFレシピに従い、コメント化されたバージョンのstubとtemplateを残します。
答え
(require 2htdp/image)
;; Color -> Image
;; Create a box of the given color
(check-expect (make-box "red") (square 10 "solid" "red"))
(check-expect (make-box "gray") (square 10 "solid" "gray"))
;(define (make-box c) (square 0 "solid" "white"))
#;
(define (make-box c)
(... c))
(define (make-box c)
(square 10 "solid" c))
ensure-question-starter.rkt 難易度★★☆
関数の設計方法(HtDF)レシピを使用して、文字列を受け取り、文字列がすでに"?"
で終わっていない限り"?"
を追加する関数を設計します。
この質問では、与えられる文字列の長さが0より大きいと仮定します。HtDFレシピに従い、コメント化されたバージョンのstubとtemplateを残します。
答え
;; String -> String
;; add ? to end of str unless it already ends in ?
(check-expect (ensure-question "Hello") "Hello?")
(check-expect (ensure-question "OK?") "OK?")
;(define (ensure-question str) ;stub
; "str")
;(define (ensure-question str) ; template
; (... str))
(define (ensure-question str)
(if (string=? (substring str (- (string-length str) 1)) "?")
str
(string-append str "?")))
cartesian-starter.rkt 難易度★★★
インタラクティブゲームでは、画面上の2点間の距離を測定できると便利なことがよくあります。これらの点は、デカルト座標を使用して4つの数値x1、y1およびx2、y2として記述することができます。これらの点間の距離の公式は次のとおりです。
関数の設計方法(HtDF)レシピを使用して、2点を表す4つの数値を消費し、2点間の距離を計算するdistance
という関数を設計します。
最初のexample/testとして(distance 3 0 0 4)
を使用すると、5が生成されます。 関数がそのテストで機能したら、(sqrt 2)
を生成する(distance 1 0 0 1)
を試してください。 エラーメッセージをよく読み、ヘルプデスクを使用して、このケースでcheck-within
を使用する方法を見つけてください。
私たちがデザインと言うとき、それはレシピに従うことを意味することを忘れないでください。
コメント化されたバージョンのstubとtemplateを残します。
注記:
このような関数のsignatureは次のとおりです。
;; Number Number Number Number -> Number
このような関数のtemplateは次のとおりです。
; (define (distance x1 y1 x2 y2)
; (... x1 y1 x2 y2))
答え
;; Number Number Number Number -> Number
;; produce cartesian distance between (x1, y1) and (x2, y2)
(check-expect (distance 3 0 0 4) 5)
(check-within (distance 0 1 1 0) (sqrt 2) .00001)
;(define (distance x1 y1 x2 y2) ;stub
; 1)
;(define (distance x1 y1 x2 y2) ; template (provided by starter)
; (... x1 y1 x2 y2))
(define (distance x1 y1 x2 y2)
(sqrt (+ (sqr (- x2 x1))
(sqr (- y2 y1)))))