teaching
https://matklad.github.io/2022/10/19/why-linux-troubleshooting-advice-sucks.html
programming language theory
https://craftinginterpreters.com/
A great book on writing interpreters.
style
Parse, don't validate
https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
A static type system can be used to prove if a given function is possible to implement. For example, a function which takes a list of items and returns the first item from the list:
head :: [a] -> a
This is actually problematic because we cannot (with this function signature) handle the case where the list is empty.
There are two different approaches to handling this situation. The most
immediately obvious is to change the return type to some sort of Maybe type:
head :: [a] -> Maybe a
But this solution is not ideal. Often, in the code calling your function you will have already checked that the list is empty, but now you will need to check it twice. This introduces more room for errors and means a small, but non-zero performance toll.
When possible it is better to strengthen the argument type rather than weakening the return type:
head :: NonEmpty [a] -> a
In haskell and other strongly typed languages we can use a trait which proves that a list is non-empty. The code now has no redundant checks. We've parsed and then passed along our proof rather than validating at every usage.
Making illegal states unrepresentable
https://corrode.dev/blog/illegal-state/
Related to the "Parse, don't validate" advice is the concept of using your type system to make illegal states impossible to represent. One of the most common examples of this is having a bucket full of booleans:
let is_admin: bool = true;
let is_guest: bool = true;
Assuming your system isn't actually meant to have users who are both guests and admins you would likely be better served with a enum:
enum UserRole {
User,
Admin,
Guest,
}
Composibility
Lets compare a few implementations of an INI parser. The best one has two
functions; parse(string) INI and INI.stringify() string to convert ini data
to and from a structured format. This is quite an elegant system which can be
reused in many different instances.
Another way of creating an INI parser would be to have a parse(filename) INI
function and a INI.write(filename) to save it back to a file. There's a few
issues with this. First of all it makes the library useless for reading INI
files which come from other sources, such as over a network, or even using a
file reader that buffers the input for example. Second, this system often mixes
filesystem reading error and parsing errors... if it even handles errors at all.
Ultimately, it's adding complexity to something that would better be addressed
by composing the library with some file reading function.
The third way would be where you must create an INI object, then load the file/string into your object, and finally use specialized methods to get at the results. This type of thing is common in the object-oriented tradition, and it's terrible. Instead of making a single function call and moving on, you now have to perform the ritual of moving your object through various states. And because data is now wrapped in a specialized object type, all code that interacts with it has to know it's type, creating unnecessary interdependencies. When a standard data type suffices prefer that over creating a custom data structure.
project documentation
Readmes, Changelogs, etc