;; 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-beginner-reader.ss" "lang")((modname union-type-intro) (read-case-sensitive #t) (teachpacks ()) (htdp-settings #(#t constructor repeating-decimal #f #t none #f () #f))) #| ; Union Types -- intro ; ; - What is the type of (the signature of)... ; a. string=? : ... ; b. string? : ... ; ; - What are the steps of the design recipe [complete the 3 lines] ; ; 1. datatype definition ; 2. give examples of the new datatype ; 3. write the TEMPLATE for this datatype ; a. if you have a union-type with k variants, make a cond with k branches. ; ;;; Per-function: ; 4. test cases ; 5. SSS -- Signature, Purpose Statement, Stub ; 7. complete body of function (after pasting template) ; 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 |# ; Datatype definition: ; A *course-result* is: ; - a number in [0.0,4.0], (interpretation: grade) OR ; - 'incomplete, (interpretation: student has taken an incomplete, which hasn't expired) OR ; - 'in-progress, (interpretation: the course is currently in progress) OR ; - a string (interpretation: the planned semester to take it in future) ; We say this is a "union type, with 4 variants". ; Give several examples of the data. 0.0 4.0 3.8 'in-progress 'incomplete "Spring 2022" ; Task: write a function to determine if a student can register itec220 (CS2) next semester, ; 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-itec220? 0.0) #false) (check-expect (can-register-itec220? 4.0) #true) (check-expect (can-register-itec220? 3.8) #true) (check-expect (can-register-itec220? 'in-progress) #true) (check-expect (can-register-itec220? 'incomplete) #true) (check-expect (can-register-itec220? "Spring 2022") #false) (check-expect (can-register-itec220? 2.0) #true) (check-expect (can-register-itec220? 1.999) #false) ; NOTE: we do NOT need to write a unit-test that passes in a number > 4.0, ; nor one that passes in a symbol like 'elephant. ; That's because our signature says we must be given a *course-result* -- ; if the caller disobeys that pre-condition, that's on them. ; (Similarly, you don't need unit-tests which pass in characters or scanners or regexps.) ; ; (If you really want to be a defensive coder, you could write `course-result? : ANY -> boolean` ; and then have a first line which calls that on the input. ; In fact, you can make your signature into *actual* code if you like; ; see Lectures/contract-examples.rkt .) ; can-register-itec120? : course-result -> boolean ; can a register itec220 (CS2) next semester, ; given their course-result from itec120 (CS1), `cr120`. ; (define (can-register-itec220? cr120) (cond [(number? cr120) (>= cr120 2.0)] [(string? cr120) #false] [(symbol=? cr120 'incomplete) #true] [(symbol=? cr120 'in-progress) #true])) #| We saw that if we swap the order of the middle-two cases, our code throws an error! That's because `symbol=?` expects two symbols; it will barf if you pass it a string. Remedies (all okay for this course): - Carefully order your cond question&answers to avoid this (as we do here). The code is less robust, but too much so; we're use to `if-else-if` being order-sensitive. - use `and` to make the check more robust: [(and (symbol? cr120) (symbol=? cr120 'incomplete)) #true] - Instead of calling `symbol=?`, you can call `equal?` -- this compares *any* two types. (But in cases where you really do expect a symbol, it doesn't let the language help you out.) |# ;; a TEMPLATE for course-outcomes: ;; (define (func-for-c-o a-co) (cond [(number? a-co) ...] [(equal? a-co 'incomplete) ...] [(equal? a-co 'in-progress) ...] [(string? a-co) ...])) ; Datatype Def'n: A taxable income is either: ; - a real number in (-∞, 0] (interpretation: a loss), OR ; - a real number in (0, 9875] (interpretation: a modest income), OR ; - a real number in (9875,40125] (interpretation: a middle income), OR ; - a real number in (40125,∞) (interpretation: a high income) ; Task: Write a function to return the tax-due, given taxable-income. ; (for single tax-payers; we'll stop after 3 brackets) ; ($9875 => 10%; $40120 => 12%; else 22% ) ; https://taxfoundation.org/2020-tax-brackets/ ; examples of the data: -10 0 10 9875 10000 40125 100000 ; func-for-taxable-income : taxable-income -> ?? ; (define (func-for-taxable-income a-ti) (cond [(and (< -inf.0 a-ti) (<= a-ti 0)) ...] [(and (< 0 a-ti) (<= a-ti LO)) ...] [(and (< LO a-ti) (<= a-ti MED)) ...] [(and (< MED a-ti) (<= a-ti +inf.0)) ...])) ; Task: Write a function to return the tax-due, given taxable-income. (define LO 9875) (define MED 40120) ; TODO: Step 4: develop test cases: (check-expect (tax -10) 0) (check-expect (tax 0) 0) (check-expect (tax 10) 1) (check-expect (tax LO) (* 0.10 LO)) (check-expect (tax 10000) (+ (* 0.10 LO) (* 0.12 (- 10000 LO)))) (check-expect (tax MED) (+ (* 0.10 9875) (* 0.12 (- MED LO)))) (check-expect (tax 100000) (+ (* 0.10 LO) (* 0.12 (- MED LO)) (* 0.22 (- 100000 MED)))) ; tax : taxable-income -> real ; (define (tax income) (cond [(and (< -inf.0 income) (<= income 0)) 0] [(and (< 0 income) (<= income LO)) (* 0.10 income)] [(and (< LO income) (<= income MED)) (+ (* 0.10 LO) (* 0.12 (- income LO)))] [(and (< MED income) (<= income +inf.0)) (+ (* 0.10 LO) (* 0.12 (- MED LO)) (* 0.22 (- income MED)))])) ; tax : number -> number ; return the tax-due, given taxable-income `a-ti`. ; (for single tax-payers; we'll stop after 3 tax-tiers) ; #| ; An alternate implementation: ; 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 9525) (* 0.10 income)] [(<= income 36900) (+ (* 0.10 9525) (* 0.15 (- income 9525)))] [else (let* {[tier2 (- 38700 9525)] [excess (- income 38700)]} (+ (* 0.10 9075) (* 0.15 tier2) (* 0.25 excess)))])) |# ; Update: ANOTHER datatype: ; a taxable-income-entry is either: ; - 'exempt (interpretation: income is exempt-- tax-treaty?), OR ; - #false (interpretation: no taxes filed!), OR ; - taxable-income (interpretation: the reported taxable income) ; Examples of the data -- taxable-income-entry: -10 0 10 9875 10000 40125 100000 'exempt #false ; template (define (function-for-tie a-tie) (cond [(symbol? a-tie) ...] [(false? a-tie) ...] [(and (real? a-tie) (<= a-tie 9525)) ...] [... ...] [... ...]))) ; (**) 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) ...] [(boolean? a-tie) ...] [else ...])) ; send-warning-letter? : taxable-income-entry -> boolean ; SHould a letter be sent from IRS? ; (if income was high, or if they didn't file, or if income was medium then yes 50% of time.) (define (send-warning-letter? a-tie) (cond [(symbol? a-tie) #false] [(boolean? a-tie) #true] [else (send-warning-for-ti? a-tie)])) ; USE A HELPER for the "sub"union-type ; If you have a uion-type, and one of the variants is itself another union: ; DON'T collapse-the-conds (even though you could); ; INSTEAD make a helper for the "sub"type. "One function, one purpose." ; 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 course-result is: ; - a number in [0.0,4.0], (interpretation: grade) OR ; - 'incomplete, (interpretation: student has taken an incomplete, which hasn't expired) OR ; - 'in-progress, (interpretation: the course is currently in progress) OR ; - a string (interpretation: the planned semester to take it in future) ; ; 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. ; Datatype definition: ; A lookup-result: ; - a string (interpretation: RU username of an existing student), OR ; - #false (interpretation: no such person found), OR ; - 'private (interpretation: person exists, but information is non-public). ; a color-spec is: ; - a string (interpretation: one of the standard css-color-names), OR ; - a struct with RGB fields (interpretation: red, green, and blue components of the color) ; A Student is: ; - A String (interpretation: a first & last name), OR ; - a number (interpretation: an ID number). ; - A String (interpretation: a street-address in standard post-office format) OR ; - null (interpretation: the address exists, but is unknown to us). ; A card-rank is: ; - a natnum in 2..10 ; - one-of 'A, 'K, 'Q, 'J ; - 'Joker ; 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. The Java solution: `Optional`. https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html 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) ...] )) |#