Is your programming language unreasonable? (2015)
fsharpforfunandprofit.com
fsharpforfunandprofit.com
1. Variables should not be allowed to change their type.
2. Objects containing the same values should be equal by default.
3. Comparing objects of different types is a compile-time error.
4. Objects must always be initialized to a valid state. Not doing so is a compile-time error.
5. Once created, objects and collections must be immutable.
6. No nulls allowed.
7. Missing data or errors must be made explicit in the function signature.
The idea being that each feature or constraint _enables_ you to reason and predict more about a program than you could otherwise.
I encourage anyone interested in these ideas to play around with F# or a similar language and get a feeling for how they influence your code. If you've mastered one paradigm such as OO one of the best ways to find holes in your mental models is to try and find another point of view to look at the same problems. Even if you keep writing most of your code like you do today, in the language you do today, it can still be beneficial.
The borrow checker is about validating that references don't outlive their target, and R^W.
So this language would not be general purpose, as it would not be suitable for high-performance computing.
Large scale simulations almost always involve arrays that are modified in place. Being able to somehow declare a collection to be immutable would be highly useful, but not having the option of mutable collections limits the kinds of problems that can be approached with the language.
Another example is completely pure array programming such as in Accelerate[0] or Futhark[1].
So there are "effects" (non-functional state changes) that are encapsulated ("benign").
I learned this term in reference to Standard ML at CMU.
This is different from what you're asking because it isn't a compiler optimization and it isn't actually checked by the language at all, but it works pretty well in practice.
It's like unsafe in Rust: you write most of your code assuming a useful property that you then break in the small percentage of code that needs to break it.
The caveat is that, in my experience, it's a fair bit harder to reason about performance, as the execution model is even more abstracted away from the hardware than even something like the C model is (which is no longer a good fit either, in this era of speculative execution and multi-level caches.)
One solution is to have a tool developed and distributed along with the compiler (so it can never fall out of sync with the compiler, that's why) annotate the code with notes about performance.
For example, a tail-recursive function needs to have it’s type as tail-recursive.
Tail recursion is not necessary to annotate imo, but I guess the compiler/linter could maybe complain if it finds recursion it can't do a tail call optimisation for. These kinds of warnings are similar to mutable languages warning about things that are probably bad but sometimes necessary.
The reason it must be made explicit is because when somebody else comes later on to change that function they may miss the fact that it doesn’t explode only because it’s tail-recursive.
You could of course document the requirement - but why document if you can make it a compiler option? “I don’t want this to compile unless I get the behaviour I expect from it”.
Also as far as I am aware C-style functions can not be tail-recursive because they can not clean up the stack after themselves, thus you can’t support tail-recursion across FFI.
This somewhat resembles immer.js (uses a proxy around an immutable structure which records updates). Contrast this approach to Clojure transients (whose children don't magically become transient), and whatever Haskell does (https://news.ycombinator.com/item?id=24740384).
[1]: https://docs.rs/im/
This sounds nice, but is there a way to accomplish it without losing some expressibility or concision? Rather than looking at JS, consider low-level operations on a small chunk of memory as a niche example. Interpreting the same region as a buffer of 64-bit ints vs 16-bit uints gives entirely different behavior to the standard operators like addition, multiplication, and shifts, and there are plenty of cases where it makes sense to mix and match those operators. It's possible to construct a single type that encompasses all that behavior, but the price for doing so is a formidable wall of similar-looking method names rather than just being able to use a plus sign or other easier-to-understand constructs.
The variable doesn't change its type though, instead you change your interpretation of it. That's a very different and explicit operation & manipulation.
Although example 1 doesn't strictly have anything to do with types, you could get the same behaviour with `x = 0` in any language with closures (like javascript), or `foo = 0` in a language which passes parameters by (mutable) references as long as that information is not visible in the caller.
I think this is the best summary of the article. In short, please protect me from my own stupidity and/or laziness.
It obviously has some merit, but I'd rather have a language be an enabler rather than a disabler. And again, not having to worry about messing up is or at least can be a kind of enabler, but I prefer a more positive approach, with good defaults encouraging me to do the good and simple, but the language getting out of my way for things it might not know about.
IMO, the current zeitgeist seems to be a knee-jerk reaction to a prior idea: "everyone can program (e.g. previous zeitgeist)...but, we need guard rails in tools! Put as many guard rails as we can into tools!" Thus, you have languages prescribing architecture (Elm), web frameworks imposing naming conventions (Rails), etc. At least Rust's limitations help with reasoning, vs seeming capricious.
I'm preaching to the choir here (parent), but Obj-C remains the most interesting language I've seen in a long while: dynamic runtime with a static type-checking for a great majority of code, along with very good performance.
Back when I first wanted to learn FP principles, I tried with Scala. It went nowhere. I had a problem, and I didn't know how address it in an FP manner. The language didn't help me by getting in my way when I went down a wrong path, so I never learned the 'right' one for FP.
As a trivial example - I want to iterate, I can use a loop; it doesn't prevent me from using a loop and instead directing me to recursion. In fact, with recursion I get all sorts of terrifying comments about making sure it can be tail call optimized, to use @tailrec to make sure of that, or else I might explode the stack, oh-God-what-is-this-why-would-I-ever-do-this-etc. Meanwhile there is that good ol' friend 'for' waiting for me to help me get things done...
Having used other languages, though, recursion holds no fears. I still am not a fan of Scala, but I'd be okay with being given all the options. Though not for any sort of team development unless everyone else had also been exposed separately to the paradigm we were standardizing on.
If I have 2 InputStream objects, nevermind equality. It just doesn't apply here. I don't even want `a.Equals(b)` to be considered valid code in the first place. Having __any__ default behaviour here is a lot like the permissiveness that is bashed in example 1 (a trifecta of unexpected variable scoping rules, the ability to re-type variables merely by assigning an object of a different type to them, and the idea that a boolean will silently convert itself to a numeric value): There are a billion situations where you'd want to e.g. treat a boolean as a number, and in the vast majority of them, '0' is a fine value, but nevertheless, it leads to unpredictable code so let's not default. Let's require an explicit interaction from the code author to indicate they really want to do that, and, hopefully, we can make that 'explicit interaction' simple.
Apply the same logic to example 2 and the author draws the wrong conclusion.
There is no reason to assume equality is a sensible concept, so objects should not even be equatable by default, and you could have an attribute which gives them structural equality (with a third option of a full-blown custom equality if necessary) as e.g. Haskell or Rust handle it.
So when he writes:
>> Why not make the objects equal by default, and make reference equality testing the special case?
this refers in particular to these value objects.
I completely agree with you that InputStream should not have equals defined. A sane language would not conflate values with classes, and InputStream is not a value.
So perhaps it could be better-written as:
If defined, equality should test by value, rather than by reference.
> "Records with the same internal data ARE equal by default"
Floats: floating point equality tests between two floats of the same value or testing equality with itself is generally "true" with the exception of NaN (i.e. "not a number") values. Testing if NaN == Nan is "false" according to the IEEE [1]. (Probably not all languages follow IEEE's recommendation.)
Functions: some functions are trivially the same, and some functions are trivially different, but deciding whether arbitrary functions (for instance, f(x) { x + x } and f(x) { 2*x } return the same values and have the same side-effects for all possible inputs is generally not something a compiler should be expected to do. So any record with a function as a field can't really be tested for equality.
[1] https://stackoverflow.com/questions/38798791/nan-comparison-...
For example:
fact: a == b
fact: f(x) is p deterministic pure function.
So I figure f(a) == f(b)
Well, maybe... Javascript (obviously) and Python are amongst the programming languages where the above reasoning is not true. (there are exceptions).There are lots of exceptions. For starters (2) seems like an assumption rather than a fact. An other assumption which is just that is that equality is transitive through f. This is not a generalised fact in any language which allows overriding equality.
Simple example: hashsets equality & conversion to sequences.
Hashset equality, to the extent that it's present, is always based on the presence of items in the set.
But the iteration order of two hashsets with the same contents can depend on the way they were initialised (as that influences whether collisions happen) and the order in which items were added.
So given a and b be hashsets and f be toArray (or whatever), it is very common to have a == b but f(a) != f(b).
var repo = new CustomerRepository();
var customer = repo.GetById(42);
Console.WriteLine(customer.Id);
with the issue being customer can be null, which is not being accounted for. The reasonable approach he says is to use a sum type: var repo = new CustomerRepository();
var customerOrError = repo.GetById(42);
if (customerOrError.IsCustomer)
Console.WriteLine(customerOrError.Customer.Id);
if (customerOrError.IsError)
Console.WriteLine(customerOrError.ErrorMessage);
Do most (or any) languages in which people take this approach actually enforce handling of all cases? Or could a programmer write that this way: var repo = new CustomerRepository();
var customerOrError = repo.GetById(42);
Console.WriteLine(customerOrError.Customer.Id);
and still have the same problem as the original?It seems to me that the "reasonable" version is getting its reasonableness from naming the variable that gets the GetById return "customerOrError" which reminds the reader that there is an error case, not from the language having sum types. That's just a convention. Nothing stops you from naming it "customer" just like in the "unreasonable" language.
(I'd actually expect that from a lot of people, because you are probably going to have a lot more code in the case that you have gotten the Customer variant than in the Error variant case, and you probably don't want to be calling it customerOrError in the 99% of the code where you know that it is a Customer).
In this way you end up encoding more intent within your types and behaviours rather than implicitly letting the consumer figure it out themselves.
/u/anw's example shows that, in fact, the benefits of the type system are greater than that. Not only can the type system prevent you from accessing data before confirming that the data is present (which is, at the end of the day, not much better than an Optional type, already available in Java and others), but it can also prompt you to "handle" all possible cases.
I'm not familiar with most languages, but I can state that Rust requires matching sum types to cover all possible variants. For example:
enum Animal {
Dog,
Cat,
Pigeon
}
let pet = Animal::Dog;
let sound = match pet {
Animal::Dog => String::from("ouaf"),
Animal::Cat => String::from("miaou")
};
This would not compile, because you've forgotten to cover the pigeon. You would need to either add one of the two lines below: Animal::Pigeon => String::from("rou rou")
or _ => String::from("eeeeeeeeeeeei")
The last line is a catch all, which would cover anything not checked for explicitly. const c = getCustomer(); // type is Customer | null
const z : Customer = c; // ERROR; incompatible types
if (x === null) {
// handle null
} else {
const y : Customer = c; // OK; knows c isn't null here
}A simpler example:
declare type Foo = {name:String};
declare function getFoo():Foo|null;
const f = getFoo();
if(f!==null) // mandatory or yields an error
console.log(f.name);
But afaik it doesn't guard against null by default.In "most functional languages", your result would be a sum type which requires dispatching explicitly between one case or the other, aka you wouldn't have a pair of conditionals instead you'd have something along the lines
switch (customerOrError) {
case IsCustomer(customer):
Console.WriteLine(customer.ID);
case IsError(errorMessage):
Console.WriteLine(errorMessage);
}
so from `customerOrError` you wouldn't have any sort of direct access to the customer or error, you must go through a dispatching construct.In OO languages lacking sum types this sort of constructs would generally be implemented through lambdas or some sort of visitor e.g.
customerOrError.then(
customer => { Console.WriteLine(customer.Id); },
onError: errorMessage => { Console.WriteLine(errorMessage) }
);Any language that provides this sort of union type will error in some way if you try to access something that isn’t guaranteed to be on every type in the union. Some languages which support null checking will infer Customer to be nullable.
This is false, I knew this was false even without reading what he meant by "reasonability" and, upon reading what he meant, I confirmed that it is, indeed, false.
Something that I too found odd is the separation between "functional" and... "Mainstream". That got me confused, LISP is over sixty years old and anybody with more than a passing education in computer science knows about it; there are few languages more mainstream than LISP.
I just don't understand this flamewar; terrible code in Clojure is no easier to read than terrible code in Python. Conversely, good code in either language is no easier to reason about than good code in the other.
So this essentially means there can never be a NULL object assigned to anything... how is this possible? is it avoided by throwing exceptions instead of returning NULL?
For example, a division function could return (1) 'None' for 'div(20, 0)' because division-by-zero is undefined, and return (2) 'Some 10' for 'div(20, 2)' because 20/2 is defined and is equal to '10'.
In practice, it's useful to be able to answer "I'm sorry, Dave, I can't do that". Some languages do that by silently and implicitly allowing this special value "null" in a return type. Some languages allow you to throw an exception (again without ever having declared that this is something you might do). But some languages take one of those approaches and make it explicit: in the two cases, you have the "option"/"maybe" type, or checked exceptions. Either of these explicit methods let a function declare that it may fail, and moreover the language forces you to handle the possibility of failure in any function which may fail.
The "reasonability" comes from the fact that the language forces you to be explicit about the possibility of failure, rather than implicitly allowing all functions to fail.
On the other hand I unironicly write goto:s and use globals as much as a please (think GNU Bison/Flex) so who I am to judge.
I believe Crystal doesn't let anything be null either. It's quite interesting.
Just t | None
You can have your null case, and avoid the null-pointer exception, default keyword, chained ? null-check hellscape in which we currently reside.
new Customer()
should return something valid? I think the FP way would be to disallow constructor overloads with no arguments, as really, there's no reasonable thing to be done here. If you don't have all the necessary information to initialize a customer yet, you don't get to initialize one, and you pass around a partially applied constructor function instead.ie, The argument is that (in pseudocode):
Customer(name = "Jimmy", address = null)
is not really a customer if address is a necessary piece of information, so we should distinguish at the type level by passing around a function that takes the address and returns the correctly initialized Customer like so \addr -> Customer(name = "Jimmy", address = addr)
Admittedly, this prevents you from accessing name until you provide an address, but I don't think there are many valid use cases where you want to access data before the Customer value is corrrectly initialized. I'd consider this a code smell to be refactored.If you really needed to do that you could just define a partial customer with no address field and pass are that around.
The difference is, because you’re now receiving a different type, you are forced to deal with the case where there’s no value.
Rather than your function returning a Customer that could sometimes be null—and if it’s only null infrequently, you’re much more likely to forget to handle it—it instead returns Optional<Customer>. To operate on that Customer object, you are forced to unwrap the Optional by checking whether the underlying value is there.
How well you handle the None case is up to you, but the compiler won’t let you forget (generally anyway; for example Swift allows you to force-unwrap optionals with a crash on the None case, but eh).
No, it means that you can't leave bits off yet claim to have a valid object, either the object is fully initialised or it's illegal. This is mostly a concern in older languages like C where you can reserve space for a struct then not fill in anything.
In some more recent languages (Java, Go, C# historically though that's changing) everything is zeroed by default so you avoid the "random garbage" situation, but it means you need to assume anything can be zero even if that's not intentional, and depending on the specifics you might "double write" each location even when that's not actually necessary.
In the more functional slate of languages, you simply can't let any members off. There might be a concept of "default value" but that tends to be opt-in, the baseline is that if you define a type you must fill in every member, explicitly, before the compiler will accept it.
Initialising items to null is orthogonal.
Now since the rest is predicated on a misunderstanding I've a version of each for "incomplete initialisation" and a version for "null".
> how is [forcing complete initialisation] possible?
The compiler just requires that every member of the structure be specified.
> how is [things never being NULL] possible?
Don't make NULL an implicit member of every type.
> is [incomplete initialisation] avoided by throwing exceptions instead of returning NULL?
it's avoided by the compiler refusing to compile your code if you leave off members.
> is [things never being null] avoided by throwing exceptions instead of returning NULL?
It's avoided by NULL being a member of a completely different type than anything else, whether that type is a special case or not e.g. in Rust "null" is one member of the Option "wrapper type" so to indicate that, say, a user is optional you write `Option<User>`, then that can either be `None` (no user, ~ null) or `Some(user)`.
If you just type something as `User` then it's necessarily a valid user and nothing else, because there is no null value you could put into it, you'd get a type error.
* Rust playground which fails to compile because one of the struct members was left off: https://play.rust-lang.org/?version=stable&mode=debug&editio...
* Rust playground which fails to compile because trying to put a null (None) in a non-nullable field: https://play.rust-lang.org/?version=stable&mode=debug&editio...
* Version of the previous with an optional (nullable) member: https://play.rust-lang.org/?version=stable&mode=debug&editio...
* Version with opt-in default (through Rust's Default trait, the default derivation of the Default trait will recursively default each field, which usually means some sort of zeroing): https://play.rust-lang.org/?version=stable&mode=debug&editio...
I strongly disagree with this. I’d take it one step further and say that there should be no equality operator or default equals function for non-primitive data types. Some specific type of records/tuples could use a default equality, but it should be opt-in or follow from using metadata like an attribute (e.g Rust derive) or only for those specific types (e.g C# ValueTuple) . Not even strings should have a default equality (ie it shouldn’t be possible to create a map/dictionary with string keys without explicitly also saying how keys are compared. That’s right: It shouldn’t even be an optional argument so the default string equality/hash is used unless specified. It should be explicitly passed. This isn’t “boilerplate” it’s avoiding subtle bugs.
As for the rest of the arguments, some are downright crazy. You cannot program without mutation in the general case. You simply don’t always have the performance/power budget for it. Obviously mutation should be avoided, but dogmatically using all-immutable data is just not possible.
No automatic casts would be high on my list too. I don’t ever want to accidentally treat things like “falsy” or convert between types based on usage. I’d rather litter my programs with type names and explicit conversions.
So by 'general case' you mean 'every case'? Because to my mind, those instances you don't have the performance/power budget for 'it' are very much special cases, not general ones. It also seems a curious distinction; I mean, Python and Ruby both allow for mutable data, but are quite slow and memory intensive. Haskell doesn't allow mutability, but is quite fast, and also surprisingly memory efficient when written well. What languages do you consider to be able to address 'the general case'?
I think e.g C# and F# handle mutation well, but I think F# does the right thing by not encouraging it like C#.
Being able to control the in-memory representation of data and do things like sort arrays in-place etc to some people seems to be rare and to others happens on day 1 even at toy scale. Depends on what problems you are solving I suppose.
I think it's a bit of a red herring to ask "can you solve them quickly enough" - after all, if pure speed across all use cases is your concern, why would you advocate a garbage collected language?
Obviously all languages are a tradeoff between ergonomics and performance, but it should be easy and idiomatic to use the back doors. Using mutation in F# or stack allocation in C# doesn’t feel like swimming upstream. If the language offers a comfortable experience for the 80-90% case and a not-terrible experience for the rest, that’s very good. If the “back door” is e.g. JNI or being forced to use SoA when you wanted AoS in Java that’s not very impressive.
I grabbed the 10-20% number of “programs that need mutation” out of my behind obviously, but it is highly subjective.
On that same note, coming from a country that uses decimal comma, I very much would prefer it if code always had to specify a target locale when printing a decimal number.
Then came the part where I needed to work with list of records and realized that something as basic as that which I take for granted in other languages, is a monstrously complicated concept in f#. I got introduced to the shady world of Lenses, tried my hand for a bit and decided to get out. Lenses are not for me.
I can't believe that a language makes updating records so difficult. F# is my first functional language and while I expected some friction in doing such stuff in a functional language, I never expected so much work around something so trivial. But is does helps me understand why people don't use F# for things like gamedev.
Anyway TLDR; love the language. Wish that it will make concepts like Lens much easier to use in the language
Here I ended up reading bid docs on something called Lens and was never able to apply it correctly. here is the repo i was working on https://github.com/rishavs/coral/blob/master/Engine.fs
I must be missing something, but this sounds like a pretty straightforward List.map
There are also some libraries out there that make this easier.
I'd say it's an annoyance and should be improved, but it's nowhere near a blocker for us.
https://github.com/rescript-lang/rescript-compiler/wiki/Why-...
https://www.javierchavarri.com/performance-of-records-in-buc...
Its Standard library Belt is optimized to compile to efficient JS too.
(edit: added line about Belt)
No. This breaks down as soon as you have references to other objects in your objects. If you compare by reference, you probably don't get what you want. If you compare by value, you need to go arbitrarily deep, which is not a sane default, because of possible reference cycles. You need a distinction between objects and value types. C# has that (record types).
"Once created, objects and collections must be immutable."
If this is how you want to program, fine. Don't tell me this is "the right way" to program. Immutable data structures can have a lot of overhead, which disqualifies them from many domains.
The real world is all about mutable state. At some point, you need to take the training wheels off.
Your comment is just making the author's point. References, cycles, mutable state these just make it harder to reason about your code.
And a nice petty little zing at the end for good measure. Good job.
If you want to define a graph data structure, cycles are inherent, so there must be some way to deal with them - they are not something that can simply be ignored. This is a completely practical problem, and identity instead of value equality is a valid answer. Of course, this doesn't contradict the original article: reference equality can still exist in the language, it just helps if it's not the default.
For example, in Common Lisp you have both `eq` (reference equality) and `equal` (typed value equality) [and even the somewhat stranger `equalp` (structural equality, e.g. (equalp '(1 2) '(1 2)) -> true)].
Funny enough, I think the opposite is true. For example, if you have a moving point in the real world, there is never a point where only the x coordinate is mutated, yet you have that in your mutating code.
Yet, this is how immutable datastructures often work in practice. That may or may not be a worthy tradeoff, but please do not act like it is the right way to do things by default.
Mutating variables in-place is the training wheels.
Got a bank account? Want me to transfer money by increasing this account and decreasing that account?
But perhaps that's too small an example. What about a large, distributed system?
Both Paxos and Raft are recipes for clusters of machines to agree on immutable sequences of values.
It's about more than just history. Logging would (maybe) suffice for that. It's about getting cause and effect right when there's more than one computer separated by a distance.
> I don't want to keep a history of all the positions of a character in my game.
Sure - not in a single-player game.
But can you take the same approach in a multiplayer game? Grab some players, give them different clocks and separate them 20-150ms apart from another, and have each of them tell the server what happened at the time it happened? Things will diverge pretty quickly.
You end up reinventing the same old append-only log on the server. That way, when client messages arrive at the server out-of-order, you still have the info necessary to rollback state and issue corrections to other clients. When that's all set up, you can then choose some horizon (200ms?) and garbage collect all events before that. Or you can keep the events around and show replays and/or calculate stats for achievements at the end of a match.
For one, the client must not be able to tell the server "what happened" (e.g. "I shot player B"), because that would make cheating trivial without significant extra development effort.
Rather, the client simply transmits only its own input for the server to process in its simulation. The server then gives the authoritative answer as to "what happened" to all clients. The client may need to patch up any visual discrepancy that may arise from differences in the client-local simulation.
What you propose of course isn't impossible and it's exactly what I'd expect people with a DLT background to attempt, it just doesn't really have much of an upside for what it requires. It's the wrong trade-off for the domain.
The distinction I'm making is not between "I'm now moving forward" and "I'm at x position", it's between "I'm now moving forward" (mutation) and "I started moving forward at time t" (what happened).
> Rather, the client simply transmits only its own input for the server to process in its simulation. The server then gives the authoritative answer as to "what happened" to all clients.
This glosses over too much and doesn't pick a side - direct mutation or event log? How does the server simulate the game state in the face of out-of-order or dropped events?
> I don't think anybody has ever shipped anything like what you propose to simulate a real-time game, but if they did, it would be more of a curiosity.
https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
The lag compensation system keeps a history of all recent player positions for one second. If a user command is executed, the server estimates at what time the command was created as follows:
Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation
Then the server moves all other players - only players - back to where they were at the command execution time. The user command is executed and the hit is detected correctly. After the user command has been processed, the players revert to their original positions.
https://www.jfedor.org/quake3/
Everything in Quake 3, both client and server, happens in response to events. Player input like mouse movement and keypresses, and also packets received from the network, all go through a unified event system. Even the passing of time is communicated to the engine using a separate type of event. In addition to decoupling the engine from operating system specific code, this opens up an interesting possibility. During normal gameplay it’s possible to record all the events going through the queue in a journal file.I left shortly after being taught this business practice but sure makes a great anecdote.
So the C#, VB, and F# compilers (all huge, "immutable data representations" codebases serving millions of developers worldwide) are training wheels?
Of course immutable data structures, like training wheels, make some things easier. You don't have to worry about certain classes of errors, because they just can't happen. It's the same with garbage collection, you can stop worrying about managing memory, for the most part.
However, if you can't do your job without these helpers at all, that's going to be a problem. At some point, there will be something that your garbage collector can not clean up for you. There will be state that is going to be mutated, whether you like it or not. Your brain should be prepared for that.
I work with the C#/VB codebase for my job (at Microsoft on compilers and language tooling) and with the compiler and tooling engineers who also use this codebase. It uses nearly everything under the sun, but is most definitely based on immutable data representations at many layers of its "stack". The very notion of a compilation and a syntax tree are immutable.
> Of course immutable data structures, like training wheels, make some things easier. You don't have to worry about certain classes of errors, because they just can't happen. It's the same with garbage collection, you can stop worrying about managing memory, for the most part.
I know. I work on a language that is immutable by default.
> However, if you can't do your job without these helpers at all, that's going to be a problem. At some point, there will be something that your garbage collector can not clean up for you. There will be state that is going to be mutated, whether you like it or not. Your brain should be prepared for that.
It's not quite how you characterized it, but yes there are circumstances where a "mutable core" or even just a mutable domain make sense. Usually it's related to performance. The GC "not cleaning things" doesn't really have much to do about that though. You can create all kinds of mutable data (e.g., very large arrays) that the GC won't clean as a part of a gen0 collection.
For example, is "Speakeasy, Lightouch, Jazz" intended to be a single noun? A single thing? Or is it a collection? A sequence?
What do you mean by "getting stable shelter"? Or to "hand off notes to people"?
Have you written something already? is there a link?
With Speakeasy I'm aiming at a complete solution stack.
Does anyone, anywhere have an actual definition for this?
I thought the whole notion of "generations" was over and done with in the 1980s.