;; The first three lines of this file were inserted by DrRacket. They record metadata ;; about the language level of this file in a form that our tools can easily process. #reader(lib "htdp-intermediate-lambda-reader.ss" "lang")((modname union-type-intro-after) (read-case-sensitive #t) (teachpacks ()) (htdp-settings #(#t constructor repeating-decimal #f #t none #f () #f))) (require "student-extras.rkt") #| ; Union Types -- intro ; ; - What is the type of (the signature of)... ; a. string=? : (-> string? string? boolean?) ; b. string? : (-> any? boolean?) ; c. equal? : ... ; ; ; - What are the steps of the design recipe [complete the 3 lines] ; ;;; Per-function: ; 4. ; 5. ; 7. ; 8. Watch your tests pass, and celebrate! ---- per function: 4. write tests 5. stub: signature, purpose-statement, header, stub 7. complete the function-body 8. watch your tests pass |# ; To-model: ; == number of students in a course? ; == temperature, in degrees C? ; == somebody's first name? ; == zip code? ; == somebody's first + last name? ; == an address? ; == a semester, like 2024-spring or 2025-fall ? ; ; == a course-result -- something that is *either* the grade ; a student obtained, or that they got an 'I' that is ; still being resolved, or the fact that the student is currently ; enrolled, or the fact that they haven't taken the course ; but are registered to take it next fall (or next summer or next spring). ; letter-grade: a real-number in [0, 4.0], OR 'incomplete, OR #t, string ; ; To-model: ; A *course-result* is: ; - grade, OR ; - incomplete, ; - in-progress, ; - a planned semester to take it in future ; We say this is a "union type, with 4 variants". ; datatype definition: (define (course-result? x) (or (real? x) ; interpretation: letter grade (equal? x 'incomplete) ; interpretation: Incomplete (equal? x #t) ; interpretation: in-progress (string? x))) ; interpretation: planned-future-semester ; For this class: ; If you say "datatype definition" before a function, ; you DO NOT need purpose-statement nor signature (both clear from knowing it's a datatype-defn), ; nor even unit-tests! ; (though myself, I still include unit tests if there's anything nontrivial, ; like in this example making sure I got the regexp correct). ; Give several examples of the data. ; 4.0 2.0 0.0 'incomplete #true "2025spring" "2029fall" ; Template for *any* course which handles a course-result: ; (have a 4-branch cond) ; (define/contract (func-for-course-result a-cr) (-> course-result? ...?) (cond [(real? a-cr) ...] [(equal? a-cr 'incomplete) ...] [(equal? a-cr 'in-progress) ...] [(string? a-cr) ...])) ; ; Btw: ; You might say "shouldn't that first branch check that we have a `real?` *bewteen 0 and 4* ??" ; Or similarly, "shouldn't the last branch check that the string meets the desired pattern?". ; While that wouldn't be wrong, it's not necessary: ; Our signature is already ensuring that `a-cr` really is a valid course-result. ; Once we know that, just asking `real?` or `string?` is enough to know which variant we have. ; Task: write a function to determine if a student can register itec220 (CS2), ; given their course-result from itec120 (CS1). ; Policy: you are allowed to register for itec220 if you got a C or better (2.0), ; or if you're currently-enrolled, or if you have an unresolved-incomplete, ; but not if you're just enrolled in CS1 for *next* semester. (check-expect (can-register-cs220? 4.0) #t) (check-expect (can-register-cs220? 2.0) #t) (check-expect (can-register-cs220? 1.7) #f) (check-expect (can-register-cs220? 'incomplete) #t) (check-expect (can-register-cs220? #t) #t) (check-expect (can-register-cs220? "2025spring") #f) (check-expect (can-register-cs220? "2029fall") #f) ; write a function to determine if a student can register itec220 (CS2), ; given their course-result from itec120 (CS1). ; (define/contract (can-register-cs220? cs120-result) (-> course-result? boolean?) (cond [(real? cs120-result) ...] [(equal? cs120-result 'incomplete) ...] [(equal? cs120-result '#true) ...] [(string cs120-result) ...])) ; Now: write a function which, given a coruse-result, returns a string that can be put onto the transcript. ; Template: ;;; NOTE: ;;; DO NOT CONFUSE ;;; defining a new type, with a function which *handles* something of that type. ;;; On exams, people will confuse `course-result?` with `can-register-for-cs220?`, oops! ; Another example of union types: ; When querying a name in the student-database for their email address, ; the returned answer must be able to represent indicate: ; - a match (the student's username), OR ; - no such person is found, OR ; - the person exists, but information is non-public. ; ; TODO: How should we represent the student-query result? ; ; (brainstorm, then scroll) ; Datatype definition: (define (student-query-result? x) (or (username? x) (false? x) ; interpretation: no such person (equal? x 'private))) ; interpretation: info is to be kept private ; Datatype definition: (define (username? x) ; interpretation: an RU student's username (and (string? x) (<= 1 (string-length x) 17) ; "cmartinezmurillo@" (regexp-match? #px"^[a-z]+[0-9]*$" x))) (check-expect (username? "ibarland") #true) (check-expect (username? "president") #true) (check-expect (username? "z") #true) (check-expect (username? "z9") #true) (check-expect (username? "9") #false) (check-expect (username? "bottles99") #true) ; Ben Ottles -- one of many (check-expect (username? "99bottles") #false) (check-expect (username? "cmartinezmurrillo") #true) (check-expect (username? "TooLongcmartinezmurrillo") #false) (check-expect (username? "hi2bye3") #false) ; non-strings (check-expect (username? 'ibarland) #false) (check-expect (username? 99) #false) (check-expect (username? #true) #false) (check-expect (username? #px"[a-z][a-z][0-9]+") #false) ; TODO: What is the template, for any function processing ... #| ; Task: Write a function to return the tax-due, given taxable-income. ; (for single tax-payers; we'll stop after 3 brackets) ; ($11600 => 10%; $47150 => 12%; else 22% ) ; https://taxfoundation.org/2025-tax-brackets ; see also, 5min on how brackets work: https://www.youtube.com/watch?v=VJhsjUPDulw ; To-model: ; A taxable-income is either: ; - an amount less than $0 (a "loss"), OR ; - an amount in [$0, 11600) (a "modest income"), OR ; - an amount in [$11600,47150) (a "middle income"), OR ; - an amount in [$47150,∞) (a "high income") |# (define LO 11600) (define MED 47150) ; examples of the data: -23 0 5 11599.99 11600 12000 47149.99 47150 1000000000 ; Datatype Def'n (version 1) #;(define (taxable-income? val) (real? val)) ; ; This works, but we're going to be more thorough, and make some specific helpers: ; Datatype Def'ns [see note below]: (define (income-loss? x) (< -inf.0 x 0)) (define (income-modest? x) (and (<= 0 x) (< x LO))) (define (income-middle? x) (and (<= LO x) (< x MED))) (define (income-high? x) (and (<= MED x) (< x +inf.0))) ; interpretations: x is an amount *in dollars*. ; <-- always mention units! ; Now, the one function we really were after: (define (taxable-income? val) (and (real? val) (or (income-loss? val) (income-modest? val) (income-middle? val) (income-high? val)))) ; ; Okay, this datatype-definition is admittedly overly heavyweight -- I'd be happy with: ; "Datatype definition: (define taxable-income? real?)" or even just ; ";We represent taxable-income as a real". ; Both would satisfy Step 1 of the design-recipe. ; Our heavyweight egghead solution does provide a couple of other nice bits though: ; - the predicate's definition reflects the four variants inherent to taxable-incomes. ; - we have attached program-names corresponding to the technical terms tax-accountants use ("middle income") ; - we could naturally use those predicates in our code ; But yeah IRL I'd probably go with `(define taxable-income? real?)`, myself. ; func-for-taxable-income : taxable-income -> ?? ; (define/contract (func-for-taxable-income a-ti) (-> taxable-income? ...?) ; Return ??? (cond [(income-loss? a-ti) ...] ; FILL IN HERE [(income-modest? a-ti) ...] [(income-middle? a-ti) ...] [(income-high? a-ti) ...])) ; TODO: Write a function to return the tax-due, given taxable-income. ; TODO: Step 4: develop test cases: (check-expect (tax -30) 0) (check-expect (tax 0) 0) (check-expect (tax 10) 1) (check-expect (tax LO) (* LO 0.10)) (check-expect (tax (+ LO 1.00)) (+ (* LO 0.10) 0.12)) (check-expect (tax (+ LO 0.01)) (+ (* LO 0.10) 0.0012)) (check-expect (tax (+ LO 1000)) (+ (* LO 0.10) 120)) (check-expect (tax MED) (+ (* LO 0.10) (* 0.12 (- MED LO)))) (check-expect (tax (+ MED 10000)) (+ (* LO 0.10) (* 0.12 (- MED LO)) (* 0.20 10000))) ; return the tax-due, given taxable-income `a-ti`. (define/contract (tax/contract a-ti) (-> taxable-income? non-negative-real?) (cond [(income-loss? a-ti) 0] ; FILL IN HERE [(income-modest? a-ti) (* 0.10 a-ti)] [(income-middle? a-ti) (+ (0.10 LO) (* 0.12 (- a-ti LO)))] [(income-high? a-ti) ...])) ; An alternate implementation, using `let*`: ; tax : number -> number ; return the tax-due, given taxable-income. ; (for single tax-payers; we'll stop after 3 tax-tiers) (define (tax income) (cond [(income-loss? income) 0] [(income-modest? income) (* 0.10 income)] [(income-middle? income) (+ (* 0.10 LO) (* 0.12 (- income LO)))] [(income-high? income) (+ (* 0.10 LO) (* 0.12 (- MED LO)) (* 0.20 (- income MED)))] )) #;[else (let* {[tier2 (- MED LO)] [excess (- income MED)]} (+ (* 0.10 LO) (* 0.12 tier2) (* 0.25 excess)))] ; Update: ; a taxable-income-ENTRY is either: ; - 'exempt (interpretation: income is exempt-- tax-treaty?), OR ; - #false (interpretation: no taxes filed!), OR ; - a real number in (-∞, 0] (interpretation: a loss), OR ; - a real number in (0, 9950] (interpretation: a modest income), OR ; - a real number in (9950,40525] (interpretation: a middle income), OR ; - a real number in (40525,∞) (interpretation: a high income) ; ; What do you think of *this* data-def'n? ; BETTER: (define (taxable-income-entry? val) (or (equal? val 'exempt) (false? val) (taxable-income? val))) ; Examples of the data -- taxable-income-entry: 10000 37000.5 6000 'exempt #false ; template (define/contract (function-for-tie a-tie) (-> taxable-income-entry? ...?) (cond [(symbol? a-tie) ...] [(false? a-tie) ...] [(taxable-income? a-tie) ...])) ; (**) or, collapse the last three cases, and just say it's a "taxable-income" ; as previously defined? (define (func-for-taxable-income-entry a-tie) (cond [(symbol? a-tie) 0] [(boolean? a-tie) 0] [(taxable-income? a-tie) (tax a-tie)])) ; DESIGN QUESTION: ; Should we make `tax2` call `tax` as a helper? ; I'd say, Answer: depending on our data-def'n: ; if we included 5 branches in 'taxable-income-entry', we should have 5 branches. ; Otherwise three. ; But that just begs the question: what is the right data-def'n -- ; should it have 'exempt,#f,taxable-income or should it have 5 branches? ; I tend towards the former. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; More examples of union types: ; A char-found-index is: ; - a natural (the index some character first occurs at, inside some string) ; - -1 (the character doesn't occur inside that string) ; Note: Kinda unnatural -- use `#false` instead! ; But Java doesn't let us do that. ; A student-query-result ; - a username (interpretation: RU username of an existing student), OR ; - #false (interpretation: no such person found), OR ; - 'private (interpretation: person exists, but information is non-public). ; ; Datatype definition: #;(define (student-query-result? x) (or (username? x) (false? x) ; interpretation: no such person (equal? x 'private))) ; interpretation: info is to be kept private ; Datatype definition: #;(define (username? x) ; interpretation: an RU student's username (and (string? x) (<= 1 (string-length x) 16) ; "cmartinezmurillo@" (regexp-match? #px"[a-z][a-z0-9]+" x))) ; (e.g. looking at a csv file, tsv file, or file w/ hard-spaced columns) ; A column-determiner is: ; - a character (char to split by), OR ; - a positive-int (the column-width) ; ; datatype def'n: (define (column-determiner? x) (or (char? x) (and (integer? x) (positive? x)))) (check-expect (column-determiner? #\a) #true) (check-expect (column-determiner? "a") #false) (check-expect (column-determiner? "") #false) (check-expect (column-determiner? "abc") #false) (check-expect (column-determiner? 17) #true) (check-expect (column-determiner? -17) #false) (check-expect (column-determiner? 2.3) #false) (check-expect (column-determiner? 0) #false) (check-expect (column-determiner? -2.3) #false) ; path-or-string ; ; datatype def'n: #| requires full-racket for `path?`: (define (path-or-string? x) (or (path? x) (string? x))) |# ; regexp-or-string ; ; datatype def'n: (define (regexp-or-string? x) (or (regexp? x) (pregexp? x) ; two different types, in racket ?! (string? x))) (check-expect (regexp-or-string? "") #true) (check-expect (regexp-or-string? "h") #true) (check-expect (regexp-or-string? "hello") #true) (check-expect (regexp-or-string? #px"") #true) (check-expect (regexp-or-string? #px"h") #true) (check-expect (regexp-or-string? #px"hello") #true) (check-expect (regexp-or-string? #px"([hnbc]ow\\s)+") #true) (check-expect (regexp-or-string? 23) #false) (check-expect (regexp-or-string? 'bye) #false) ; /usr/sbin/periodic uses a configuration-file entry to determine how it handles output: ; ; - If this is set to a path name (beginning with a `/' character), output is simply logged to that file. ; - If this value does not begin with a `/' and is not empty, it is assumed to contain a list of email ; addresses, and the output is mailed to them. ; - If this value is the empty string (or, is not set), output is sent to standard output. ; Another Example of a union type: ; In Star Wars, we wish to represent the ; fire-power of a weapon: ; For blasters, it's easy -- bigger is better! ; But light-sabers are more complicated: each color ; has its own strengths and weaknesses relative to each other. ; SO: a fire-power is: ; a positive number (for blasters), ; OR a symbol (the color of a light-saber). ; Examples of the data: 43.2 'purple ; A Student is represented as: ; - A String (interpretation: a first & last name), OR ; - a number (interpretation: an ID number). ; ; datatype def'n: (define (student? x) (or (string? x) (number? x))) ; A street-address is represented as: ; - A String (interpretation: a street-address in standard post-office format) OR ; - null (interpretation: the address exists, but is unknown to us). ; Hmm, no way to represent "there isn't a street-address"? ; A card-rank is: ; - 2..10 ; - one-of 'A, 'K, 'Q, 'J ; - 'Joker ; ; Datatype defitinition: (define (card-rank? itm) (or (and (natural? itm) (<= 2 itm 10)) (member itm (list 'A 'K 'Q 'J)) (equal? itm 'Joker))) ; a color-spec is: ; - a string (interpretation: one of the standard css-color-names), OR ; - an struct with RGB fields (interpretation: red, green, and blue components of the color) ; a date is: ; - string, (interpretation; a date, in format "yyyy-mmm-dd"), OR ; - an int in 1..366, (interpretation: day-of-the-current-year) OR ; - a java.util.Date object (interpretation: Date -- see that class for details) ; ; Note that in this case, all three variants might be denoting the same underlying info. ; And in real code, you'd want your code to have one "canonical" way it represents ; the info, along with functions to do a "one time" translation the other variants ; into your preferred representation. ; ; E.g., you might use java.util.Date pretty much everywhere in your code base, ; and then have a Date constructor that took in an string "yyyy-mm-dd", ; and a constructor that took in an int(day-of-current-year). ; This doesn't really need a full union-type, BUT it can sure be convenient ; if your clients can pass any of those three into your functions ; (without making them have to call the `Date` constructor themselves). ; ; This accounts for SOME uses of union-types, but not all. ; a problem-report is: ; - an natnum (interpretation: an index into array of existing problem-tickets), OR ; - a string (interpretation: a new problem, to be submitted) ; A file-or-fname is: ; - a string (interpretation: filename), OR ; - a list-of-directories (a "path"), OR ; - a file-descriptor (interpretation: the already-opened file) ; ; [ It's a real-world pain to have functions that want to open a file ; and work with it, ; and then call a helper-function which requires the filename ...] ; a transaction is: ; - double (interpretation: Cash transactions: Discount) ; - int (interpretation: Check transactions: CheckNumber) ; - string-and-two-ints (interpretation: Credit transactions: Card Number and Expiration Date) ; A number-of-allowed-occurrences is: ; - a number (interpretation: the one allowed number), ; - a pair of numbers (interpretation: an interval -- min and max) ; - a list-of-intervals (interpretation: allow the union of the intervals) ; - an indicator function (interpretation: allow a number if the function returns #true when given that number) ; ; E.g. an auto-grader for hw counts #occurs of "sqrt", and ; might want exactly 3, or between 2 and 5, or either 2-5 or 7-9, or a prime-number-of-occurrences. #| Thoughts on the uses of union-types (and what we did before hearing about them). NOTE that sometimes a sentinel value is used, but it has to be shoe-horned into the existing type-system: Consider java's String#indexOf -- what does it return for not-found? What might python/racket/javascript use instead? What might be a union-type, to describe the result? NOTE that if we have a type "T, OR ", in java we'll use a class, and use `null` to represent that one sentinel value. Examples: Map#get; This has its issues [SO easy to forget that a var declared to hold a `String` might not hold a string => Null Pointer Exception; what if `null` might be a valid-info instead of a sentinel, e.g. java.util.Map#get(U) leads to weird library principles like "you can't associate an object with `null`".] NOTE that Java can hack functions that take in a union-type, by overloading. However, they can't *return* something of a union-type. Dynamically-typed languages like javascript, python, racket use union-types routinely (by mentioning them in comments) Some statically-typed languages DO let you define union-types. (ML, Ada's "variant record", Haskell) |# #| ; Another example, worked out: enumerated types. ; (But it's not a very exciting example.) ; Task: `turn`, for changing a stop-light. ; ...wait, what data type to use? ; Data Definition: ; A slc ("stoplight color") is one of: ; - 'red, (interpretation: stop) OR ; - 'yellow (interpretation: stop-if-safe) , OR ; - 'green, (interpretation: go), OR ; - 'flashing-yellow (interpretation: proceed with caution), OR ; - 'flashing-red (stop, then proceed when safe) ;Examples of the data (duh): 'red 'yellow 'green 'flashing-yellow 'flashing-red (check-expect (turn 'red) 'green) (check-expect (turn 'green) 'yellow) (check-expect (turn 'yellow) 'red) (check-expect (turn 'flashing-red) 'flashing-red) (check-expect (turn 'flashing-yellow) 'flashing-yellow) ; turn : slc -> slc ; return the next color for a stop-light. ; (define (turn curr-color) (cond [(symbol=? curr-color 'red) 'green] [(symbol=? curr-color 'yellow) 'red] [(symbol=? curr-color 'green) 'yellow] [(symbol=? curr-color 'flashing-yellow) 'flashing-yellow] [(symbol=? curr-color 'flashing-red) 'flashing-red] [else (error 'turn "fell off cond?!")] ; It's already an error in beginning-student, to fall off the end of a cond. ; So I show this just for fun. (But in full-racket it's not an error, ; even when I wish it was, so in full-racket I *may* write such an `else`.) )) ; Task: write penalty-for-running-a-light. (check-expect (penalty-for-running-a-light 'red) 50) (check-expect (penalty-for-running-a-light 'yellow) 0) (check-expect (penalty-for-running-a-light 'green) 0) (check-expect (penalty-for-running-a-light 'flashing-yellow) 25) (check-expect (penalty-for-running-a-light 'flashing-red) 99) ; penalty-for-running-a-light : slc -> non-negative-real ; Return the fine for running a stoplight, in USD. ; (define (penalty-for-running-a-light curr-color) (cond [(symbol=? curr-color 'red) 50] [(symbol=? curr-color 'yellow) 0] [(symbol=? curr-color 'green) 0] [(symbol=? curr-color 'flashing-yellow) 25] [(symbol=? curr-color 'flashing-red) 99] )) ; Step 3, TEMPLATE, for ANY function processing a slc: ; func-for-slc : slc -> ??? (define (func-for-slc a-slc) (cond [(symbol=? a-slc 'red) ...] [(symbol=? a-slc 'yellow) ...] [(symbol=? a-slc 'green) ...] [(symbol=? a-slc 'flashing-yellow) ...] [(symbol=? a-slc 'flashing-red) ...] )) |#