This tutorial assumes that you have
Sketch-n-sketch running on your browser.
The 'left pane' references the left half of that interface, whereas the 'right pane' references the right half of that interface. If you encounter editing issues, try resizing your window.
At last, your teaching assistants have graded all the assignments,
the mid-terms and the final exam.
They've provided you with a list of student names and their average grades out of 100.
How do you fairly map these scores to letter grades, such as A+, A, A-, B+ ... down to E? To start, replace the program in the Sketch-n-sketch editor by selecting everything and pasting the following code on the left pane of the Sketch-n-sketch editor:
-- We will write code here #1
-- Displays the interface
main = <span><h1>Fair grades</h1>
We want to give grades to our students.<br><br>
Graphics coming soon...
</span>
After clicking on "Run", you should see the below result in the right pane.
Note that the main = is optional, if the program ends with an expression, it will automatically insert main = in front of it.
Fair grades
We want to give grades to our students.
Graphics coming soon...
Update basics
Sketch-n-sketch not only displays the webpage, it allows you to modify it. In the right panel,
type "student" inside "Fair grades" so that it becomes "Fair student grades".
Fair student grades
We want to give grades to our students.
Graphics coming soon...
A pop-up menu "Output Editor" appears on the left panel.
Hover over "Update program".
Hover over the first solution.
You'll see that the code is updated accordingly:-- We will write code here #1
-- Displays the interface
main = <span><h1>Fair student grades</h1>
We want to give grades to our students.<br><br>
Graphics coming soon...
</span>
Click on the first solution to accept the change.
FYI: Right next to the right panel, a button labelled "Auto Sync" enables the propagation of unambiguous changes automatically. We won't be using it in this tutorial.
Import the grades
From the spreadsheet that your assistants provided you, you
generated a Comma-Separated Value (CSV) file with each line containing a name and a grade.
Fortunately, sketch-n-sketch can handle raw strings by enclosing them in triple quotes """.
Let us assign this string data to the variable input_data Replace the code -- We will write code here #1 (line 1) withinput_data = """Alice,64.7
Bob,78.5
Eve,57.9
Seya,100
Madelene,88.3
Mack,32.8
Clarita,90.5
Ulrike,98.5
Ferdinand,74.4
Tamara,93.5
Kellee,82.9
Leanne,83.7
Natalya,43.8
Leonore,84.5
Linette,98.5
Salley,88.4
Aleisha,88.6
Sabine,92.2
Ozella,60.9
Misti,86.7
Jenny,83.3
Nubia,92.3
Tennillee,95.1
Margaret,81.9
Loida,82.8
Cassandra,65.3
Derrick,66.4
Rudolph,83.2
Rafael,86.6"""
-- We will write code here #2
Convert student data
If you want to display this data, you could simply insert @input_data into the main code, but that would only display the string, which would not be very helpful. First, we convert this data to a list of datatypes like Student "Alice" 64.7 and store it to a variable studentsGrades. Replace the code -- We will write code here #2 (line 31) withtype Student = Student String Num
studentsGrades =
input_data
|> Regex.split "\r?\n"
|> List.map (\line -> case Regex.extract "^(.*),(.*)" line of
Just [name, gradeString] ->
Student name (Update.freeze <| String.toFloat gradeString)
)
-- We will write code here #3First, notice how we define a datatype constructor Student that accepts a string (the name) and a number (the grade out of 100). This datatype is like a pair that stores the two pieces of information. The construction b |> f a is equivalent to the more natural f a b (a function f applied to two arguments a and b) but it is a good way to visualize the transformation step by step. Here, we use the library function Regex.split to split the input_data on newlines, which gives us a list of strings.
Then, a regular expression is applied to each of these "lines" to extract the data before and after the comma, so that we can build a Student datum with the extracted name and the grade that we convert to a float (number). Note that we also add Update.freeze to make sure we will never modify this grade indirectly via the output.
Define the cut-offs
We need to define associations between letter grades and the minimum scores (i.e., cutoffs) required to obtain them.
Let's say we have a first guess (e.g., using the cutoffs from the previous year, or a simple linear function). Replace the code -- We will write code here #3 (line 41) withtype CutOff = CutOff String Num
cutoffs = [
CutOff "A+" 96.8,
CutOff "A" 90,
CutOff "A-" 86,
CutOff "B+" 82.9,
CutOff "B" 76,
CutOff "B-" 68.5,
CutOff "C+" 61,
CutOff "C" 55.8,
CutOff "C-" 40.4,
CutOff "D" 29.5,
CutOff "E" 0]
-- We will write code here #4
Example: get students in each group
Just as an exercise, if we wanted to display how many students obtained a grade between "B-" and "A+" (both inclusive), we could insert the following code next to Graphics coming soon... in main:
@(let
thresholdOf name =
let aux cutoffs = case cutoffs of
[] -> 0
CutOff n t :: tail -> if name == n then t else aux tail
in aux cutoffs
minGrade = thresholdOf "B-"
maxGrade = 100
in
(studentsGrades
|> List.filterMap (\Student name grade ->
if grade >= minGrade && grade < maxGrade
then Just name else Nothing)
|> String.join ", ") ++ " have at least a B-"
)
You would end up with the following output in the right pane.
Fair student grades
We want to give grades to our students.
Bob, Madelene, Clarita, Ulrike, Ferdinand, Tamara, Kellee, Leanne, Leonore, Linette, Salley, Aleisha, Sabine, Misti, Jenny, Nubia, Tennillee, Margaret, Loida, Rudolph, Rafael have at least a B-..
This is just an example to illustrate general-purpose computing techniques (such as recursion, pattern matching, list construction and deconstruction), but for now you can discard this code snippet.
Display cut-offs and grades as SVG
We display cut-offs as horizontal black lines, and the grades as short blue lines,
to "see" where the grades are in relation to the cutoffs. To make it meaningful, we sort students by grade. Replace the code -- We will write code here #4 (line 56) withsortedGrades =
studentsGrades
|> sortBy (\(Student _ n1) (Student _ n2) -> n1 > n2)
numStudents = List.length sortedGrades
{softFreeze=keep} = Update
textstyle = "fill:black;font-family:monospace;font-size:12pt"
barChart x y w h = let
barWidth = w / numStudents
bars = List.indexedMap (\i (Student name avg) -> let
xi = x + (i * barWidth)
hi = h * (freeze 0.01! * avg)
yi = y + h - hi
in line 'blue' 2 xi yi (xi + barWidth) yi)
<| List.reverse sortedGrades
separators = cutoffs |> List.concatMap
(\(CutOff abc cutoff) -> let
y = (freeze y + freeze h * freeze 0.01! *
(freeze 100! - cutoff))
in
[line 'black' (keep 2) (keep x) y (keep <| x + w) y,
<text x=20 y=y style=textstyle>@abc</text>,
<text x=w+60 y=y style=textstyle>@abc</text>
])
background = rectWithBorder 'gray' 5 'lightgray' x y w h
in List.concatMap identity [
[background],
bars,
separators
]
width = 450
height = 389
chart = barChart 50 0 width height
-- We will write code here #5Replace the code Graphics coming soon... (line 97) with<span contenteditable="false">
<svg width=toString(width + 100)
height=toString(height)>@chart</svg></span><br>
Table coming soon...
You should obtain the following result, after running the program.
Fair student grades
We want to give grades to our students.
Table coming soon...
..
Thanks to SVG editing capabilities, we can now move the horizontal lines up and down to change the cut-offs. Make sure the line line 48 is visible in the code editor (in the left panel). Your task is now to move the cut-off of the letter B (in the right panel) to accept one more student (by dragging it slowly downwards).
A pop-up "Output Editor" appears. Hover over "Update program", it then takes 1 second to find where to change the cut-off. Click on the solution to accept it.
You should obtain something like the following on the right-hand side.
Fair student grades
We want to give grades to our students.
Table coming soon...
..
First lens: 1-digit cut-offs
After this update, the cut-off number for the letter B might start to have a lot of decimals.
However, only one is necessary.
To avoid this, we add a lens to modify how the cut-off is updated. Replace the code -- We will write code here #5 (line 92) withupdateDecimal = Update.lens {
apply cutOff = cutOff
update {input=oldCutOff,outputNew=newCutOff} =
Ok (Inputs [round (newCutOff * 10) / 10])
}
-- We will write code here #6Replace the code freeze 100! - cutoff (line 76) withfreeze 100! - updateDecimal cutoff
A lens is a pair of two functions (here apply and update),
such that the first contains the logic to do normal forward computation, and the second the logic to back-propagate an updated value.
In our case, this function takes the new output and rounds it to the nearest multiple of 0.1.
Since a lens is a record, we invoke it on an argument using a special constructor called Update.lens.
Modify the cut-off for letter B back to where it was, approximately (just above the blue line right above it).
You will observe that, on update, the cut-off contains only one decimal in the code.
Table of results
At this point, we hope that our cut-offs are well placed.
We now want to know how many students there are in each category. Replace the code -- We will write code here #6 (line 98) withassignLetter avg =
List.find (\CutOff letter cutoff -> avg >= cutoff) cutoffs |>
case of
Nothing -> error <|
"""Could not find a letter for this average: @avg"""
Just (CutOff letter cutoff) -> letter
buckets = let
initBucket = List.map (\c -> (c, [])) cutoffs
addToBucket buckets letter student = case buckets of
(((CutOff bletter _) as c, v) as head)::tail ->
if bletter == letter then (c, v ++ [student])::tail
else head :: addToBucket tail letter student
[] -> error <| "Letter " + letter + " not found in buckets"
in
List.foldl (\((Student studName avg) as student) buckets ->
addToBucket buckets (assignLetter avg) student
) initBucket sortedGrades
displayStudents =
List.map (\Student name grade ->"""@name:@grade""")
>> String.join ","
displayBuckets =
<table>
<tr><th>Grade</th><th>Students</th><th>Grouped</th>
<th>Cut-offs</th></tr>
@(List.indexedMap (\i (CutOff name cutoff, students) ->
<tr style="background:"+(
if i % 2 == 0 then "#dedede" else "white")>
<td>@name</td>
<td title=displayStudents(students)
>@List.length(students)</td>
@(if i % 3 == 0 then
<td rowspan="3">@(
buckets |> List.drop i |> List.take 3
|> List.map (\(n, students) ->
List.length students) |> List.sum)
</td>
else [])
<td>@cutoff</td>
</tr>
) buckets)
</table>
-- We will write code here #7Replace the code Table coming soon... (line 152) with<h2>Grade by category</h2>
@displayBuckets
You should see the following table appear below the graph in the output view:
Grade by category
Grade
Students
Grouped
Cut-offs
A+
3
13
96.8
A
5
90
A-
5
86
B+
5
9
82.9
B
3
76
B-
1
68.5
C+
3
6
61
C
2
55.8
C-
1
40.4
D
1
1
29.5
E
0
0
Note that you can again change the cutoffs from this table right where they are displayed, using basic update techniques.
Unfair letter grade assignments
We want to know if any letter grades have been assigned unfairly.
A potentially unfair letter assignment arises if two student grades are very close to each other (e.g., the difference is less than 0.5),
but they have been assigned different letters.
To avoid being contested, it is best to make sure there is no potentially unfair letter grade assignment.
We compute and display this information in the table. Replace the code -- We will write code here #7 (line 143) withifunfair i =
if i == 0 then "" else let
epsilon = 0.5{0-1.9}
(CutOff nameA cutoffA, studentsAbove) = nth buckets (i - 1)
(CutOff nameB cutoffB, studentsBelow) = nth buckets i
in case (List.last studentsAbove, List.head studentsBelow) of
(Just (Student enviee gradeA),
Just (Student envier gradeB)) ->
if gradeA - gradeB < epsilon then
<span>@envier (@gradeB, @nameB) might protest that
@enviee had almost the same grade (@gradeA) but was
assigned @nameA</span>
else <span></span>
_ -> <span></span>
-- We will write code here #8
Replace the code <th>Cut-offs</th> (line 124) with<th>Cut-offs</th><th>Status</th>Replace the code <td>@cutoff</td> (line 138) with<td>@cutoff</td><td>@ifunfair(i)</td>
Grade by category
Grade
Students
Grouped
Cut-offs
Status
A+
3
13
96.8
A
5
90
A-
5
86
B+
5
9
82.9
B
3
76
Loida (82.8, B) might protest that
Kellee had almost the same grade (82.9) but was
assigned B+
B-
1
68.5
C+
3
6
61
C
2
55.8
C-
1
40.4
D
1
1
29.5
E
0
0
It looks like our concern was justified - we have one unfair grade assignment.
Of course, we can use the graph to change the cut-offs to avoid that, or change the cut-offs directly in the table.
But what if we had a way to resolve the potential complaint just using buttons, such as "Upgrade XXX" or "Downgrade XXX"?
Buttons to modify cut-offs
Adding buttons to upgrade or downgrade a student's grade means that these buttons will modify the cut-off.
This behavior can be locally defined with the following trick:
When we click a button, it uses Javascript to modify the property that is supposed to contain the cut-off with a new cut-off. Let us focus on the function "ifunfair" for this task. Replace the code assigned @nameA</span> (line 154) withassigned @nameA.
<button onclick="""this.setAttribute('v', '@(gradeB - 0.1)')"""
v=toString(updateDecimal cutoffA)>Give @nameA to @envier
</button>
<button onclick="""this.setAttribute('v', '@(gradeA + 0.1)')"""
v=toString(updateDecimal cutoffA)>Give @nameB to @enviee
</button>
</span>
Grade by category
Grade
Students
Grouped
Cut-offs
Status
A+
3
13
96.8
A
5
90
A-
5
86
B+
5
9
82.9
B
3
76
Loida (82.8, B) might protest that
Kellee had almost the same grade (82.9) but was
assigned B+.
B-
1
68.5
C+
3
6
61
C
2
55.8
C-
1
40.4
D
1
1
29.5
E
0
0
You can now precisely resolve the dilemmas before they arrive, much faster than if you had to talk to students.
What is the fastest way to resolve all problems here? To downgrade or to upgrade? Can you guess from the graph?
Second lens: Group size
Sometimes we wish we could simply change the number of students in a group to change the cut-off.
In typical interfaces this is impossible, but in Sketch-n-Sketch
we can apply a lens to the displayed number of students.
This lens takes care of modifying the cut-offs in the backward direction.
Note that we never change the logic used to compute the count in the code - the change to the count in the output is used to change
the cut-offs in the code.
Replace the code -- We will write code here #8 (line 165) withupdateGroupCount bucketNum cutoff numStudents =
Update.lens2 {
apply (cutoff, count) = count
update {input=(cutoff, prevCount), outputNew=newCount} =
if newCount > prevCount then let
-- Lower the bar, we want more students
(CutOff _ cutoffBelow, stds2) = nth buckets (bucketNum+1)
(upgrading, staying) =
List.split (newCount-prevCount) stds2
Student _ newCutoff = last upgrading
in Ok (Inputs [(newCutoff, prevCount)])
else let -- Raise the bar, we want less students
(_, students) = nth buckets bucketNum
(staying, declassed) = List.split (
List.length students - (prevCount - newCount)) students
Student _ belowCutoff = hd declassed
in Ok (Inputs [(belowCutoff + 0.1, prevCount)])
} cutoff numStudentsReplace the code @List.length(students) (line 130) with@updateGroupCount(i)(cutoff)<|
List.length(students) Now try to change the number of students in one group to see the cut-offs change appropriately.
For example, you can change the number of students with a B+ to 4 (downgrading Kellee) or 6 (upgrading Loida).
Grade by category
Grade
Students
Grouped
Cut-offs
Status
A+
3
13
96.8
A
5
90
A-
5
86
B+
5
9
82.9
B
3
76
Loida (82.8, B) might protest that
Kellee had almost the same grade (82.9) but was
assigned B+.
B-
1
68.5
C+
3
6
61
C
2
55.8
C-
1
40.4
D
1
1
29.5
E
0
0
Final code
You can compare your code with the code used by this tutorial:
input_data = """Alice,64.7
Bob,78.5
Eve,57.9
Seya,100
Madelene,88.3
Mack,32.8
Clarita,90.5
Ulrike,98.5
Ferdinand,74.4
Tamara,93.5
Kellee,82.9
Leanne,83.7
Natalya,43.8
Leonore,84.5
Linette,98.5
Salley,88.4
Aleisha,88.6
Sabine,92.2
Ozella,60.9
Misti,86.7
Jenny,83.3
Nubia,92.3
Tennillee,95.1
Margaret,81.9
Loida,82.8
Cassandra,65.3
Derrick,66.4
Rudolph,83.2
Rafael,86.6"""
type Student = Student String Num
studentsGrades =
input_data
|> Regex.split "\r?\n"
|> List.map (\line -> case Regex.extract "^(.*),(.*)" line of
Just [name, gradeString] ->
Student name (Update.freeze <| String.toFloat gradeString)
)
type CutOff = CutOff String Num
cutoffs = [
CutOff "A+" 96.8,
CutOff "A" 90,
CutOff "A-" 86,
CutOff "B+" 82.9,
CutOff "B" 76,
CutOff "B-" 68.5,
CutOff "C+" 61,
CutOff "C" 55.8,
CutOff "C-" 40.4,
CutOff "D" 29.5,
CutOff "E" 0]
sortedGrades =
studentsGrades
|> sortBy (\(Student _ n1) (Student _ n2) -> n1 > n2)
numStudents = List.length sortedGrades
{softFreeze=keep} = Update
textstyle = "fill:black;font-family:monospace;font-size:12pt"
barChart x y w h = let
barWidth = w / numStudents
bars = List.indexedMap (\i (Student name avg) -> let
xi = x + (i * barWidth)
hi = h * (freeze 0.01! * avg)
yi = y + h - hi
in line 'blue' 2 xi yi (xi + barWidth) yi)
<| List.reverse sortedGrades
separators = cutoffs |> List.concatMap
(\(CutOff abc cutoff) -> let
y = (freeze y + freeze h * freeze 0.01! *
(freeze 100! - updateDecimal cutoff))
in
[line 'black' (keep 2) (keep x) y (keep <| x + w) y,
<text x=20 y=y style=textstyle>@abc</text>,
<text x=w+60 y=y style=textstyle>@abc</text>
])
background = rectWithBorder 'gray' 5 'lightgray' x y w h
in List.concatMap identity [
[background],
bars,
separators
]
width = 450
height = 389
chart = barChart 50 0 width height
updateDecimal = Update.lens {
apply cutOff = cutOff
update {input=oldCutOff,outputNew=newCutOff} =
Ok (Inputs [round (newCutOff * 10) / 10])
}
assignLetter avg =
List.find (\CutOff letter cutoff -> avg >= cutoff) cutoffs |>
case of
Nothing -> error <|
"""Could not find a letter for this average: @avg"""
Just (CutOff letter cutoff) -> letter
buckets = let
initBucket = List.map (\c -> (c, [])) cutoffs
addToBucket buckets letter student = case buckets of
(((CutOff bletter _) as c, v) as head)::tail ->
if bletter == letter then (c, v ++ [student])::tail
else head :: addToBucket tail letter student
[] -> error <| "Letter " + letter + " not found in buckets"
in
List.foldl (\((Student studName avg) as student) buckets ->
addToBucket buckets (assignLetter avg) student
) initBucket sortedGrades
displayStudents =
List.map (\Student name grade ->"""@name:@grade""")
>> String.join ","
displayBuckets =
<table>
<tr><th>Grade</th><th>Students</th><th>Grouped</th>
<th>Cut-offs</th><th>Status</th></tr>
@(List.indexedMap (\i (CutOff name cutoff, students) ->
<tr style="background:"+(
if i % 2 == 0 then "#dedede" else "white")>
<td>@name</td>
<td title=displayStudents(students)
>@updateGroupCount(i)(cutoff)<|
List.length(students)</td>
@(if i % 3 == 0 then
<td rowspan="3">@(
buckets |> List.drop i |> List.take 3
|> List.map (\(n, students) ->
List.length students) |> List.sum)
</td>
else [])
<td>@cutoff</td><td>@ifunfair(i)</td>
</tr>
) buckets)
</table>
ifunfair i =
if i == 0 then "" else let
epsilon = 0.5{0-1.9}
(CutOff nameA cutoffA, studentsAbove) = nth buckets (i - 1)
(CutOff nameB cutoffB, studentsBelow) = nth buckets i
in case (List.last studentsAbove, List.head studentsBelow) of
(Just (Student enviee gradeA),
Just (Student envier gradeB)) ->
if gradeA - gradeB < epsilon then
<span>@envier (@gradeB, @nameB) might protest that
@enviee had almost the same grade (@gradeA) but was
assigned @nameA.
<button onclick="""this.setAttribute('v', '@(gradeB - 0.1)')"""
v=toString(updateDecimal cutoffA)>Give @nameA to @envier
</button>
<button onclick="""this.setAttribute('v', '@(gradeA + 0.1)')"""
v=toString(updateDecimal cutoffA)>Give @nameB to @enviee
</button>
</span>
else <span></span>
_ -> <span></span>
updateGroupCount bucketNum cutoff numStudents =
Update.lens2 {
apply (cutoff, count) = count
update {input=(cutoff, prevCount), outputNew=newCount} =
if newCount > prevCount then let
-- Lower the bar, we want more students
(CutOff _ cutoffBelow, stds2) = nth buckets (bucketNum+1)
(upgrading, staying) =
List.split (newCount-prevCount) stds2
Student _ newCutoff = last upgrading
in Ok (Inputs [(newCutoff, prevCount)])
else let -- Raise the bar, we want less students
(_, students) = nth buckets bucketNum
(staying, declassed) = List.split (
List.length students - (prevCount - newCount)) students
Student _ belowCutoff = hd declassed
in Ok (Inputs [(belowCutoff + 0.1, prevCount)])
} cutoff numStudents
-- Displays the interface
main = <span><h1>Fair student grades</h1>
We want to give grades to our students.<br><br>
<span contenteditable="false">
<svg width=toString(width + 100)
height=toString(height)>@chart</svg></span><br>
<h2>Grade by category</h2>
@displayBuckets..
..
</span>