HN user

gary_bernhardt

2,312 karma
Posts19
Comments112
View on HN
www.executeprogram.com 2y ago

Show HN: Beta test Execute Program's interactive "Python for Programmers" course

gary_bernhardt
99pts11
www.executeprogram.com 4y ago

The Code Is the To-Do List

gary_bernhardt
116pts79
www.executeprogram.com 4y ago

TypeScript Features to Avoid

gary_bernhardt
254pts204
adamwathan.me 5y ago

CSS Utility Classes and “Separation of Concerns” (2017)

gary_bernhardt
1pts1
www.youtube.com 5y ago

End-to-End TypeScript: Database, Back End, API, and Front End [video]

gary_bernhardt
2pts0
www.executeprogram.com 5y ago

Teaching the Unfortunate Parts

gary_bernhardt
126pts95
www.executeprogram.com 5y ago

Teaching the Unfortunate Parts

gary_bernhardt
1pts0
www.youtube.com 5y ago

End-to-End TypeScript: Database, Backend, API, and Frontend [video]

gary_bernhardt
6pts0
www.executeprogram.com 5y ago

Spaced Repetition (For Learning Programming and Other Skills)

gary_bernhardt
1pts0
www.fullstackradio.com 5y ago

Gary Bernhardt – TypeScript and Testing – Full Stack Radio

gary_bernhardt
2pts0
www.executeprogram.com 6y ago

TypeScript's excess properties can bite you

gary_bernhardt
2pts0
www.executeprogram.com 6y ago

Porting a React Front End to TypeScript

gary_bernhardt
72pts103
www.executeprogram.com 6y ago

Porting a React Front End to TypeScript

gary_bernhardt
9pts0
www.destroyallsoftware.com 8y ago

A Bourne-style shell built from scratch in 35 minutes [video]

gary_bernhardt
197pts64
www.destroyallsoftware.com 8y ago

Text Editor from Scratch [video]

gary_bernhardt
3pts0
www.destroyallsoftware.com 9y ago

The Biggest and Weirdest Commits in Linux Kernel Git History

gary_bernhardt
394pts51
www.destroyallsoftware.com 12y ago

Test Isolation Is About Avoiding Mocks

gary_bernhardt
78pts25
www.destroyallsoftware.com 12y ago

TDD, Straw Men, and Rhetoric

gary_bernhardt
177pts88
www.destroyallsoftware.com 12y ago

The Birth and Death of JavaScript [video]

gary_bernhardt
635pts227

Yes, currently there's no way to jump to the middle of a course. That lets us keep the lessons shorter, without needing to recap earlier topics. We're planning to add a way to skip, but that's not in yet unfortunately.

Edit: Just to be clear, all of the lessons shown on the course page are available, but you have to start at the beginning for now.

This is a great excuse to repeat one of my favorite quips, from the late Jim Weirich (from memory, but this is at least very close): "half-assed is OK as long as it's the right half of the ass."

That sentence's grammar is intentional. (I wrote the sentence.)

English is not a programming language. I'm sure that we can both name revered authors who have used far "worse" grammar consistently throughout their careers, whereas the word choice that has so ruffled you here involves a single word.

As to "flexing knowledge": I had some fun in a couple sentences in that article. I aim to be at least slightly more than a machine that extrudes gray paste of unit density. Fabricating an ulterior motivate and attributing it to me is offensive.

Almost none of the e2e tests for https://www.executeprogram.com meet this description. They're all tests like:

  - Visit a lesson page with a code example that returns a string.
  - Enter the correct answer to that code example, but without the quotes around it.
  - Assert that the text "Almost, but the answer is a string so it needs quotes." now exists on the page.
No implementation knowledge, nothing faked, real UI behavior tested. You can test most app behavior like this. E.g, we test our entire billing system like this, making these kinds of real assertions.

(To make sure that we know when actual rendered pixels change unexpectedly, we use Percy, which is a separate topic.)

I second this.

I switched to Dvorak on regular staggered keyboards and got to 80-90 WPM or so, but pretty much lost my ability to QWERTY.

Then I switched back to QWERTY full time for 5+ years.

Then I switched to Norman on an ErgoDox, getting to 80-90 WPM or so again. This time around, I could still QWERTY on a staggered keyboard just fine. Night and day vs. the first attempt.

