Inevitably the best way to make design decisions is to practice implementation over and over again. Marianne experiments with writing different types of specs and considers various ways they could be executed under the hood. Midway through she realizes she doesn't actually understand how to think about errors and attempts to do more research while her cat tries to distract her.
Become a patron to support this project.
Open to helping Marianne out by giving up 30 minutes of your time to try to use her language? Book a time slot here
MB: In trying to figure out what my language should look like and how it should align itself philosophically, I spent a lot of time writing sample models in Go.
MB: ...This week my guest is (unfortunately) my cat, Smokey Jones.
MB: I'm actually surprised I haven't had a need to introduce you to him before this. Interrupting conference calls is sort of his thing. Knock it off!
MB: The type of modeling I like to do is called system dynamic modeling. If you've reading the book Thinking in Systems, you're probably already familiar with the terminology I'm about to explain. I prefer these models over more traditional specifications and verification approaches because I'm interest is in resilience. Perfect systems that never misbehavior do not exist. I'm much more interested in systems that can absorb a multitude of errors before losing their integrity.
MB: In system dynamic modeling you have stocks and flows. A stock is a resource of a certain amount. A flow is the rate of change of that resource. The traditional example is a bathtub full of water. The water is a stock. The rate at which it flows out when you unplugged the drain is a flow. The rate at which new water flows into the tub if you turn on the facet is also a flow.
MB: From these two building blocks, we can model almost any type of system. My equivalent of a "hello world" is modeling a load balancer. I have a stock representing the amount of memory for each VM and a flow representing traffic consuming that memory. The functions that govern's the flow's rate can product a round robin affect, or turn off and on depending on the value of each VM's stock.
MB: A model in this sense is a series of feedback loops. All you really need is stocks and flows, but for convenience we might include some additional elements: delays (which involve how quickly or how often the flow operates) and what I'm going to call factors which will operate a bit like constants. For example, the number of workers in a factory obviously has an effect on how quickly new product is produced. Whether that effect is positive—more workers mean more productivity— or negative—more workers mean slower production—depends on your philosophy about the issue.
MB: Up to this point I've spent a lot of time reading and picking the brains of other people much smarter than me. I can't go any further until I make a few design decisions about this language.
MB: I want to represent both stocks and flow as structs. Stocks will have a name property and a value property. Flows will have a delay property, a target property linking it to a stock, and a rate property.
MB: What data types should I allow? I want to keep things as simple as possible to start out. Strings are an obvious one. Integers another must have. I'd love to get away without floats, but that doesn't seem likely. At this point I'm not even sure I need an array. The language is going to use them under the hood of course, but allowing the user to create arrays seems like it might add unnecessary complexity.
MB: Oh well, early days still… can always change that later.
MB: Throughout this process I've been using what's come before as a reference. I spent a fair amount of time picking through the source code for Insightmaker (an online tool for system modeling... thank God for open source~~!) but my favorite resource has been my copy of Introduction to System Dynamics Modeling with DYNAMO. DYNAMO was a language written in the 1950s for simulations on mainframes. It's a product of its era, at first glance it's a jarring jumble of all caps commands and creative uses of columns. When I see the code for these models the part of my brain that knows a little COBOL kicks into gear. But because it was always proprietary it has since been lost to time, but its manual is full of all sorts of great example models. If my language can reproduce these, then I'm confident I'll have the right functionality to start.
MB: So… the big question is: where do we want to end up on the functional, procedural spectrum? Should data be immutable? I love the idea of immutable data but the whole point of the model is state change. On the other hand users are going to want to see a history of a stock values... I tried a couple different variations on this with my test models. I played around with linked lists before deciding that was probably more complexity than I needed (truthfully). Then I built a simple model where instead of changing the value, we make a copy of the struct, change that and append it to the end of a list. But I don't like that so much as certain parts of the struct (like the stock's name) stay the same for each copy. Seems wasteful
MB: My next attempt was to make the value of property a list of all previous states of that property. This works pretty well, but as I move from simple models to examples in my DYNAMO reference manual I realize that some models might need values other than the stock might change. For example, it's conceivable that we want some factor to increase or decrease the delay of the flow, right?
MB: I go through a couple of different ways to do that... none of which I like. I mean these are functions, right? We’re talking about something that influences that value of another property. That’s obviously a function. But…Where should those functions live? I like the idea of every propriety in stocks and flow actually being a lambda that either returns a static value if the user defines no function, or the output of said function. But when would that function be called exactly? If it's called every single time the propriety is accessed you could create infinite loops really easily by specifying that two proprieties are dependent on one another. Is that a problem for the compiler to identify and prevent or is that a design flaw? I mean certainly you could do that with most normal programming languages, right?
MB: What do you think Smokey?
MB: You know… Actually… this is a question I've been pondering for a while: when a given behavior is not allowed what part of language implementation should handle it? When should a problem be a parsing error or a run time problem? Or.... I don't know what ever comes between that.
MB: Let’s google ... "compiler errors -vs- runtime errors” and “language theory" --Oh!
MB: Here's an interesting thought: an exception is not a synonym for error. I never thought about that but I’m looking at a Stackoverflow post that calls them "flow control mechanisms" ... in other words they’re like guide rails meant to keep the program from moving into an undefined state with unpredictable consequences. Errors at runtime it seems are about issues you know are possible but that you can't see coming before the program executes (dividing by zero, for example). Compiler errors are the things you can see coming when parsing the program.
MB: Okay.... um.... well then… back to our initial question. Let’s try “infinite loop recursion compiler error” ....
MB: Huh… It seems that this is runtime error—I guess I should say exception now—specifically something like maximum recursion depth exceeded.
MB: From this point I went down a bit of a rabbit hole looking into how tracking recursion depth is implemented (spoiler alert you increment a counter, obviously) and a bunch of details that just aren't relevant at this stage ... but you know ... when you know close to nothing about a subject, getting off track is inevitable.
MB: Back to functional -vs- procedural... okay. So we’ve got functions. Are these functions pure functions? Absolutely, definitely. Because these are simulations there's no need to connect to a database or make a request to an API. Functions in our model should be side effect free. Particularly if they only exist to define the current state of a given property. This decision at least is easy.
MB: So… they’re pure function but are they first class? Can they be valid parameters? Can a function return a function? Uhhhhhh... I think probably not. Again let's keep things simple.
MB: Last one: strict typing... another strong yes there. If the stock value is an integer there should be nothing in the model that changes it to a string, or even a float. That seems super easy.
MB: I think to start out, if a flow has a precedent defined we will honor that, if not will execute the flows in the order they are declared in the spec. Concurrent flows would be super interesting. It is a real reason why systems fail in real life but for right now it seems like too much to take on. We’ll add that behavior later...
MB: Before wrapping up I'm going to go through a more complicated model from my DYNAMO manual and see if I can represent the same thing with the structures I've laid out here:
MB: Let's see.... Oh here we go. How about an epidemic model-- seems appropriate for our times. This model has three stocks: the susceptible population, the sick population, and the recovered population. On the flow side there is an “infection rate” that moves people from the susceptible population to the sick population and there's a "cure rate" that moves people from the sick population to the recovered populations. Those two rates are influenced by the following factors: the number of susceptible people contacted per infectious person per day influences the rate of infection (makes sense). Also influencing the rate of infection is the fraction of contacts who become sick, and the overall size of the sick population.
MB: The cure rate is influenced by the overall size of the sick population and the duration of the disease.
MB: Over time as I write more and more models I find myself trending towards simplicity. My original draft stocks stored their historical values in a specific "history" parameter before I decided that the value itself could store its own history. My draft flows used to have an increment value that specified how much the flow increased or decreased a given stock. But then I thought, that can be defined in the rate function, can't it? If the rate is constant you can just write a function that returns a constant. Now working on the draft version of this model I find myself staring hard at the target parameter.... Is it necessary to configure the flow to target a specific stock? Can't that also be defined in the rate function?
MB: Wait... no, no. I don't want to do that. There needs to be some way to tell the program where to store the value returned by that function. If we let that happen inside the function itself then we're opening the door to side effects, which we do not want. Okay so targets stay.
MB: Second problem: In this model each flow effects two stocks. The infection rate decreases the stock of susceptible people and increases the stock of sick people by the same amount. The cure rate decreases the stock of sick people and increases the stock of recovered people. How should this be represented?
MB: Perhaps the easiest solution is to create two flows to represent each side... but then how to keep their results in sync? Better to let the interpreter handle this and add a parameter called 'source' to the flow that allows us to specify a stock that should be increased or decreased an inversely proportional amount when the function runs.
MB: In order to represent the factors that influence each flow I have two options. Option 1: we can define global constants. Option 2: either stocks or flows can have additional metadata fields. That should allow for variable factors. In this model, all of the factors are static values, so I'll do them as constants.
MB: The first test is whether the parser can make sense of this model the way it's written-- Great. That worked and the tree looks good.
MB: The second test is whether I can write this model in Go in a way that approximates how the tree will be walked. This implementation will probably get much much simpler when I start writing a compiler, but for right now rewriting the model in a runnable format does help me spot edge cases the evaluator will need to be prepared for.
MB: Anyway... everything looks good and is running smoothly!
MB: Since I have a little extra time I want to go back to the issue of errors. When is something an error and what (if anything) should the language do about it?
MB: It probably will not surprise you to know that there are many different opinions about this. There is no consensus. No authoritative source on the best practices of program language design or postmortems on the mistakes that came before. There are, however, lots and lots of opinion pieces about the issue. You only need to Google "programming language design flaws" to see all the threads on Reddit, Quora and Stack Exchange.
MB: Last night I tried to write a spec in Japanese-- Well not entirely in Japanese, obviously keywords need to be in English, but yeah, could variable names be in another language in my language?
MB: The results were pretty interesting. The spec name was "unikudo" and since that's a foreign loan word in Japanese it's written in a specific script called katakana. But the variable names were the words for "people" "to study" and "knowledge base", are all in Kanji (a pictograph set of characters more borrowed from Chinese). The parser was fine with the katakana, but choked on the kanji. String literals and comments were a-ok though.
MB: So I only have partial unicode support for variable names.
MB: ....Is that a bug though?
MB: Some languages specify that only ASCII can be used for identifiers, some allow a limited set of unicode character. The argument for support tends to settle around the utility of using mathematical symbols as part of the identifier in order to leverage familiar conventions when programming actually mathematical formulas.
MB: The arguments against include things like look alike characters, control characters that reverse the direction, space modifying letters, combining marks ... Unicode characters aren't just a set of letters from foreign languages and some silly pictures. Some characters manipulate the other characters around them. That creates a lot of unpredictable behaviors potentially.
MB: The task of designing program languages seems to be full of "mistakes" that might not actually be mistakes depending on how you look at it. For example, the phrase "null reference" automatically invokes the response "the billion dollar mistake" from language designers. Null earned that reputation after its inventor gave a conference talk titled exactly that: "Null reference the billion dollar mistake." It was intended as a mea culpa. It's true that null pointer exceptions are the most common runtime errors in programming. It's also true that since they could theoretically appear anywhere in a program they are also very difficult to catch. I don't know that anyone has done a survey of this but they do seem to be a key contributing factor to system instability ... and perhaps they have cost various industries billions of dollars.
MB: But after the talk, one audience member pointed out that if you didn't have null references... you would still need to invent them:
Audience Member: The thing is, you have to handle with the actual case, either if you use a technical null pointer or not, you have to check later on. Is it a technical null pointer or is it a pointer to a null class type? Yes, you just delayed the check. And in terms of thinking to fail fast the null pointer allows you definitely to fail fast, which, of course, I mean, a null pointer is a bad thing to happen. But it will— it will happen either, if you have the null pointer or the pointer to the null type
Tony Hoare: I think that’s, that's a very good, very good point.
TH: And I think as a language designer, I would say that language design, the best thing we can really hope for is not to create any new problems. Problems of programming are difficult enough already for the very reason that you suggest there are just so many cases and each case can be tested here or there or elsewhere. And no matter what lovely notations or concepts or checks you put in your language, you can't get away from the fact that there are many more cases that that the program has to think of and deal with all those cases. It doesn't really make programming easy, it just removes one of the difficulties.
MB: And yes, if you watch the full video you'll see someone jump up to talk about Haskell right away. So don't worry Haskell fans. It was covered.
MB: Another famous "mistake" in program language design is the ambiguous nature of grammar in C++. It makes C++ almost impossibly complex to parse and yet C++ is still one of the most popular languages. A large part of that is probably because the users of C++ are not likely to ever need to write a parsers for their own programs, they just used the compiler that's already solved the issue.
MB: Still there are others that are open for debate. Is the plus sign handling both addition and concatenation a mistake? If you forget a break statement at the end of the case block of your switch statement in any C based language and the program will execute the case below. Is that a design flaw?
MB: While working on my language someone pointed out to me that none of the major programming languages were written by academics studying program language theory. Some language inventors worked in research labs and most have advanced degrees in computer science. But the languages that actually get traction tend to be invented by professionals who designed them based on their own pragmatic needs and purposes. So the question of when is a mistake a design error to be corrected or a user error to be documented but ultimately left alone is tied to utility. What value is lost by allowing a program to break? What does the user gain in being able to do unexpected things? SHOULD my parser recognize identifiers in unicode?
MB: Ultimately we can't do anything about that. We can't predict the future. We can only figure out the use cases for today. And the only way I know how to figure out user utility is by talking to some actual users...