solution.scm 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. ;; Why compound data?
  2. ;; To raise the conceptual level at which we design programs.
  3. ;; To be able to handle many things as one thing and talk about as if it were really only one thing.
  4. ;; When there are too many different things to keep in mind, we will lose track and get confused.
  5. ;; Task
  6. ;; We imagine to design a system for performing arithmetic operations.
  7. ;; DATA ABSTRACTION:
  8. ;; Isolate the parts of the programm, which deal with how data objects
  9. ;; are represented frim the parts of the programm, that deal with how
  10. ;; data objects are used.
  11. ;; Using data abstraction we can make our programs work on abstract
  12. ;; data, making no assumptions about the data, which are not
  13. ;; necessary.
  14. ;; Selectors and constructors (procedures) implement abstract data in
  15. ;; terms of their concrete representation.
  16. ;; Example: Rational numbers can be represented as pairs of
  17. ;; integers. But the rest of the program should only think of them as
  18. ;; rationals and not have to care about how they are actually
  19. ;; represented. The rest of the program should instead use operations
  20. ;; on them, which abstract this detail away.
  21. ;; Example: Linear combination
  22. #;(define (linear-combination-bad a b x y)
  23. (+ (* a x)
  24. (* b y)))
  25. ;; However, defined like this, it can only deal with things, that the
  26. ;; primitive + and * operations can process.
  27. ;; Suppose we want the linear combination of rational numbers, complex
  28. ;; numbers or polynomials. Then this will not do. Instead, we could
  29. ;; use more generic operations like `add` and `mul`, which can be
  30. ;; defined to be more complex operations, which work on many types of
  31. ;; arguments:
  32. #;(define (linear-combination a b x y)
  33. (add (mul a x)
  34. (mul b y)))
  35. ;; NOTE:
  36. ;; We already learned about something called PROCEDURE ABSTRACTION:
  37. ;; Isolating the way a procedure is constructed from how the procedure
  38. ;; is used.
  39. ;; Example: Rational Numbers
  40. ;; make-rat is later changed to reduce the fractions
  41. (define (make-rat-no-reduce numerator denominator)
  42. (cons numerator denominator))
  43. ;; make-rat is using the gcd procedure
  44. (define (gcd a b)
  45. (if (= b 0)
  46. a
  47. (gcd b (remainder a b))))
  48. (define (make-rat-with-reduce n d)
  49. (let ((g (gcd n d)))
  50. (cons (/ n g)
  51. (/ d g))))
  52. (define (numer rational-number)
  53. (car rational-number))
  54. (define (denom rational-number)
  55. (cdr rational-number))
  56. ;; Now we can define more complex operations on top of the data
  57. ;; abstraction for rational numbers as follows:
  58. (define (add-rat x y)
  59. (make-rat (+ (* (numer x) (denom y))
  60. (* (numer y) (denom x)))
  61. (* (denom x) (denom y))))
  62. (define (sub-rat x y)
  63. (make-rat (- (* (numer x) (denom y))
  64. (* (numer y) (denom x)))
  65. (* (denom x) (denom y))))
  66. (define (mul-rat x y)
  67. (make-rat (* (numer x) (numer y))
  68. (* (denom x) (denom y))))
  69. (define (div-rat x y)
  70. (make-rat (* (numer x) (denom y))
  71. (* (denom x) (numer y))))
  72. (define (equal-rat? x y)
  73. (= (* (numer x) (denom y))
  74. (* (numer y) (denom x))))
  75. ;; We add an optional output port as argument here to be able to
  76. ;; better test the print procedure. It can be ignored in normal usage
  77. ;; and the procedure can be made to output to a string while testing.
  78. (define* (print-rat x #:optional port)
  79. (display
  80. (simple-format #f
  81. "~a/~a\n"
  82. (numer x) (denom x))
  83. port))
  84. ;; ============
  85. ;; Exercise 2.1
  86. ;; ============
  87. ;; Define a better version of make-rat that handles both
  88. ;; positive and negative arguments. make-rat should normalize the sign
  89. ;; so that if the rational number is positive, both the numerator and
  90. ;; denominator are positive, and if the rational number is negative,
  91. ;; only the numerator is negative.
  92. ;; ============
  93. ;; Solution 2.1
  94. ;; ============
  95. (define (sign num)
  96. (if (< num 0) - +))
  97. ;; Only defining `negative?` and `positive?` in case the Scheme
  98. ;; dialect does not support them already.
  99. (define (negative? num)
  100. (equal? (sign num) -))
  101. (define (positive? num)
  102. (equal? (sign num) +))
  103. (define (reduce-numer-and-denom numerator denominator)
  104. (let ([g (abs (gcd numerator denominator))])
  105. (cons (/ numerator g)
  106. (/ denominator g))))
  107. (define (normalize-sign numerator denominator)
  108. (cond
  109. ;; When there are 2 negative numbers, we want both signs to be
  110. ;; inverted, so that only positive numbers remain. Since we want
  111. ;; to invert sign if only the denominator is negative, so that
  112. ;; the numerator instead is the negative number, asking the
  113. ;; question, whether the numerator is negative and the
  114. ;; denominator is negative is irrelevant. This case is already
  115. ;; covered by only asking whether the denominator is negative.
  116. #;[(and (negative? numerator) (negative? denominator))
  117. (cons (/ (* -1 numerator) g)
  118. (/ (* -1 denominator) g))]
  119. [(negative? denominator)
  120. (cons (* -1 numerator)
  121. (* -1 denominator))]
  122. [else
  123. (cons numerator denominator)]))
  124. ;; -> `make-rat` shall not receive a sign as an argument, in order to
  125. ;; still conform to the interface we are currently assuming make-rat
  126. ;; to adhere to.
  127. (define (make-rat numerator denominator)
  128. ;; By accessing car and cdr here, we do not assume that the
  129. ;; `normalize-sign` procedure returns a fraction, but instead
  130. ;; assume, that it will return a pair of numbers as a result. If the
  131. ;; fraction representation changes, the `normalize-sign` procedure
  132. ;; remains unchanged.
  133. ;; `reduce-numer-and-denom` takes 2 numbers as well, also remaining
  134. ;; independent of the actual fraction representation.
  135. (let ([normalized-numer-and-denom (normalize-sign numerator denominator)])
  136. (let ([numer-and-denom
  137. (reduce-numer-and-denom (car normalized-numer-and-denom)
  138. (cdr normalized-numer-and-denom))])
  139. ;; By "unpacking" numer-and-denom here again we express the
  140. ;; representation of fractions using cons explicitly, instead of
  141. ;; relying on the return value from reduce-numer-and-denom.
  142. (cons (car numer-and-denom) (cdr numer-and-denom)))))
  143. ;; =====
  144. ;; ASIDE
  145. ;; =====
  146. ;; In Guile Scheme we can also write it as follows:
  147. ;; A required import:
  148. (use-modules (ice-9 receive))
  149. (define (reduce-numer-and-denom-multi-value numerator denominator)
  150. (let ([g (abs (gcd numerator denominator))])
  151. (values (/ numerator g)
  152. (/ denominator g))))
  153. (define (normalize-sign-multi-value numerator denominator)
  154. (cond
  155. [(negative? denominator)
  156. (values (* -1 numerator)
  157. (* -1 denominator))]
  158. [else
  159. (values numerator denominator)]))
  160. (define (make-rat-guile-scheme numerator denominator)
  161. (receive (sign-norm-numer sign-norm-denom)
  162. (normalize-sign-multi-value numerator denominator)
  163. (receive (reduced-numer reduced-denom)
  164. (reduce-numer-and-denom-multi-value sign-norm-numer sign-norm-denom)
  165. (cons reduced-numer reduced-denom))))
  166. ;; This would avoid constructing and deconstructing pairs of numerator
  167. ;; and denominator multiple times, but I guess, it would create
  168. ;; lambdas instead in the `receive` macro.
  169. ;; =========
  170. ;; Tests 2.1
  171. ;; =========
  172. (use-modules (srfi srfi-64))
  173. ;; (define (call-with-output-string procedure)
  174. ;; (let ((port (open-output-string)))
  175. ;; (procedure port)
  176. ;; (get-output-string port)))
  177. (test-begin "test-2.01")
  178. (test-group
  179. "test-2.01"
  180. (test-assert
  181. (string=?
  182. (string-trim-both (call-with-output-string
  183. (λ (port)
  184. (print-rat (make-rat 1 2) port)))
  185. char-set:whitespace)
  186. "1/2"))
  187. (test-assert
  188. (string=?
  189. (string-trim-both (call-with-output-string
  190. (λ (port)
  191. (print-rat (make-rat -1 2) port)))
  192. char-set:whitespace)
  193. "-1/2"))
  194. (test-assert
  195. (string=?
  196. (string-trim-both (call-with-output-string
  197. (λ (port)
  198. (print-rat (make-rat 1 -2) port)))
  199. char-set:whitespace)
  200. "-1/2"))
  201. (test-assert
  202. (string=?
  203. (string-trim-both (call-with-output-string
  204. (λ (port)
  205. (print-rat (make-rat -1 -2) port)))
  206. char-set:whitespace)
  207. "1/2"))
  208. (test-assert
  209. (string=?
  210. (string-trim-both (call-with-output-string
  211. (λ (port)
  212. (print-rat (make-rat 10 -2) port)))
  213. char-set:whitespace)
  214. "-5/1"))
  215. (test-assert
  216. (string=?
  217. (string-trim-both (call-with-output-string
  218. (λ (port)
  219. (print-rat (make-rat 10 -30) port)))
  220. char-set:whitespace)
  221. "-1/3")))
  222. (test-end "test-2.01")