(Then I switched back to QWERTY again. Now I can't Dvorak or Norman or ErgoDox!)

I'm the founder of Execute Program (https://www.executeprogram.com), where we've done a similar thing (knowledge graph + SRS) for programming languages/tools since 2019. Interesting to see that you have a graphviz render of a subgraph right on the landing page! We've toyed with the idea of exposing the graph visually, but haven't done it yet.

I second this advice, having done it. I flew one-way from Seattle to Dublin in 2010 with no plan beyond the first week. 25ish cities and 4.5 months later, I flew back. You'll figure it out as you go.

But, a warning from experience: when you fly one-way to another country where you'll go through immigration, always pre-book travel OUT of that country as well, and have printed (on paper!) proof to show border control. Fortunately this doesn't apply between most European countries due to the Shengen area. Immigration is not the part of the trip where you want to wing it. Always make sure you understand the visa and border crossing rules in advance.

I use `git clean` maybe once per year on average, so very low risk for me. And if that ever happens... that's why I have hourly backup snapshots! If I didn't trust my backups, I'd definitely address that before worrying about dotfile management. If you don't have a good safety net then you end up reinventing the net 100 times in other ways and everything gets more complicated.

I've had my dotfiles under version control for over a decade (hg for a couple years, then git). I've never used or felt the need for any special tools, and I don't use any extra symlinking.

If you do that stuff, great, but here's what I do if you don't want to use any specialized tools:

My home directory is just a git repo. There's a regular old ~/.git directory. All of the management I do is regular git stuff, like any other repo. Everything I don't want to commit is listed in .gitignore, like any repo.

The only wrinkle is setting up a new machine. You can't clone into an existing directory (AFAIK), and your home directory already exists, so you need a workaround. An easy solution: clone the remote repo to ~/dotfiles, then `mv ~/dotfiles/.git ~`, then `rm -r ~/dotfiles`. Now your home directory is a git repo where the working copy is exactly as the home directory was before, and HEAD points to your repo's default branch. It will be dirty because all of your config files are missing. Use regular `git status` to take a look. It's always looked fine to me, so I `git checkout .`, which effectively installs the dotfiles. Then I'm done until I need to do that copy trick again when I buy my next computer. In the meantime I just commit and push as normal, and occasionally add a new file to .gitignore.

Seconded; it's a fantastic tool and ecosystem. We use sql.js in production (https://www.executeprogram.com). The SQL course's live code examples all run directly in the browser via sql.js.

I initially had low expectations because it's such a weird use case, but it's been totally reliable. We did have to ignore a few types of errors from old browsers that don't support wasm properly, but we've never had a bug in current browsers caused by sql.js.

I appreciate it. We've struggled with communicating the non-binge aspect, and usually approach it from the other side: by talking about reviews, spaced repetition, remembering, etc. But I think you're right that approaching it from the "non-binge" side is a good idea. Coincidentally, we're about to do a major landing page revision, so the timing is right to give that a try!

As for apologizing for charging money... you're right there too. A lot of people will complain when someone dares to charge money for learning resources, even those that take multiple full-time staff to maintain. I think that's worn me down a fair bit!

I think your argument here could be made about pretty much any activity that humans do repeatedly. The critical difference is that both WaniKani and Execute Program quickly tell you "go home, you're done", so it's difficult to dump hours of low-quality time into them. Whereas advertisement-driven platforms are incentivized to retain your eyeballs for as long as possible, as you pointed out.

Here's an anecdote about what happens when you design a product in the opposite way.

I own https://www.executeprogram.com, which has interactive in-browser courses on various software development technologies. Currently they all cover languages, more or less: TypeScript, SQL, various JS topics, regexes. (Disclosure: it costs money after you finish your 16th lesson.)

Almost all (maybe literally all) of our competitors are amenable to binging. It's true of books, video learning platforms, and most/all other interactive learning platforms.

Execute Program is very intentionally non-bingeable. When you start a course, you get 5 lessons on the first day, then it stops you and tells you to come back tomorrow. On the next day, you get some brief reviews of yesterday's lessons, then a few new lessons, then it stops you again until the next day. That cadence repeats until you finish the course. You can't binge/cram even if you want to.

(A bit more technical detail: it's a spaced repetition system with exponential review intervals, similar to those used for language learning in e.g. WaniKani and Anki. But it also has a lot of fine-grained knowledge of its own course structure, so it can use reviews to intelligently unlock different lessons depending on how the user performed on their reviews.)

Occasionally, we get support email from new users who don't like this. They want to cram a whole course in a day. But cramming is a very time-inefficient way to learn, so this is self-defeating! Since launch, we've had good success adjusting the app's behavior and internal explanations to reduce these complaints.

However, we still get emails from long-term users who appreciate the time limitations. Generally these fall into two categories:

1. Users like that an enforced break before the reviews provides tangible evidence that "yes, I genuinely understood yesterday's lessons". If we allowed cramming, that reassurance wouldn't exist; it's too easy to succeed at a review when you just finished the lesson 30 seconds ago.

2. Users like that the usage limits remove a source of anxiety and worry. You do your reviews and lessons, you finish, and then you wait until tomorrow. There's no temptation to think "I really should've done 10 lessons today instead of 5; I'm so lazy".

It's still possible for a very dedicated user to do all of our courses in parallel within their first monthly billing cycle. (Median course start-to-finish time is 8-18 days depending on the course.) So this scheme doesn't make users pay us more than they would otherwise. And they're spending the same amount of wall-clock time that they'd spend if they crammed all of the lessons in one day. That makes it pure win: they memorize the topics more deeply, they worry less, and they get those benefits for no extra time expenditure. The only exception I can think of would be people who think "I must get exposure to all TypeScript syntax and semantics before tomorrow morning, even if that significantly reduces my ability to remember what I learned."

Obviously I'm very biased here, and the goals that we're optimizing for don't even exist in most other product spaces. But I thought it would be nice to have a counterexample to "engagement at all costs".

Only people using Safari < 13 will see that message. 13 is 1.5 years old, so yours must be older. We saw tons of wasm failures on Safari < 13, to the point that it was better to stop people before they got deep into something and hit a failure.

We run the entirety of SQLite, compiled to wasm, in the browser, among other things. It depends on the course, but our version checks are site-wide because it's all one app.

Ahh, if you're taking the microservice path then you're going to have to fight all of that complexity. So far we've stuck to the constraints that I set at the beginning of the project: vanilla Heroku web dynos; exactly one Heroku worker dyno (never 0 or 2); and Postgres is the only data store (no queues etc.)

(I wrote the article.)

We use a single git repo with no npm packages defined, other than the package.json in the root because we have to put dependencies etc. in there. The directory structure for our source code is dead simple:

  src/server
  src/client
  src/common
For example, the API endpoint definitions are common code, so you'll see stuff like this in client code that uses the API:
  import * as quizApi from "../../common/api/pages/quiz"
The ".."s are annoying, but working around them isn't worth the effort. Even when we heavily reorganize the file organization, it only ends up taking a few minutes to mechanically update these imports with a vim macro, or even with sed if it's a perfectly mechanical change.

We run two copies of tsc: one for the client and one for the server, building to build/server and build/client. That results in a weird build directory structure:

  src/server/db.js
  src/client/app.tsx
  src/common/endpoints.ts
  
  build/server/server/db.js
  build/server/common/endpoints.ts
  
  build/client/client/app.tsx
  build/client/common/endpoints.ts
If you need separate build settings for client and server, this weirdness is going to show up one way or another. However, we only introduced this build separation in the last month or so. For the first 1.5 years of the project, we got away with a single tsc process and tsconfig.json, with no build separation at all between client and server. If anyone who's newer to TS reads this, I'd encourage looking for simple solutions like that; you can get to the weirder stuff down the road if you need it (and you may never need it!)

(I wrote the article.)

We don't use ts-node for our dev processes because it adds some annoying startup delay. We keep the TS compiler running at all times on our dev machines, and we run our server/tests/etc. from the TS compiler's build output directory. This hasn't been a big pain point for us so I wouldn't worry about it much.

Related, just in case: I definitely wouldn't use ts-node in production. If your deployed code fails to typecheck for some reason, you don't want to learn that by seeing your backend server failing to boot in production. I think you should always compile your app during your deploy process, then have production boot the compiled JS as if TS weren't involved at all.

This is right. You'd have to read a ton of our source code to see this first hand, but the reducer state has nothing about React in it. It's a lot of stuff like "what answers did the user give?"; "was the answer correct or not?"; "what were the start and end timestamps for each attempt?"; etc. If we maintained multiple clients, we could reuse that structure because it's all about the application, not about React.

I wrote the post.

Every step has had a unique ID since day one. Database migrations would be wildly more complex than our actual solution and involve a huge increase in maintenance and (depending on what you're imagining) increase database server load by multiple orders of magnitude.

Today, we can edit lessons in completely arbitrary ways, including adding and removing steps, and the system adapts dynamically. With your proposed solution, lesson content changes that take seconds today would require writing and running an entire database migration.

Your scheme doesn't address metadata at all, but metadata is one of the biggest complications discussed in the post. If you're imagining a database schema that knows about the structure of the metadata, broken into separate tables, then your scheme would amplify a single write per step (as implemented today) into hundreds of separate writes per step (with your change). Those writes would happen for every step advancement in a lesson, so a user completing a single lesson would result in thousands or possibly tens of thousands of database writes instead of the roughly 25 writes that happen today.

But regardless of whether you're imagining the metadata in an opaque blob or separate tables, it doesn't actually solve the metadata migration problem! Imagine this situation: You have tens of thousands of half-finished lesson sessions sitting in the database. Now you add a new piece of lesson metadata to the system. What do you do with those old sessions when you run your database migration? What if the relevant metadata can't be reconstructed for those old sessions (which will be the normal case)? Do you make up a fake, incorrect value for the metadata? Do you make that metadata field nullable, forever, which over time will result in all metadata being nullable, defeating the type system guarantees? Our scheme solves all of this with a relatively simple code change, and it works automatically in all situations, and it has minimal runtime overhead or database load, and it requires no ongoing maintenance whatsoever.

The metadata is mostly used for analytics. I didn't even get into this in the post, but "throw the incomplete lessons away when the metadata structure changes" is equivalent to saying "don't fabricate analytics data that will generate incorrect analytics results, which will cause us to optimize the business incorrectly."

I rename the "email" key in our register endpoint to "emailAddress" and all code that touches that key turns red less than 1s later, whether it's in the client or server.

Edit: All of these edits to your posts after I've already replied are very confusing.