On this page:
Prerequisites (5 minutes)
Abstraction and Higher-Order Functions (10 minutes)
Finishing the Predicates (15 minutes)
Working with Abstraction, Higher-Order Functions, and JSON (35 minutes)
Designing New Abstractions (35 minutes)
Before you go...
6.7

Lab 5 JSON and Higher-Order Functions

home work!

Purpose: To familiarize you with the design and use of higher-order functions, and to practice querying and manipulating mutually recursive, tree-structured data such as JSON values.

Textbook References: Chapter 19: The Poetry of S-Expressions, Chapter 20: Incremental Refinement, Chapter 22: The Commerce of XML Chapter 14: Similarities Everywhere Chapter 15: Designing Abstractions Chapter 16: Using Abstractions

Prerequisites (5 minutes)

You have graduated to Intermediate Student Language; congratulations! Don’t forget to change the language level in DrRacket (Language -> Choose Language...).

Today’s lab will continue to use the JSON data we worked with last week. As a reminder, here’s the data definition for JSON:

; A JSON is one of:
; - String
; - Number
; - Boolean
; - 'null
; - JSONArray
; - JSONObject
 
; A JSONArray is a [List-of JSON]
 
; A JSONObject is a [List-of JSONPair]
 
; A JSONPair is a (list Symbol JSON)
 
; Interpretation: JSON values are standardized representations of
; atomic values, lists of values (JSONArray), and collections of
; name/value pairs (JSONObject).

If you need to reinstall the JSON parser library, open DrRacket, click File > Install .plt File... and in the Web tab type in http://www.ccs.neu.edu/course/cs2500accel/code/cs2500f16-jsonlab.plt and hit enter. When the file is finished installing you will no longer have the option to "Abort". To use this library, add (require cs2500f16-jsonlab) at the top of your file.

Once the parser library is installed, use the following code to fetch the weather data from OpenWeatherMap’s API:

(define weather-data (string->json (get-url "http://tinyurl.com/zbqnrhp")))

Finally, we have provided for you (most of) the various accessors, predicates, and equality relations you were developing last week:

