How to read these notes
Welcome to CSCI 0320/1340!
This page contains the lecture notes for the course. With JavaScript enabled, the table of contents will allow you to select a specific chapter. Likewise, the search icon should allow you to search for arbitrary words throughout all the notes. The lecture indexing is for Fall 2025. Some lectures are mainly done via slides, in which case we will attempt to link the slides here as well.
These notes are meant to be accompanied by the course live code repository, which contains a collection of code examples done in and out of class. The course assumes that students will review the live code.
This outline is subject to change, and notes are often modified in response to TA feedback or student Q&A before class. Some notes will be greyed out and inaccessible in the table of contents.
Exercises
Exercises are enclosed in expandable spoiler tags:
If you see an expandable piece of text, like this:
Think, then click.
Text for you to read after thinking or doing an exercise...
Success In 0320
Logistics
We have been moved to B&H.
- I check override requests every day after the course starts.
- If you need and get an override, please accept ASAP so that Brown and the department get accurate enrollment estimates. CAB does not report active overrides in the enrollment total, so delaying prevents others from getting in.
- If you are still shopping, keep 0320 in your cart so that you see announcements, etc. that we send to the list or post on EdStem. If you do not have 0320/1340 in your cart, I will not grant you an override.
- You need the prerequisites to take 0320 or 1340. If you haven't completed 0200, 0190, or a comparable course, you cannot take 0320 yet. (If you're a transfer student and concerned, let's talk; I was also a transfer student as an undergrad so I have some understanding of potential worries.)
When I say "0320" in the course notes, I mean both 0320 and 1340.
Welcome!
Before we get started, turn to the people sitting next you to now and introduce yourself.
The most important part of 0320 is collaboration. With the exception of the very first assignment, everything in 0320 is group work.
- You'll be submitting your work to repositories that everyone in the class can (eventually) read.
- You'll be helping one another solve bugs and design problems (even if you're in different project groups; this is a requirement). To make this work, we need a professional and friendly environment.
We'll be passing out index cards. I like to anonymously learn some important things about students in this class, so please fill one out, answering:
- What is a goal you have for 0320?
- What is something you need to succeed in 0320?
- What is something you're afraid of in 0320? At the end of class, leave your card either here in front or on (one single) chair in the back so I can collect.
0320 is Different
You'll want to read the missive. You may not need to read all of it super deeply yet, but you should note the ways that 0320 is different from what you're used to. E.g., you'll be getting formative feedback every week from a mentor. Like in industry, you will not be graded by an autograder.
TA Introductions
Speaking of mentors, let's introduce the TAs!
Disclaimer
This offering of 0320 is experimental. Much of the assignment content is new, as are the structured AI content. We've tried to minimize issues, but issues will happen. We'll act to mitigate them. Likewise, the perspective on AI we use is based on common practice as of summer 2025. I've reserved sprint 9 to address anything new that comes up (and have an assignment in reserve in case nothing does).
Immediate Advice
The first sprint goes out Monday, and there's a setup deliverable due before then. Please do it soon; you don't want to spend a lot of time early next week on setting up VSCode and so on.
Go to gear up sections. This semester, they're on Mondays and Tuesdays (with one exception around IPD). Switching doesn't require a "conference" switch in CAB.
Read lecture notes and livecode. More than reading the code examples, pull the code and experiment with them! I try to strike a balance between depth and drill in class, which means that when we have in-class work, these resources will be vital references. (Today, for example, I won't talk through every bullet point in these notes.)
Prototype early. (Paraphrasing Andy: Start soon! Start yesterday!)
Be aware that while this is an S/NC course, it is a lot of work. To quote a very wise remark from prior feedback: "You will work very hard, but not get an A".
Reading for Next Time
Read the missive.
Also, a short non-technical reading: Clever Manka. Read it. What does it have to do with software engineering?
There will be readings, but you won't need to pay for them.
Programming vs. Engineering
Let's say I asked you to open up your laptop and write me a sorting algorithm in the language of your choice. How would you react?
Think, then click.
Hopefully you didn't immediately start programming. I didn't really give you enough information, did I? I didn't say (among other things):- what kind of values you would have to sort;
- whether the sort needed to be stable, or in-place, or if there were any runtime requirements; or
- what the input interface should be (do you need to do file I/O or parse a CSV file?)
If you'd written insertion sort over lists of integers, but I needed to sort lists of records with a low worst-case runtime, you'd have had to start all over.
Get in the habit of asking questions. It will save you time and pain in the future.
Communicate clearly and often. It will save you time and pain in the future.
Document your assumptions and needs. It will save you time and pain in the future.
Don't misunderstand me: you'll work with a lot of code in this class. But just writing code is, frankly, not good enough.
What makes "good software"?
There are many valid viewpoints on this. In the context of 0320, we're going to think about a few dimensions.
- Is the software ready for change? (Can it be extended easily, and without modifying the original code?)
- Is the software safe from bugs? (Not just "bug free", but resilient to the bugs of other software that might call it, or that it might call.)
- Is the software easy to understand? (Is it well documented? Is it designed in a way that other engineers will be able to follow without a lot of work? Is it clear how to use it?)
- Is the software fit to purpose? (Does it do what it is supposed to do? Figuring that out involves a lot of work, because other people are involved.)
That last category is incredibly broad. For me, it incorporates issues beyond functional correctness like:
- performance,
- security,
- harm minimization, etc.
The 0320 Collaboration Standard
This may be the weirdest course you'll take during your time at Brown CS. It will also challenge you in new ways. That is deliberate.
Our goal is not to teach you to do better at programming courses, or even computer science courses in general. Our goal is to teach you to be a better engineer, period. That goal has some natural consequences.
The collaboration policy is part of that. We'll use Git, an industry standard version control system. If you haven't had Git experience yet, that's OK! Here's an example of how it works on a project I just happen to have here on my laptop. By the way, I'm using IntelliJ to work on this code. This course strongly encourages you to use (and supports) IntelliJ; the gear up sections today and tomorrow can help you set it up.
One advantage of using Git is that all the code from lecture will be available on a public repository in the F25
folder (for Fall 2025) and also in the vignettes
subfolder, which I use for small examples that may or may not appear in class. You can clone the repository to experiment with the code on your own machine. You should clone this repository as soon as you're able; we'll use it for in-class exercises, and it gives you the ability to experiment with changing the code on your own machine. Starting next week, I will assume that you have the repository cloned on your laptop, if you bring one.
What About AI?
We'll use it. Please see the missive. There's lots of new stuff, and I've written enough text about it already. :-)
How Does Grading Work?
You may look at the first assignment and think "OMG I am not prepared!" But you are. This course is designed to prompt this feeling—in a safe environment, so you can build the skills necessary to navigate unpreparedness. The course works for those who embrace our (admittedly unusual) system.
If you're a highly prepared student and you find that even most supplemental problems aren't challenging you: this is your chance to cut loose. Impress me. Final demos are public for everyone, but for Master's students and undergraduates who opt in, they will be publicized.
0320 is mandatory S/NC. We award "S with distinction" based on completing supplemental sprint challenges, exceptional professionalism, and strong performance on the term project.
- The sprints let you demonstrate command of new technical skills, which we give formative feedback on.
- The term project lets you show you can apply those skills in a new context of your own. Expectations are stronger here: I want everyone to finish 0320 with a good item for their portfolio (whether you're going into traditional SWE or not).
The missive talks at length about grading; I won't try to duplicate all that information here. However, here are some key features of 0320's grading.
1-week sprints: You'll have a deliverable every week. This gives us a better cadence for feedback. You'll alternate between asynchronous video demos and synchronous demos each week, and the demos (and your answers to questions they ask) are the primary way your mentors will generate feedback.
Feedback Spreadsheet: You'll receive access to a spreadsheet where your mentors will leave formative feedback every week, along with whether you've met expectations for the week. We'll expect you to work on addressing this feedback between sprints.
No Hidden Rubrics: There are no secret rubrics or hidden test suites in 0320; we want you to know what our expectations are. You can see our grading instructions, and a recipe for giving demos, linked in the first sprint. (We do provide mentors with some additional private material to help them give feedback efficiently, but everything in that material is also in the sprint handouts.)
Collaboration Matters We would like to more strongly encourage collaboration between students. You'll need to earn a certain number of crests throughout the semester and per sprint. You will handle reporting this to us on your feedback sheet.
Testing Matters: We'll talk more about testing on Tuesday, but it should go without saying that in 0320, we take testing seriously. The handouts contain pretty good advice on meeting our requirements. Read them.
TypeScript
We're going to be using TypeScript for nearly all of this course. Last year, we used Java for the "back end" and TypeScript on the "front end", and it was tough to devote enough time to general TypeScript practice. So we're doing everything in one language. The first two weeks give you an introduction.
TypeScript is essentially "JavaScript with types". It transpiles to JavaScript, meaning that Node and your browser only need to "understand" JavaScript. Why do we need types here, though?
Equality
When I'm learning a new language, I like to identify language features that I rely on, and experiment with them. Coming up with these facets isn't always easy, which is why we're doing it together. For instance, let's check out equality, a deceptively simple yet subtle idea that many languages differ on. We've got a couple options for our experiments:
- If we were interested in web behavior, I'd use the console of whatever browser I wanted. (Safari's JS console is Command-Option-C, if you've enabled developer tools.
- If we were interested in general program behavior, I'd just run Node. Node is a runtime library for JavaScript that's often used for backend servers—the same sort of setting you might see a Java program used for.
Behavior isn't always the same between these. We'll get to that later. For now, let's use Node.
> 15 == "15"
true
> 15 == true
false
> 1 == true
true
0 == false
true
Already we've learned a great deal. JavaScript's ==
operator performs type conversion before comparing values. It allows us to pretend that the number 1
is "true" and that the number 0
is "false", but other numbers aren't equivalent to either.
Those of you who have programmed in Racket before: notice this is different from Racket! In Racket, every non-false value (Racket calls false #f
) is considered true. The same is true in many other languages.
JavaScript has a second equality operator, ===
, that checks for equality without type conversion. So:
> 15 === "15"
false
Java also has two different types of equality (==
and the .equals
method), but there the difference has nothing to do with implicit type conversion, but with references vs. structural equality. JavaScript's implicit conversion adds an extra layer of complexity.
Arithmetic
Let's try a few arithmetic operators that are often overloaded across different languages.
> '1' + 1
'11'
> 1 + '1'
'11'
> 1 - '1'
0
> '1' - 1
0
JavaScript tries to "do the reasonable thing" whenever possible. This can lead to surprising bugs; When in doubt, use explicit conversion functions (e.g., parseInt
). In fact, let's try a couple more (Credit for these to Gary Bernhardt from 2012---it's 4 minutes long; watch it.) For context, []
denotes an array or list in JavaScript and {}
denotes an object.
> [] + []
""
> {} + {}
'[object Object][object Object]'
> [] + {}
[object Object]
Do you see why TypeScript is helpful, now? JavaScript lets you throw off the constraints of the type system, but those constraints are like a safety belt on a roller coaster.
Supplemental: What's Really Happening?
The details involve how JavaScript is implicitly converting between different types. Before applying +
, it converts to a "primitive" type, which for an object is a string. An empty array is converted to the empty string. And so on. The details would consume a full class, or more! Just beware, and embrace types.
Objects
While we won't be using TypeScript classes much in 0320, we can't avoid using objects. Objects are collections of fields; there's no sharp distinction between fields that are methods and fields that are data. If you've used Python before, these may remind you of dictionaries. For example:
> const cat = {talk: function() { console.log('meow'); }}
undefined
This defines a value for the variable cat
. The console (in both a browser and in Node) displays undefined
because that's the return value of the definition statement.
> cat.talk
function talk()
The value of cat.talk
is a function. We can pass it around the same way we would a string or a number. But we can also call it:
> cat.talk()
meow
undefined
Again, the console is printing the "meow" and then saying what the return value was. You won't need to worry about this apparent double result when you're working with TypeScript in general.
Don't get too attached to the method-style syntax, however. Objects in TypeScript are more flexible. A field is a field, no matter what it contains:
> cat["talk"]
function talk()
> cat["talk"]()
meow
undefined
You'll get a lot more practice with TypeScript over the next 2 weeks.
TypeScript Types
TypeScript works a little bit differently from Java. See the Thinking Like an Engineer: Types document for more. I'll cover some of them live today and Tuesday.
We'll be using a library called Zod for validation, and to enrich what types can give us. Zod appears in the very first sprint! I'll be talking about it on Tuesday, but do not wait until then to start the sprint. There are other, Zod-free deliverables.
The Debugging Recipe
Rule One
Never debug when tired or angry. If you must debug when tired, commit to a stopping time. Set a timer or ask a friend to help you keep that commitment.
Identify What's Wrong
Assemble your knowledge. Write no more than two sentences for each question:
- What is the purpose of the code you're working on?
- What steps can you perform to reproduce the bug?
- What is the expected behavior/result, and what is the unexpected actual behavior/result?
- Why do you expect the result that you expect?
Tell the Story
Now describe how you think the system operates as it approaches the unexpected actual result. Write your description as a series of steps. Use no more than one or two sentences for each step, but each step should be testable as a hypothesis about how the system works.
Localization
Confirm each step in your description is accurate. Use any reasonable means (e.g., print statements or a debugger). The first step where the program behaves unexpectedly is a possible location for the original bug. More commonly, it contains the call site for the actual buggy code.
Record this location, along with the expected and actual behavior.
Explanation
If you have a hypothesis about the cause of the bug, make an experiment to see if you are correct.
- If yes, proceed to fix the problem. Whenever possible, add a regression test so that this bug will be quickly identified if it happens again. Re-run all tests to confirm that your "fix" hasn't broken something else.
- If you have no hypothesis, or your hypothesis was incorrect, increase your level of detail about that single step. Look for unstated assumptions and dependencies; often, you'll find you left something important out of the original story. Then repeat the localization step.
Agile, Lambdas, and Validation
Logistics
If you haven't yet installed Node or cloned the starter code, it would be a good idea to do that ASAP.
We don't have a no-laptops policy in 0320. But if you're going to play chess or write an essay while in class, please sit where you won't distract anyone sitting behind you.
Agile Development
We're going to be using a few terms of art in 0320. While we do put a bit of our own spin on these (largely because this is a class, and not a full-time job) it's important to understand what's going on.
We say that a development methodology is agile if, broadly speaking, it prioritizes the ability to change plans in response to regularly-sought feedback. Once we try to define it more precisely, you'll find multiple competing definitions, and many competing methodologies that purport to be "agile". But change and feedback are good enough for us today.
Usually, you'll see the term used in contrast to "waterfall" development, where the software project proceeds along a linear path, like this:
- Requirements
- Specifications
- Design
- Implementation
- Testing
- Maintenance
Of course, all of these "phases" still exist in an agile project! The difference is that (e.g.) customer feedback on an early demo might result in changes to the requirements, or some trouble with testing an early prototype might mean changing the design to make testing easier. To make this possible, agile projects usually start development early, expecting that some, or even most, of that prototype code will be replaced later. But something must be possible to demo at every stage, or feedback is hard to obtain.
Sprints
Agile development is often divided into short periods of development effort: sprints. The duration of a sprint varies depending on the project and company. In 0320, we've organized development into one-week sprints with relatively small requirements for each. Each sprint builds on the last.
Organizing sprints is an important skill for project managers. Timelines need estimates (and the ability to alter those estimates when needed), tickets need to be managed, and so on. We'll gloss over most of this in 0320, although your term project will need some attention to project-management tasks like these.
User Stories
A user story is a short description of desired behavior. You'll often find these used in requirements documents for agile projects. There are a few templates, but we (mostly) follow the "As a USER-ROLE I can NEEDED-BEHAVIOR so that TASK-OR-BENEFIT" pattern. Look at the user stories in sprint 1.1, and you should see this pattern.
It's important to remember that, although they follow a template, user stories are informal: they may not be enough by themselves to really describe what the customer needs. To help bridge this gap, they are often accompanied by acceptance criteria, which give additional lower-level requirements. But these are still fairly informal, and there's often a need for more precise specification to be agreed upon between developers!
But what makes a good user story? A user story describes a narrow, demoable piece of functionality that can reasonably be accomplished in a single sprint. Often, you might start with a rough user story and realize that it's too big, and then split it into multiple stories. If users request an enhancement to an existing story, commonly these would be added as new stories (and more infrequently as additional acceptance criteria).
If you're building a software package that is intended for other developers to use, they are potential users! In this class we will be giving you user stories from the developer-user perspective in addition to the end-user perspective. Your API design, documentation, etc. will matter to developer users.
Reading: Clever Manka
There's a fairy tale I like: Clever Manka, from a 1920's compilation called The Shoemaker's Apron, available on Project Gutenburg here. I'll reproduce the text in full here, within these spoiler tags.
Clever Manka
There was once a rich farmer who was as grasping and unscrupulous as he was rich. He was always driving a hard bargain and always getting the better of his poor neighbors. One of these neighbors was a humble shepherd who in return for service was to receive from the farmer a heifer. When the time of payment came the farmer refused to give the shepherd the heifer and the shepherd was forced to lay the matter before the burgomaster.
The burgomaster, who was a young man and as yet not very experienced, listened to both sides and when he had deliberated he said:
"Instead of deciding this case, I will put a riddle to you both and the man who makes the best answer shall have the heifer. Are you agreed?"
The farmer and the shepherd accepted this proposal and the burgomaster said:
"Well then, here is my riddle: What is the swiftest thing in the world? What is the sweetest thing? What is the richest? Think out your answers and bring them to me at this same hour tomorrow."
The farmer went home in a temper.
"What kind of a burgomaster is this young fellow!" he growled. "If he had let me keep the heifer I'd have sent him a bushel of pears. But now I'm in a fair way of losing the heifer for I can't think of any answer to his foolish riddle."
"What is the matter, husband?" his wife asked.
"It's that new burgomaster. The old one would have given me the heifer without any argument, but this young man thinks to decide the case by asking us riddles."
When he told his wife what the riddle was, she cheered him greatly by telling him that she knew the answers at once.
"Why, husband," said she, "our gray mare must be the swiftest thing in the world. You know yourself nothing ever passes us on the road. As for the sweetest, did you ever taste honey any sweeter than ours? And I'm sure there's nothing richer than our chest of golden ducats that we've been laying by these forty years."
The farmer was delighted.
"You're right, wife, you're right! That heifer remains ours!"
The shepherd when he got home was downcast and sad. He had a daughter, a clever girl named Manka, who met him at the door of his cottage and asked:
"What is it, father? What did the burgomaster say?"
The shepherd sighed.
"I'm afraid I've lost the heifer. The burgomaster set us a riddle and I know I shall never guess it."
"Perhaps I can help you," Manka said. "What is it?"
So the shepherd gave her the riddle and the next day as he was setting out for the burgomaster's, Manka told him what answers to make.
When he reached the burgomaster's house, the farmer was already there rubbing his hands and beaming with self-importance.
The burgomaster again propounded the riddle and then asked the farmer his answers.
The farmer cleared his throat and with a pompous air began:
"The swiftest thing in the world? Why, my dear sir, that's my gray mare, of course, for no other horse ever passes us on the road. The sweetest? Honey from my beehives, to be sure. The richest? What can be richer than my chest of golden ducats!"
And the farmer squared his shoulders and smiled triumphantly.
"H'm," said the young burgomaster, dryly. Then he asked:
"What answers does the shepherd make?"
The shepherd bowed politely and said:
"The swiftest thing in the world is thought for thought can run any distance in the twinkling of an eye. The sweetest thing of all is sleep for when a man is tired and sad what can be sweeter? The richest thing is the earth for out of the earth come all the riches of the world."
"Good!" the burgomaster cried. "Good! The heifer goes to the shepherd!"
Later the burgomaster said to the shepherd:
"Tell me, now, who gave you those answers? I'm sure they never came out of your own head."
At first the shepherd tried not to tell, but when the burgomaster pressed him he confessed that they came from his daughter, Manka. The burgomaster, who thought he would like to make another test of Manka's cleverness, sent for ten eggs. He gave them to the shepherd and said:
"Take these eggs to Manka and tell her to have them hatched out by tomorrow and to bring me the chicks."
When the shepherd reached home and gave Manka the burgomaster's message, Manka laughed and said: "Take a handful of millet and go right back to the burgomaster. Say to him: 'My daughter sends you this millet. She says that if you plant it, grow it, and have it harvested by tomorrow, she'll bring you the ten chicks and you can feed them the ripe grain.'"
When the burgomaster heard this, he laughed heartily.
"That's a clever girl of yours," he told the shepherd. "If she's as comely as she is clever, I think I'd like to marry her. Tell her to come to see me, but she must come neither by day nor by night, neither riding nor walking, neither dressed nor undressed."
When Manka received this message she waited until the next dawn when night was gone and day not yet arrived. Then she wrapped herself in a fishnet and, throwing one leg over a goat's back and keeping one foot on the ground, she went to the burgomaster's house.
Now I ask you: did she go dressed? No, she wasn't dressed. A fishnet isn't clothing. Did she go undressed? Of course not, for wasn't she covered with a fishnet? Did she walk to the burgomaster's? No, she didn't walk for she went with one leg thrown over a goat. Then did she ride? Of course she didn't ride for wasn't she walking on one foot?
When she reached the burgomaster's house she called out:
"Here I am, Mr. Burgomaster, and I've come neither by day nor by night, neither riding nor walking, neither dressed nor undressed."
The young burgomaster was so delighted with Manka's cleverness and so pleased with her comely looks that he proposed to her at once and in a short time married her. > >"But understand, my dear Manka," he said, "you are not to use that cleverness of yours at my expense. I won't have you interfering in any of my cases. In fact if ever you give advice to any one who comes to me for judgment, I'll turn you out of my house at once and send you home to your father." > >All went well for a time. Manka busied herself in her house-keeping and was careful not to interfere in any of the burgomaster's cases. > >Then one day two farmers came to the burgomaster to have a dispute settled. One of the farmers owned a mare which had foaled in the marketplace. The colt had run under the wagon of the other farmer and thereupon the owner of the wagon claimed the colt as his property. > >The burgomaster, who was thinking of something else while the case was being presented, said carelessly: > >"The man who found the colt under his wagon is, of course, the owner of the colt." > >As the owner of the mare was leaving the burgomaster's house, he met Manka and stopped to tell her about the case. Manka was ashamed of her husband for making so foolish a decision and she said to the farmer: > >"Come back this afternoon with a fishing net and stretch it across the dusty road. When the burgomaster sees you he will come out and ask you what you are doing. Say to him that you're catching fish. When he asks you how you can expect to catch fish in a dusty road, tell him it's just as easy for you to catch fish in a dusty road as it is for a wagon to foal. Then he'll see the injustice of his decision and have the colt returned to you. But remember one thing: you mustn't let him find out that it was I who told you to do this." > >That afternoon when the burgomaster chanced to look out the window he saw a man stretching a fishnet across the dusty road. He went out to him and asked: > >"What are you doing?" > >"Fishing." > >"Fishing in a dusty road? Are you daft?" > >"Well," the man said, "it's just as easy for me to catch fish in a dusty road as it is for a wagon to foal." > >Then the burgomaster recognized the man as the owner of the mare and he had to confess that what he said was true. > >"Of course the colt belongs to your mare and must be returned to you. But tell me," he said, "who put you up to this? You didn't think of it yourself." > >The farmer tried not to tell but the burgomaster questioned him until he found out that Manka was at the bottom of it. This made him very angry. He went into the house and called his wife. > >"Manka," he said, "do you forget what I told you would happen if you went interfering in any of my cases? Home you go this very day. I don't care to hear any excuses. The matter is settled. You may take with you the one thing you like best in my house for I won't have people saying that I treated you shabbily." > >Manka made no outcry. > >"Very well, my dear husband, I shall do as you say: I shall go home to my father's cottage and take with me the one thing I like best in your house. But don't make me go until after supper. We have been very happy together and I should like to eat one last meal with you. Let us have no more words but be kind to each other as we've always been and then part as friends." > >The burgomaster agreed to this and Manka prepared a fine supper of all the dishes of which her husband was particularly fond. The burgomaster opened his choicest wine and pledged Manka's health. Then he set to, and the supper was so good that he ate and ate and ate. And the more he ate, the more he drank until at last he grew drowsy and fell sound asleep in his chair. Then without awakening him Manka had him carried out to the wagon that was waiting to take her home to her father. > >The next morning when the burgomaster opened his eyes, he found himself lying in the shepherd's cottage. > >"What does this mean?" he roared out. > >"Nothing, dear husband, nothing!" Manka said. "You know you told me I might take with me the one thing I liked best in your house, so of course I took you! That's all." > >For a moment the burgomaster rubbed his eyes in amazement. Then he laughed loud and heartily to think how Manka had outwitted him. > >"Manka," he said, "you're too clever for me. Come on, my dear, let's go home." > >So they climbed back into the wagon and drove home. > >The burgomaster never again scolded his wife but thereafter whenever a very difficult case came up he always said: > >"I think we had better consult my wife. You know she's a very clever woman."
Stories like this appear in numerous places throughout the world. For example, India has the story of Hiranyakashipu, a king with a boon protecting him from death "by day or by night", "indoors and out", etc. Narasimha, an avatar of Vishnu, finds a way around the riddle.
These stories are so common that I might say humans find something universally valuable about them. but what in the world does a fairy tale have to do with testing?
If you're reading these notes without having been in lecture: think about these questions first, before you read the answer! If you don't, you'll be robbing yourself of a chance to participate and learn. (The collapsible sections are meant to help you avoid spoilers while you think.)
Don't worry about whether your answer is the same as mine. Especially in 0320, there are often many different good answers, even to technical questions.
Think, then click!
Clever Manka is a boundary condition tale. There is something special about boundary conditions and how they challenge our preconceptions. As testers, we should give boundaries the respect they deserve.
(Are you aware of more mythological or literary settings for this sort of boundary-condition riddle? Share them with Tim!)
Boundary Conditions: Testing and Defensive Programming
Here's an example: "I have tested a positive number, and I have tested a negative number." Surely all numbers are positive or negative. (Is this true?)
Here's another example: "I have tested this function, which accepts a Java Boolean
, on both values: true and false." (What's missing?)
Never assume that the obvious partition of the space actually covers the space; be on the lookout for special cases, outliers, and even new dimensions about which to think. This isn't only about testing, either---see, for example, the Falsehoods Programmers Believe About Time. Part of programming defensively is trying to avoid making unnecessary assumptions, while still allowing for extensibility. Keep this in mind as we start to code (and test) together.
Runtime Validation
Ok, so we're on the lookout for faulty assumptions that we might make when programming. But what about other people who might be writing code we depend on—or even code that calls ours? We can't solve this problem, and (as careful as we might be) we can't entirely solve it for ourselves, either. So we'll need to make our code robust against bugs, whether they are our mistakes or others'.
Runtime validation involves checking for issues with data your code is given, unexpected changes in state, and so on. Here are two examples.
Example 1: User Input
Suppose that a user just typed in a pair of numbers and we want to know if one is bigger than another. Because they just arrived, they will be strings, so we need to convert them. No problem:
function rawGreaterThan(arg1: string, arg2: string): boolean {
return parseInt(arg1) > parseInt(arg2)
}
This seems reasonable enough, but it forgets something important about numbers in JavaScript: NaN
(not a number) is a number. Since TypeScript is JavaScript with added protection, the same is true in TypeScript. Imagine that a user accidentally types "!00" instead of "100". In Java, we'd get an exception since "!00"
can't be converted to a number. But in TypeScript, parseInt("!00")
produces a value: NaN
. And NaN
isn't greater or less than anything! (It can't even be equal to itself: NaN == NaN
evaluates to false
.) So:
> parseInt("!00") > parseInt("100")
false
> parseInt("!00") <= parseInt("100")
false
Oh, dear. It's one of those pesky boundary conditions! But the problem is invisible outside of our function. The caller only gets a boolean. Let's do some validation to protect them and us. We'll first check that both arguments can be converted:
function rawGreaterThan(arg1: string, arg2: string): boolean | undefined {
const num1 = parseInt(arg1)
const num2 = parseInt(arg2)
// num1 === NaN won't work! Instead:
if(Number.isNaN(num1) || Number.isNaN(num2)) return undefined
return parseInt(arg1) > parseInt(arg2)
}
Now the caller will be given something special if there's an issue converting the input to numbers. We could improve this, actually: undefined
is better than hiding the problem, but it doesn't communicate much. We could throw an error, but instead let's explore building a better error value.
interface ConversionError {
error: "parseInt"
arg1: string
arg2: string
}
Yeah, that's a string literal as the type of the error
field. In TypeScript, this means the type containing only that string. Thus, the error
field can only ever be the string "parseInt"
So why have that field at all? Because if our larger program can ever produce a different kind of conversion error, we can add other options via union types:
interface ConversionError {
error: "parseInt" | "parseFloat" // | ... | ... | etc.
arg1: string
arg2: string
}
If we said error: string
instead, we'd be allowing any old string. Sometimes this is what you want, but if you're establishing a protocol for reporting errors, you probably want an enumeration of error codes, not arbitrary strings. Anyway, now we can write:
function rawGreaterThan(arg1: string, arg2: string): boolean | ConversionError {
const num1 = parseInt(arg1)
const num2 = parseInt(arg2)
if(Number.isNaN(num1) || Number.isNaN(num2))
return {error: "parseInt", arg1: arg1, arg2: arg2}
return parseInt(arg1) > parseInt(arg2)
}
The caller is now being told exactly what the problem is, not only that they had some kind of problem. This is only possible because we're now validating the input.
Reading Data
The same problem occurs if you're reading in data from files or responses to your web requests. Suppose the registrar stored student information in a comma-separated-value (CSV) file:
Name,Credits,Email
Tim Nelson,10,Tim_Nelson@brown.edu
Nim Telson,11,MYAWESOMEEMAIL
If I expect the third column to contain an email address, I'm going to be very surprised with the data I get from the second row. Hopefully the registrar is validating every column of every row so they can avoid relying on the bad data. But now it's not as simple as it was before: do I need to manually write a validation function that matches all possible email addresses? Argh!
Library Support: Zod
Fortunately, people write and share libraries that solve real problems. We'll be using a library called Zod, which is built to help us with exactly these kinds of validation tasks. Zod is free, open source, and widely used. Zod works off of something called a schema, which is kind of analogous to a type (not entirely; more on this later). Here's a schema to validate each CSV row according to the registrar's expectations:
const studentRowSchema = z.tuple([z.string(), z.coerce.number(), z.email()])
Zod schemas are compositional. z.string()
is a schema that matches any string. z.coerce.number()
is a schema that matches any number or any string that can be converted to a number. z.email()
is a schema that matches strings containing email addresses. And z.tuple
takes an array of schemas and matches arrays of exactly the same length, where each of the sub-schemas matches its corresponding array element.
I hear that at this point, you might be enhancing a CSV parser. Probably it splits a big file into rows, and then splits each row by ,
(or something like that) to produce an array. So for the 2-row example above, you might get a string[][]
that looks like:
[["Tim Nelson","10","Tim_Nelson@brown.edu"],
["Nim Telson","11","MYAWESOMEEMAIL"]]
The first row will match the schema, but the second won't: "MYAWESOMEEWMAIL" isn't an email address. Zod will give a structured error for this failure, although it contains even more detail than the parseInt
error we built before.
Zod makes validating external data much, much easier. Whenever validation becomes non-trivial, stop writing ad-hoc if
statements and use Zod instead.
Anonymous Functions
Zod doesn't just do validation: it can also transform data into new shapes. But it needs us to tell it how that transformation works: a strategy function that says how to map the old shape into the new one. Often, we'll use an anonymous function for this. Anonymous functions are sometimes called "lambdas" (e.g., in Python). Even Java has them in the form of the Function<T, R>
interface and special syntax to build them concisely. TypeScript's syntax for these is quite similar to Java's.
For example, let's turn a (validated) CSV row into an object instead:
const studentRowSchema = z.tuple([z.string(), z.number(), z.email()])
.transform(arr => ({name: arr[0], credits: arr[1], email: arr[2]}))
Now the first row becomes an object: {name: "Tim Nelson", credits: 10, email: "Tim_Nelson@Brown.edu"}
.
Passing functions arguments to to other functions is so powerful that it appears in multiple contexts. In Object-Oriented Programming, you see it everywhere under the name of "strategy pattern". For example, Java's Collections.sort
method takes an object called a Comparator
. A Comparator
implements a method that takes two elements of the collection and says whether one is greater than another. In this way, the Collections
library allows a single type to be sorted many different ways.
Many objects implement Java's Comparable
interface, and Collections.sort
will indeed use that if no comparator is provided. The advantage of taking arbitrary comparators is in its flexibility: the caller might want to sort in ascending or descending order for example. Records might be sorted by one key or another key, and so on. The strategy pattern is all about flexibility.
Exercise
I'm not going to start the required exercises this early in shopping period. But I'd still like us to get some practice with TypeScript.
Tools Challenge: package.json
Let's start with the toolchain we're using in the course. You've probably cloned the starter repository by now. Let's look at the package.json
file together. JSON means "JavaScript Object Notation", and it's a very common text data format. You'll see fields like dependencies
and scripts
and so on. What do you think they mean? How do they interact with the npm
console command?
Research has shown that instruction is more effective if students commit to a hypothesis first, rather than being told the answer immediately. I also want you to finish 0320/1340 with a confidence in making guesses that might be wrong.
Design Challenge: Unchecked Casting
Just like Java, TypeScript has a way to tell the type checker to be quiet because you know best. We call these "unchecked typecasts". In TypeScript, this happens whenever a value has type any
. You might explicitly cast this (via as any
) or TypeScript might infer the type because it has no further context. The any
type exists because TypeScript needs to interoperate with untyped JavaScript, and it's dangerous to keep around if you don't need it.
Union types mean that TypeScript may be "uncertain" about your code. Here's an example:
function whatToDo(input: string[] | number): string {
return input[0]
}
If the input is a string array, this is fine. But what if it's a number? TypeScript reports this problem with the following error:
Element implicitly has an 'any' type because expression of type '0' can't be used to index type 'number | string[]'.
Property '0' does not exist on type 'number | string[]'.ts(7053)
What should you do about this?
Think, then click!
Any time you see something like "Element implicitly has an 'any' type" you should be suspicious. It means that you haven't given TypeScript enough information. This information might need to go in your function headers, in your variable declarations, or the logical flow of your code. Notice what happens when I add an if
statement:
function whatToDo(input: string[] | number): string {
if(typeof input === "number") return ""
return input[0]
}
The error goes away! TypeScript looks at your conditionals for hints, and uses those hints to resolve union types and other kinds of uncertainty. This is called narrowing, because TypeScript is able to reduce the size of the set of possible values.
We'll talk more about this trick soon.
The typeof
operator is technically part of JavaScript, and it isn't very precise at all! JavaScript has only a few "types":
string
;object
;number
; and ...only a few others.
JavaScript, on its own, makes no distinction between an array and an object, or between two different kinds of object. This is one thing TypeScript handles a lot better, but it still can use these basic JavaScript checks.
Narrowing and Refinements
My Homework
Last time, a student asked what would happen if we didn't return an object directly, and instead worked with it before returning it.
function rawGreaterThan(arg1: string, arg2: string):
boolean | ConversionError {
const num1 = parseInt(arg1)
const num2 = parseInt(arg2)
if(Number.isNaN(num1) || Number.isNaN(num2)) {
// This produces an error
const result = {error: 'parseInt', arg1: arg1, arg2: arg2}
return result
// Before, it was:
//return {error: 'parseInt', arg1: arg1, arg2: arg2}
}
return num1 > num2
}
The error isn't super helpful:
Type '{ error: string; arg1: string; arg2: string; }' is not assignable to type 'boolean | ConversionError'.
Type '{ error: string; arg1: string; arg2: string; }' is not assignable to type 'ConversionError'.
Types of property 'error' are incompatible.
Type 'string' is not assignable to type '"parseInt" | "parseFloat"'
But it's surprising, right? Look at that first type. TypeScript isn't using the literal string: it's inferring string
for the error
field. Initially, TypeScript can directly infer the return type and then check for consistency. TypeScript isn't smart enough to carry that inference over to the value of result
without help. So we'll provide it:
const result: ConversionError = {error: 'parseInt', arg1: arg1, arg2: arg2}
Now the error goes away. Sometimes we do need to give TypeScript a little help.
Testing as a Human
Suppose your job is to build a statistical app that summarizes United Nations data on population, GDP, and so on. You need to test your app, so think of a country. What country are you thinking of?
Think, then click!
Chances are, the country you thought of was:
- close to home;
- large; or
- in the news often.
And it's even more likely that the country you thought of was currently in existence. You probably didn't say "the USSR" or "Austria-Hungary". And note that my choices there were all limited by my own historical knowledge. I went and looked up more after writing that sentence. Even if we only count nations that existed after the U.N. was created, there are many: the Republic of Egypt (1953-1958), the Fourth Brazilian Republic (1946-1964), etc.
This is an example of something called availability bias (or the availability heuristic). All humans exhibit it, and usually it's an advantage: just like caching in a program, our brains tend to recall information in cache. For us, it's an energy-saving measure.
I'm not a cognitive scientist! If you want to learn more about this in depth, take a CLPS class. But even so, let's ask: How does this cognitive phenomenon impact software testing?
Think, then click!
You probably test what you have loaded into your mental cache. If you aren't thinking of it at the moment, or haven't been thinking of it recently, you likely won't test it unless you work to find examples outside your current context.
Even worse, if you aren't aware of the thing to begin with, you won't think to test it. Beware of the kind of thing that Iain Banks called an "outside context problem", translated from fiction into the real world of testing. This is why getting outside feedback from others can be so valuable for testing.
TypeScript: Narrowing
We saw last time that TypeScript uses the control flow of your program to infer type information. You can read more about this in the TypeScript documentation. For now, let's do a few narrowing exercises.
Recall: last time we used the typeof
operator to check whether a value was a number
. This operator is from JavaScript, and there aren't many "types" in that setting: string
, number
and a few others. To really use TypeScript well, we'll need more than typeof
.
Calling a schema's safeParse
method in Zod will return either a "success" or "failure" type. If we call parse
instead then we might get an exception. I think safeParse
is better, because it lets us keep some useful context for the caller in a normal value. Here's an example. First, we'll define the (more simple) schema from last time:
// Mouse over type: z.ZodTuple<[z.ZodString, z.ZodCoercedNumber<unknown>, z.ZodEmail], null>
const studentRowSchema = z.tuple([z.string(), z.coerce.number(), z.email()])
We can make the types cleaner by creating a new identifier and using z.infer
. But we have to use it right, or we get a strange error:
// Property 'infer' does not exist on type
z.infer<typeof studentRowSchema>
The error isn't great, but it means we're misusing infer
. It's not a function in TypeScript; it operates on types. So we can't just call it like it is a normal function; we need to put it into a type context:
type StudentRow = z.infer<typeof studentRowSchema>
Now let's call safeParse
:
// We don't need the explicit type annotation here. You can mouse over without it and you'll see:
// type ZodSafeParseResult<T> = z.ZodSafeParseSuccess<T> | z.ZodSafeParseError<T>
// Either way, this is a union type!
const result: z.ZodSafeParseResult<StudentRow> = studentRowSchema.safeParse(["Tim Nelson", 10, "Tim_Nelson@brown.edu"])
The result
value is either a success or an error. We can try to get the data either way, but it will be undefined
in the error case. So this won't work as written:
// Error: 'result.data' is possibly 'undefined'
result.data[0]
A great time to use narrowing!
// This works: we're directly checking that the value is not undefined
if(result.data) {
result.data[0]
}
// This also works
if(result.success) {
result.data[0]
}
Wait: how is TypeScript able to infer the type of result.data
based on result.success
? Let's look at the definition of these types.
// From Zod's library code
export type ZodSafeParseResult<T> = ZodSafeParseSuccess<T> | ZodSafeParseError<T>;
export type ZodSafeParseSuccess<T> = {
success: true;
data: T;
error?: never;
};
export type ZodSafeParseError<T> = {
success: false;
data?: never;
error: ZodError<T>;
};
Notice that the type of success
differs. It can only be true
in a ZodSafeParseSuccess
. That's how TypeScript narrows the type in the second case.
Added after class: Using result.success
to narrow the type of result.data
worked above. But if we give an intermediate name to result.data
outside the narrowed scope, that variable will have the union type, and the link is broken for TypeScript:
const student: StudentRow | undefined = result.data
if(result.success) {
console.log(student[0]) // type error: possibly undefined
}
But this works, because the variable is declared within the narrowed scope:
if(result.success) {
const student2 = result.data
console.log(student2[0])
}
Escaping the any
type
There are built-in ways to parse JSON in TypeScript. Let's play:
const jsonString = '{"course": "CSCI 0320", "instructor": "Tim Nelson"}';
// cs32 has inferred value *any*. TypeScript sees a string being parsed, it has no way to know the result type.
const cs32 = JSON.parse(jsonString);
This seems OK, right? But then:
// We can check whether a key exists on an object
if("course" in cs32) {
// Notice the mouseover type is still: any
console.log(cs32["course"])
// So I can do this with no problem:
console.log(cs32["DOESNT_EXIST"])
// The "if" statement is doing nothing! We get no protection!
}
TypeScript trusts the any
type. It always applies, and so narrowing doesn't matter. This is worse than it appears. What happens if we add a new field?
const cs32_withLocation = {...cs32, location: "B&H"}
// The inferred type is _still any_. ARGH!
We might try to protect ourselves
interface ClassWithLocation {
course: string,
instructor: string,
location: string
}
const cs32_withLocation_better: ClassWithLocation = {...cs32, location: "B&H"}
// Whew! Now we're safe, right? Well...
const jsonString2 = '{"course": 17, "teacher": "Tim Nelson"}';
const cs32_bad = JSON.parse(jsonString2)
const cs32_withLocation_bad_better: ClassWithLocation = {...cs32, location: "B&H"}
// Oh. Oh no. TypeScript _trusts the any type_ implicitly.
// So I can't get where the actual data is:
console.log("Prof. " + cs32_withLocation_bad_better.teacher)
// But I can reference a field that doesn't exist.
console.log("Prof. " + cs32_withLocation_bad_better.instructor)
How do we deal with any
? TypeScript has a feature called type predicates that can work. But those can be verbose. We'd like a simpler solution in this situation. Fortunately, Zod is great for exactly this.
const classRecordSchema = z.object({course: z.string(), instructor: z.string()})
// What do you think each of these will produce?
const result1 = classRecordSchema.safeParse(cs32_withLocation_better)
const result2 = classRecordSchema.safeParse(cs32)
const result3 = classRecordSchema.safeParse(cs32_withLocation_bad_better)
What do you think these produce? Try it.
Try it, then click!
result1
: a success result containing{"course":"CSCI 0320","instructor":"Tim Nelson"}
result2
: a success result containing{"course":"CSCI 0320","instructor":"Tim Nelson"}
result3
: an error result reporting 2invalid_type
errors: one forcourse
(number
) and one forinstructor
(undefined
).
Notice that both result1
and result2
contain the same values, even though one of them had a location
field originally. This is because Zod throws away fields it isn't told to keep, at least by default. If we want to avoid this, we use .passthrough()
:
const classRecordSchema = z.object({course: z.string(), instructor: z.string()}).loose()
Now the location
field is kept, if there is one there. (But, of course, now TypeScript will need some convincing before it lets you access that field.)
Refinements
We saw before that Zod can create schemas that are richer than what TypeScript types can represent.. TypeScript has no type for "email addresses", but Zod has a schema for them. But a type is just a set. So it's not that these richer schemas can't be thought of as types. Rather, it's that the type checker (which runs at "compile", or "static" time) isn't expressive enough to handle them.
Whenever we add a further restriction on a base type, we'll call it a refinement of that type. So, "it's a string, but in email-address format" would refine "it's a string". Zod has a lot of these, including a very expressive method: .refine()
, which takes a refinement function:
const evenNumberSchema = z.number().refine( num => num % 2 == 0)
const departments = ['CSCI', 'MATH', 'MCM'] // etc.
const refinedClassRecordSchema = z.object(
{course: z.string().refine( c => departments.includes(c.split(' ')[0])),
instructor: z.string()})
const course1 = refinedClassRecordSchema.safeParse(cs32)
console.log(course1.data)
By the way, watch out for "in". You might want it to work like it does in Python, but:
> 0 in [0, 1, 2]
true
> "a" in ["a","b","c"]
false
Use lst.includes()
instead.
Exercise: Building a suite for CSV
Your first sprint asks you to build tests that probe how the parser we gave you might not be handling CSVs very well. We want you to think carefully about what's missing, but it's also a useful place to take time in class and talk about using Copilot to synthesize tests.
I've used Copilot a good amount now, and sometimes it's great at building test suites, and other times not so much: I've often needed to prompt it to change something, or to realize there's an issue with a test it wrote. So let's get some practice critiquing. We'll do this over multiple classes.
How do you want to start?
Narrowing and Refinements
Logistics
Good job on sprint 1.
My "note of encouragement" post has only been read by 107 unique users. The week 2 summary has done better (128 unique users), but very few have responded with an emoji. (These counts also include any staff members who have clicked the threads.)
We will pause for a couple of minutes so everyone can read my post.
Now: everyone turn to your neighbor and show them something from your sprint 1 solution. Let's normalize collaboration...
Exercise: Disappointing Types
We'll start with today's in-class exercise. I'd like you all to look at this Java project in the livecode repository. There are some questions for you in this web form.
Now would be a great time to clone the repository. We won't be using Java much this semester, so we don't provide a formal setup process. I wanted to make this exercise more accessible, so used Java. When we do use Java, the projects use Maven for dependencies: open the pom.xml
file as a project in IntelliJ, not the folder. But you don't need to run the code (right now) to do the exercise.
What do you notice?
Types of Type System
Let's continue on from last time.
Type Assertions / Typecasting
In Java, you've likely seen typecasting, or "downcasting", that looks like this:
Object anObject = new ArrayList<Integer>();
// I can't refer to anObject.size() because the variable has type Object.
// So I can tell Java "I know better than you; it's an ArrayList". Something like this:
System.out.println(((ArrayList<Integer>)anObject).size());
This is called a typecast, or more precisely a downcast (because it asserts that an object has a more specific type—further down the type hierarchy—than what Java inferred). These are sometimes necessary, but in general they are dangerous: what happens if I'm wrong? What happens if that object is actually something different? I get an exception at runtime. Ouch!
TypeScript has a form of downcasting, too: the as
keyword. We can always disable TypeScript on anything by asserting it has the expected type (or, even more dangerously, any
). As a general rule, you won't want to use this operator. When in doubt, ask.
(Funnily enough, when I was experimenting with Copilot over the summer it kept trying to add (as any)
to a bunch of stuff, even when it wasn't necessary.)
Structural, not Nominal
TypeScript types corresponding to Zod schemas will only be as specific as TypeScript itself can support.
type ClassRecord = z.infer<typeof refinedClassRecordSchema>
If you mouse over this type, you'll see:
type RefinedClassRecord = {
course: string;
instructor: string;
}
That's missing our refinement, which makes sense because TypeScript's types can't express our schema refinement.
TypeScript is structurally typed, not nominally typed (meaning that it looks at the structure of objects, not the actual name of their types). This is in contrast to languages like Java, where it's the name that matters.
Because it only cares about structure, TypeScript won't stop me from claiming that an object with these fields (of the less specific types) is a ClassRecord
:
// No type error
const uhoh: ClassRecord = {course: "NOT A VALID COURSE ID", instructor: ""}
This is a threat to success: I might accidentally manufacture a value that, to TypeScript, looks like something Zod has validated.
Type Branding
Let's make it impossible to create a ClassRecord
without going through Zod first. We'll use something called "type branding": add a special flag (which won't exist at runtime) to the type that only Zod can add. At a basic level, this is easy: we'll just .brand()
the schema.
const brandedCRSchema = refinedClassRecordSchema.brand("ClassRecord")
type BrandedCR = z.infer<typeof brandedCRSchema>
const uhoh2: BrandedRCR = {course: "NOT A VALID COURSE ID", instructor: ""}
If you mouse over BrandedRCR
you'll see something strange:
type BrandedRCR = {
course: string;
instructor: string;
} & z.core.$brand<"RefinedClassRecord">
That &
means intersection (the same as we use |
to build unions). If a value doesn't carry that brand, it can't be used as an instance of this type. Hence, uhoh2
now gives an error. This is a longhanded way to say that the brand is missing:
Type '{ course: string; instructor: string; }' is not assignable to type '{ course: string; instructor: string; } & $brand<"RefinedClassRecord">'.
Property '[$brand]' is missing in type '{ course: string; instructor: string; }' but required in type '$brand<"RefinedClassRecord">
A Danger of Mutable State
Type branding isn't perfect. One major threat is that if these objects are mutable, then the guarantees might hold immediately but then stop holding (without TypeScript protection!) after some code changes the object.
const brandedCRSchema = refinedClassRecordSchema.brand("ClassRecord")
type BrandedCR = z.infer<typeof brandedCRSchema>
const uhoh2: BrandedCR = {course: "NOT A VALID COURSE ID", instructor: ""}
const cs32 = brandedCRSchema.safeParse({course: "CSCI 0320", instructor: "Tim Nelson"}).data
if(cs32 !== undefined)
cs32["course"] = "FAKE 1234"
TypeScript objects are mutable, similar to fields in Java objects. In Java, we can prevent a field from being modified with the final
keyword. In TypeScript, we use readonly
when declaring a field. Zod will even help us if we call the .readonly()
method on a schema:
/** Schema for valid class records. */
const refinedClassRecordSchema = z.object(
{course: z.string().refine( c => departments.includes(c.split(' ')[0])),
instructor: z.string()}).readonly()
Now, when we mouse over the inferred type:
type ClassRecord = {
readonly course: string;
readonly instructor: string;
}
Beware: again, just like with final
in Java, declaring a field immutable doesn't mean that an object stored in that field is itself immutable. If we were storing something more complicated than strings in those fields, we'd want to mark the inner schemas as .readonly()
as well.
Branding and mutability are sometimes subtle, especially when trying to create cyclic data. See the separate types document we wrote for more on this.
Reminder: Copilot chats do persist!
Copilot chats persist, but they are stored in a workspace-specific way. So if you're wondering how to recover a chat, just make sure you're in the same workspace as when you created it. To see the chats you've created in the current workspace, open the command palette (Cmd-Shift-P on Mac) and select "Chat: Show Chats".
In case you're interested: each workspace has a unique ID and a folder for all its storage. If you have Copilot installed, you can see where this is by opening the command palette (Cmd-Shift-P on Mac) and selecting "Developer: Open Chat Storage Folder". The chat logs are stored as JSON, and are rather verbose.
Threads and Promises
The material in these notes goes deeper than we can cover in class. If you're having issues with concurrency, parallelism, or asynchronous execution: read these notes! If you're having trouble with the difference between promises and threads, read these notes!
Motivating Concurrency
Let's go back to the hours queue dispatcher. You can find a more complete version of the example in the project linked at the beginning of these notes.
If we run the main
method in the Main
class of this new example, we'll get something like this (using TA names from a prior semester):
08:45:09:837 Dispatcher: Welcome to edu.brown.cs32.livecode.threads.TA hours!
08:45:09:839 Dispatcher: Hi Nim; you'll be seen by Erica
08:45:09:844 Erica says: Hello Nim!
Erica says: Goodbye Nim, I hope that helped!!
08:45:10:859 Dispatcher: Hi Alice; you'll be seen by Erica
08:45:10:859 Erica says: Hello Alice!
Erica says: Goodbye Alice, I hope that helped!!
08:45:11:865 Dispatcher: Hi Bob; you'll be seen by Erica
08:45:11:865 Erica says: Hello Bob!
Erica says: Goodbye Bob, I hope that helped!!
08:45:12:870 Dispatcher: Hi Charli; you'll be seen by Erica
08:45:12:871 Erica says: Hello Charli!
Erica says: Goodbye Charli, I hope that helped!!
08:45:13:876 Dispatcher: Hi Boatswain; you'll be seen by Erica
08:45:13:877 Erica says: Hello Boatswain!
Erica says: Goodbye Boatswain, I hope that helped!!
08:45:14:882 Dispatcher: Hi Bucky; you'll be seen by Erica
08:45:14:882 Erica says: Hello Bucky!
Erica says: Goodbye Bucky, I hope that helped!!
08:45:15:887 Dispatcher (6 helped so far): Nobody waiting in queue, will check again in three seconds.
Until we look at the timestamps, this seems fine.
This doesn't look so great. We have some problems to solve. If you look at the code, you might see other problems too. Here are three:
- Challenge 1: the dispatcher is waiting for an individual TA to finish helping a student before allocating the next TA. Lots of TAs will be idle, and students will wait a lot longer.
- Challenge 2: maybe we'd like to add TAs while the dispatcher is running.
- Challenge 3: how can new students join the queue?
All of these problems are related to today's topic: concurrency:
- We'd like TAs to be able to help students without holding up the dispatcher from allocating other students to other TAs. We need the dispatcher to run concurrently with the TAs.
- We need a way for the
Main
class to calldispatcher.addTA()
after the dispatcher starts running. But right now, there's no way for theMain
class to run independently of the dispatcher. We need the dispatcher to run concurrently with themain()
method. - We need a way for new students to be added to the queue. This is the same problem as above!
Concerns like these pop up all the time in engineering. (The problem isn't just that the way I've written the dispatcher library is not very realistic.)
Here's a picture demonstrating the current state of affairs. These are called sequence diagrams, and they are very common in networking and distributed systems. Each vertical line corresponds to an entity (in this case, 3 different classes). Horizontal lines are messages or method calls. We can see the control flow, in a single thread, passing between the classes. Because Erica
is always the first free TA in the list whenever the dispatcher gets to go, Erica
is the only TA who gets to see students.
How does concurrency help us?
A program is concurrent if it involves multiple simultaneous threads of execution. Note that this doesn't necessarily mean that these multiple threads really are running at the same time, just that they are "executing". We will make a distinction in this class between concurrency and parallelism, where threads really are executing at the same time, usually on multi-core hardware or in the cloud. A parallel program is always concurrent, but the threads of a concurrent program may or may not actually be run in parallel.
To illustrate this idea, consider what your computer is doing right now. You're probably running more programs than you can count, even before you think about what your operating system is doing. How many CPU cores do you have? Probably not more than a dozen (and likely fewer). So not all of the concurrency that's happening can be parallelized: you'd need hundreds of cores for that! Instead, your operating system runs a scheduler program which allocates slices of time to different threads of execution.
Concurrency is more common than you might imagine. Because we're working with Java, every program you write is concurrent, even a "Hello World!" program. Why?
Think, then click!
The garbage collector!
Using concurrency to get what we want
Imagine logically splitting our big program into separate, independent "threads" of execution: one that runs the dispatcher, another that runs when a TA helps a student, and so on. We just need to separate them from each other, and help them communicate.
So far we've only had one thread: the one that starts up in our main
method. How do we get another, and which should it correspond to?
Think, then click! (Image within...)
There are a few options. But let's start simply, and not try to solve all the challenges at once. We'll have every TA correspond to their own thread, and have those threads woken up by the dispatcher. it would look something like this:
The triangle is a drawing error I need to remove, but I ran out of time before class. :-)
Runnables and Threads
Java has an interface called Runnable
, which requires the implementation of a run()
method. It's also got a class called Thread
, which has a constructor that accepts a Runnable
and a method called start()
. So, as a first cut, let's make TA
implement Runnable
, and whenever we dispatch a student to that TA, we run the thread.
public class TA implements Runnable {
// ...
public void seeStudent(Student student) throws TABusyException {
// ...
new Thread(this).start(); // NOT the same as .run()
}
// ...
@Override
public void run() {
// When the above .start() method is called, the *NEW THREAD* will execute this method.
}
}
Now, when we run, we'll see:
09:24:18:642 Dispatcher: Hi Charli; you'll be seen by Erica
09:24:18:647 Dispatcher: Hi Boatswain; you'll be seen by Orion
09:24:18:647 Erica says: Hello Charli!
09:24:18:647 Orion says: Hello Boatswain!
Orion says: Goodbye Boatswain, I hope that helped!!
09:24:19:652 Dispatcher: Hi Bucky; you'll be seen by Orion
09:24:19:652 Orion says: Hello Bucky!
Erica says: Goodbye Charli, I hope that helped!!
09:24:19:653 Dispatcher: Hi Nim; you'll be seen by Erica
09:24:19:653 Erica says: Hello Nim!
Orion says: Goodbye Bucky, I hope that helped!!
09:24:20:658 Dispatcher: Hi Alice; you'll be seen by Orion
Erica says: Goodbye Nim, I hope that helped!!
09:24:20:658 Dispatcher: Hi Bob; you'll be seen by Erica
09:24:20:658 Orion says: Hello Alice!
09:24:20:658 Erica says: Hello Bob!
09:24:20:658 Dispatcher (4 helped so far): Nobody waiting in queue, will check again in three seconds.
Orion says: Goodbye Alice, I hope that helped!!
Erica says: Goodbye Bob, I hope that helped!!
09:24:23:662 Dispatcher (6 helped so far): Nobody waiting in queue, will check again in three seconds.
Much better!
Concurrency vs. Parallelism (Again)
The TA
thread and the main thread really are running separately. It's not clear whether they are truly running in parallel, though: the operating system and Java runtime decide that, in part based on how many cores the hardware has available.
What Could Go Wrong?
Concurrency seems really powerful. But are there any risks associated with it? Let's investigate.
Suppose we want to record how many students have been seen before the dispatcher terminates. One natural way to do this is by adding a static
counter to the dispatcher class:
static int studentsSeen = 0;
Now, every TA
can increment this counter in its run
method, when the student has been helped:
HoursDispatcher.studentsSeen += 1;
If we tell the dispatcher to print this counter out, we'll see output like this at the end of the queue:
11:56:10 Dispatcher: Nobody waiting in queue, will check again in three seconds. So far we helped 6 students.
And indeed that's what we see. But let's see how this works at scale. Instead of using these names, we'll create a hundred TAs, and a few thousand students who need to be helped!
So that we can simulate helping so many students without waiting, let's reduce the delay time to help a student: students will be helped instantaneously! We'll also remove the printing in the TA
class, since that slows things down.
We might see something like this:
12:10:52 Dispatcher: Nobody waiting in queue, will check again in three seconds. So far we helped 299993 students.
Uh oh.
What's going on? (By the way, this issue might be less likely to happen if we left the printing in.)
Think, then click!
I've made the classic thread safety mistake. Incrementing that counter isn't atomic: two threads might be trying to edit it at once. Suppose they both fetch the current value at once, add 1 to that value, and then write. If that sequence of operations happens, the counter will only be increased by one.
This sort of issue is pervasive in multi-threaded programming. Do the reading---you'll save yourselves a lot of pain.
How Can We Fix This?
The first approach is old-school synchronization:
synchronized (HoursDispatcher.class) {
HoursDispatcher.studentsSeen += 1;
}
This will tell Java that only one TA
can be running that increment operation at a time. (The argument to synchronized
helps disambiguate between multiple dimensions of synchronization we might have happening).
Another approach is to use thread safe objects from Java's standard library. In particular, I could have used an AtomicInteger
for the counter:
static AtomicInteger studentsSeen = new AtomicInteger(0);
// ...
HoursDispatcher.studentsSeen.incrementAndGet();
Both of these approaches fix the problem. The key takeaways are:
- Having multiple threads of execution lets a program better separate responsibilities into different execution paths that run at the "same time", logically speaking.
- These threads might actually be run at the same time, but, depending on the operating system, they might be run by time-slicing (i.e., taking turns on one core).
- Concurrency can cause unusual bugs that don't always happen, or that happen differently across executions. Situations where the ordering of operations results in inconsistent behavior are called race conditions.
Asynchronous Execution
Let's get back to TypeScript. TypeScript has really convenient support for concurrency. Try these in the browser console:
console.log('1')
setTimeout(() => console.log('2'), 5000)
console.log('3')
But just under that surface, complexity lurks:
console.log('1')
setTimeout(() => console.log('2'), 5000)
console.log('3')
while(true) {}
What's happening here? JavaScript—a language built for the web—is a language whose design is deeply and unavoidably tangled with concurrency.
And yet, JavaScript itself (barring some modern extensions, which are best used for expensive tasks that would block important things like UI interactivity) is only single-threaded. We don't create a new thread to wait for a web request to finish. Instead, we create a callback, like we would for a button being clicked.
In principle, callbacks are called as soon as possible. But the definition of "as soon as possible" is complicated. The browser is in charge (or Node is, if you're running a backend server).
The Callback Queue
Because TypeScript is single threaded, it can't actually invoke a callback while it's running some other piece of code. It has to wait until that code finishes, and then it looks at its callback queue to see if it has callbacks waiting to be processed.
Every time a callback is registered (in the setTimeout
example above, the 0-argument function that invokes console.log
is a callback) it is added to the queue. Crucially, these calls will only ever take place if they're popped off the queue. And they're only ever removed when the code currently being executed is finished.
This will become extremely important when you start sending web requests from your frontend to your API server. Callbacks are not threads. Asynchronous execution is very closely related to concurrency, however.
Callbacks are not threads. Neither are promises, async
functions, or anything else in the remainder of these notes.
Code Review Exercise
Let's look at some code and anticipate potential errors related to concurrency. (I've removed the types so that we can run this in the browser console.) What's the value that you expect to be printed by each block of code below, after its respective call is uncommented?
function example0() {
let toReturn = 0
setTimeout(() => {toReturn = 100}, 5000)
return toReturn
}
//console.log(example0())
function example1(){
let toReturn = 0
setTimeout(() => {toReturn = 500}, 0)
setTimeout(() => {toReturn = 100}, 5000)
return toReturn
}
// console.log(example1())
function example2(){
setTimeout(() => {console.log('A')}, 0)
setTimeout(() => {console.log('B')}, 5000)
console.log('C')
while(0 == 0) {}
console.log('D')
}
// example2()
Connecting to Sprint 2
Right now, you're working with file I/O. Because reading from or writing to a file can take time, file I/O is asynchronous in TypeScript. The async
and await
operators help us deal with asynchronous execution.
In class, we'll look at this informally in my own CSV parser code.
Next time, we'll introduce asynchronous execution in the context of making web requests.