; json? : Any -> Boolean
; Is j a valid JSON value?
(define (json? j)
  (or (string? j)
      (number? j)
      (boolean? j)
      (and (symbol? j) (symbol=? j 'null))
      (json-array? j)
      (json-object? j)))
 
; json-array? : Any -> Boolean
; Is j a valid JSON array?
(define (json-array? j)
  ; TODO: finish this
  #f)
 
; json-object? : Any -> Boolean
; Is j a valid JSON object?
(define (json-object? j)
  ; TODO: finish this
  #f)
 
; json-pair? : Any -> Boolean
; Is j a valid JSON pair?
(define (json-pair? j)
  (and (list? j)
       (= (length j) 2)
       (symbol? (first j))
       (json? (second j))))
 
; json=? : JSON JSON -> Boolean
; Are the inputs the same JSON value?
(define (json=? j1 j2)
  (or (and (string? j1) (string? j2) (string=? j1 j2))
      (and (number? j1) (number? j2) (= j1 j2))
      (and (boolean? j1) (boolean? j2) (boolean=? j1 j2))
      (and (symbol? j1) (symbol? j2))
      (and (json-array? j1) (json-array? j2) (json-array=? j1 j2))
      (json-object=? j1 j2)))
 
; json-array=? : JSONArray JSONArray -> Boolean
; Are the inputs the same JSON array?
(define (json-array=? j1 j2)
  (or (and (empty? j1) (empty? j2))
      (and (cons? j1)
           (cons? j2)
           (json=? (first j1) (first j2))
           (json-array=? (rest j1) (rest j2)))))
 
; json-object=? : JSONObject JSONObject
(define (json-object=? j1 j2)
  (or (and (empty? j1) (empty? j2))
      (and (cons? j1)
           (json-object-has-pair? j2 (first j1))
           (json-object=? (rest j1) (json-object-remove j2 (first j1))))))
 
; json-object-has-pair? : JSONObject JSONPair -> Boolean
; Does o contain the pair p?
(define (json-object-has-pair? o p)
  (and (cons? o)
       (or (json-pair=? (first o) p)
           (json-object-has-pair? (rest o) p))))
 
; json-object-remove : JSONObject JSONPair -> Boolean
; Returns o but with the pair p removed (assumes that p is in o).
(define (json-object-remove o p)
  (cond
    [(json-pair=? (first o) p) (rest o)]
    [else (cons (first o) (json-object-remove (rest o) p))]))
 
; json-pair=? : JSONPair JSONPair -> JSON
(define (json-pair=? j1 j2)
  (and (symbol=? (first j1) (first j2))
       (json=? (second j1) (second j2))))
 
; A MaybeJSON is one of:
; * JSON
; * 'notfound
 
; find-value : JSONObject Symbol -> MaybeJSON
; Returns the JSON value in o corresponding to key, or 'notfound if no such pair exists in o
(define (find-value o key)
  (cond
    [(empty? o) 'notfound]
    [(symbol=? (first (first o)) key) (second (first o))]
    [else (find-value (rest o) key)]))

Abstraction and Higher-Order Functions (10 minutes)

Sample Problem Here are two functions that process the raw 7-day forecast values into a more digestible form:

; forecast-descriptions : JSONArray -> [Listof String]
; 
; Given an OpenWeatherMap JSONArray describing the 7-day forecast,
; returns a list of strings describing the weather on each day.
(define (forecast-descriptions days)
  (cond
    [(empty? days) '()]
    [else
     (cons (find-value (first (find-value (first days) 'weather)) 'description)
           (forecast-descriptions (rest days)))]))
 
; forecast-highs : JSONArray -> [Listof Number]
; 
; Given an OpenWeatherMap JSONArray describing the 7-day forecast,
;  returns the list of high temperatures for each day.
(define (forecast-highs days)
  (cond
    [(empty? days) '()]
    [else
     (cons (find-value (find-value (first days) 'temp) 'max)
           (forecast-highs (rest days)))]))

Clearly there is similarity between the two. Each one walks down the list of days in the forecast and produces one new element of a list for every day. Now imagine we have 20 such functions, and we decide to change how we walk down lists. We’d have to make the exact same change in 20 different places! Not only is this tedious, but we’re also near-guaranteed to make a mistake in at least one of those functions.

Let’s use abstraction to prevent this problem. Write a function that abstracts over the differing parts of these functions, and rewrite the two functions in terms of your abstraction.

Finishing the Predicates (15 minutes)

Exercise 1 We have defined stubs for json-array? and json-object? above. Finish defining those functions with higher-order abstractions and test them. (Hint: one of ISL’s built-in functions will be useful here). Make sure to test for cases that should return false, like (json-array? 3) or (json-object? "foobar").

Working with Abstraction, Higher-Order Functions, and JSON (35 minutes)

Exercise 2 Without using higher-order functions, design a function that returns the list of humidity values for each day.

Exercise 3 Without using higher-order functions, design a function that returns a list of formatted srings for the forecast, where each day’s string is of the form "<description>, high of <high>, low of <low>". For example, "light rain, high of 51, low of 36". Make sure to round the highs and lows to the nearest integer using round; otherwise the numbers might not print the way you expect.

Exercise 4 Use a higher-order abstraction to rewrite the previous two functions. (Hint: one of ISL’s built-in functions will be useful here)

Exercise 5 Without using higher-order functions, design a function that returns the list of days when it’s going to rain (i.e. when the "main" field’s value is "Rain").

Exercise 6 Without using higher-order functions, design a function that returns the list of days when the high is above 50 degrees.

Exercise 7 Use a higher-order abstraction to rewrite the previous two functions. (Hint: one of ISL’s built-in functions will be useful here)

Exercise 8 Design a function average-data that takes two inputs: a function select that returns a number when given a day’s forecast, and a list of day-forecast values. The function should return the average of the value that select chooses from each day. (Hint: one of ISL’s built-in functions will be useful here)

Use average-data to design the functions average-high and average-rainfall that compute the average high temperature and rainfall amounts of the given days, respectively.

Note: some days do not have an entry for 'rain; use the value 0 for those cases instead.

Designing New Abstractions (35 minutes)

The library we provided to you represents JSON objects as lists, but when dealing with sets of name/value pairs (a.k.a dictionaries, a.k.a. hash tables), we often want to treat the key and the value separately, rather than manipulating both as a pair. In this section, you will design and apply abstractions for JSONObject.

Exercise 9 Design the function json-object-map that takes as input a function f and a JSONObject j and returns an object like j except that all values in the JSONPairs of j are replaced with the result of applying f to the value.

Exercise 10 Use json-object-map to design a function that when given a temperature object from a day’s forecast (i.e. the object labeled as 'temp) returns the same temperature data with each value increased by 10.

Exercise 11 Use json-object-map to design a function that when given a temperature object from the weather data returns a temperature object with the temperatures converted to Celsius. (Reminder: celsius-temp = (farenheit-temp - 32) * (5/9)).

Exercise 12 Design the function json-object-filter that takes as input a function f and a JSONObject j and returns a list containing the pairs in j such that applying f to the pair returns true.

Exercise 13 Use json-object-filter to design a function that when given a temperature object from the weather data, returns a similar object with only the pairs whose value is greater than 50.

Exercise 14 Use json-object-filter to design a function that when given a JSONObject returns a similar object that contains only the pairs with numeric values.

Before you go...

If you had trouble finishing any of the exercises in the lab or homework, or just feel like you’re struggling with any of the class material, please feel free to come to office hours and talk to a TA or tutor for additional assistance.