137 comments

[ 2.8 ms ] story [ 196 ms ] thread
(comment deleted)
> "4. Objects must always be initialized to a valid state. Not doing so is a compile-time error."

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?

You use sum types / optional<T> / Maybe / Option<T> / whatever your language calls them. Such a value can have two states: 'None', where it calls no value, and 'Some(t)',where it carries a value 't'.

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'.

Not quite. There can be a null value if the object type allows for it, think Optional<T>. What you can't have is an undefined object, as you could in Javascript for example.
If you forget everything you ever learned about programming, and then I ask you for an integer, you're never in a million years going to reply "null"; the existence of "null" is something you had to learn. A language could simply not have a concept of "null". Even an OO language could have no concept of "null".

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.

Null is just an invalid pointer target that you can verify easely. It is just abi convenience and removes the need for a flag.

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.

At ABI level you might want to use all-bits-zero to represent something other than a valid instance of your type, sure; that's how Rust's option type works. But making your language semantics say that that has to be allowed everywhere a valid instance could be passed is a mistake; for most functions it's an error to call that function without passing valid argument values, so it's better if the language has a way of enforcing that rather than forcing you to write it out by hand every time.
Ye some "always valid kinda a pointer"-type is nice. I wish there was some standardized compiler support for that in C, like MS kinda silly empty macros in function signatures to annotate input and output pointers etc.
I don't really view that as being relevant. F# compiles `Option.None` down to a null under the covers; what matters is that the type system is null-aware.
>So this essentially means there can never be a NULL object assigned to anything... how is this possible?

I believe Crystal doesn't let anything be null either. It's quite interesting.

Not only that, it implies that there must be a valid null-state for every type, which is also a bad idea.
No, it implies that there must be a generic two-valued datatype to wrap every state:

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.

I'm not entirely sure what you're getting at here. Are you saying the author is arguing a call like

  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.
> 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.

No it doesn't; the whole point is to avoid every type having a null state. You only define null states for those types for which it's semantically valid. For most types, you just don't have a null state and don't allow them to be constructed in an invalid state.
No, it implies that you can use your type system to surface the fact that a variable may have no values.
You can still have “the absence of a value”, but it’s codified in the type system. Most languages with this feature implement an Optional or Maybe wrapper type, that is either Some<T> or None (i.e. no value).

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).

You can represent the possible absence of value with your type system. You can use a Maybe/Option type for example.
> So this essentially means there can never be a NULL object assigned to anything…

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 don’t care what my language will let me do, I care more about what my language won’t let me do.

I think this is the best summary of the article. In short, please protect me from my own stupidity and/or laziness.

I like how this could be taken as an endorsement or criticism of the article, depending on the hubris of the reader
(comment deleted)
Or the hubris of the language designer.
I would only take it as hubris or condescension if the language designer chose to write their compiler in a language other than the one they were designing. Otherwise, I'd interpret the presence of strong guard rails as humility, an acknowledge of how fallible our puny human minds are and how much help we need from machines to minimize the cost of our inevitable mistakes.
I find that these sorts of polarizing statements which either side can take to mean a one-up on the other side tend to get lots of upvotes on applicable forums.
Alan Kay referred to this as the "Wirth school of non-programming".

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.

Why was it called non-programming? Because it is so concerned with limitations?

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.

I think you need to start with a limited language in a given paradigm before you use a language that tries to be all things.

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.

Well, I'd like to differentiate between different use cases. Am I exploring an idea on a lazy afternoon or am I one of dozens contributing to a code base older than the millennium in which bugs might cause measurable loss and some top500 exec call our enterprise's exec? I wouldn't think it reasonable to expect that there is one language which suits those (and there are other) needs equally well. I know which language I'll enjoy better and which my co-workers (and potentially my later self) enjoy better. It helps to be paid for using the latter.
Accessibility (as in execution environments) trumps language features in my book, and JS is the most accessible language there is. But of course, it all boils down to what you're trying to solve and where your program will live.
(comment deleted)
It is very likely that JS is the most accessible language in terms of execution environments, but it is not so as a consequence of the issues that are discussed in this article.
I lost the line of reasoning (or perhaps the author did) at the second example.

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.

Note that F# has the `[<NoEquality>]` attribute for exactly that sort of case.
That's the wrong default though.

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.

Alzabraic types and records have strutural equality while classes don't have it I believe.
I don't understand how you concluded that Example 2 implies that the author thinks that `a.Equals(b)` should work on InputStream objects.
I'm not sure you're contradicting the author as much as taking his argument a step further. His second example is arguing for value-equality instead of reference-equality as a saner default. He created two "value-ish" Customer objects and used them to motivate his argument.

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.

'0' is a string (or char), not a number. Just saying.
A couple other counterexamples to

> "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-...

Speakeasy, Lightouch, Jazz is a fifth-generation programming language. I find only small hurdlesv when showing it to programmers. My challenge now is mainly getting stable shelter so I can compile these notes and hand them off to people.
I really can't work out what you're trying to say here. I can guess, but maybe it would be better if you wrote something more expository or discursive.

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?

I'm assuming that comment was written by a bot, tbh.
Judging by that user's other comments, you're probably right.
(comment deleted)
> fifth-generation programming language

Does anyone, anywhere have an actual definition for this?

I thought the whole notion of "generations" was over and done with in the 1980s.

"Objects containing the same values should be equal by default."

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.

The whole point is that equality should have nothing to do with comparing references.

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.

The point they raise is valid, though the tone is lacking.

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)].

> The real world is all about mutable state. At some point, you need to take the training wheels off.

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.

There are no points in the real world, the are things, like chairs. In the real world, if you were to move a chair to the right, the natural way to do it would not be to build the same kind of chair at the desired coordinate and then, upon discovering that you do not need the extra chair, to discard it.

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.

I did not say nor act that it is the right way. I think it is easier to reason about code if you cannot (accidentally) represent things that don’t exist in your model, which in my experience happens very easily with mutable state. I used a point as an example.
Accountants do not use erasers.

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.

One small nitpick with this otherwise excellent post - Paxos allows a cluster of nodes to agree on a _single_ value, not a sequence (which requires Multi-Paxos or some other similar extension of 'basic' single-decree Paxos).
They do that because they want to keep a history of each transaction. I don't want to keep a history of all the positions of a character in my game.
> They do that because they want to keep a history of each transaction.

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.

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.

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.

> 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.

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.
Fair enough, I understand what you mean now. By namedropping Paxos and Raft you were leading me onto something quite different.
Fun fact my first accounting job we had to use pencils so we could erase and change the figures if we ever got audited.

I left shortly after being taught this business practice but sure makes a great anecdote.

> The real world is all about mutable state. At some point, you need to take the training wheels off.

So the C#, VB, and F# compilers (all huge, "immutable data representations" codebases serving millions of developers worldwide) are training wheels?

Not sure what you mean, the C# compiler source code I looked at seems to be using a lot of mutable data structures.

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.

> Not sure what you mean

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.

A bit of a tangent, but I tried F# recently to make a small gamedev framework. I absolutely adored the language at first. Loved the syntax. loved the type definitions and domain modeling. I felt maybe, this is it, this is the language for me. Never felt that way about a language since the last time I wrote something in Crystal.

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

Could you go into a little more detail what task you were trying to solve?
Its been a while, so I might be saying it wrong but, I was trying to create an entity collection. Essentially, a super basic scene graph. on every tick in my update call, i would update something about a specific entity in that collection (maybe position, transform etc). And then in my draw call I would render the entire updated collection. Just getting a specific entity from the list was a huge issue and then updating it and putting it back was worse. I gave up after a many days of trying something as basic as this. In JS, Nim, Crystal etc. this would have taken me a couple of lines of code.

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

> Just getting a specific entity from the list was a huge issue and then updating it and putting it back was worse.

I must be missing something, but this sounds like a pretty straightforward List.map

(comment deleted)
Yep, this is definitely something that should be made easier and there's an RFC to address it.

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.

The article goes through a series of examples to show motivations for the following:

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.

Rust's borrow checker would prevent Example 5 from compiling, since once you add `cust` to the collection, you can't touch it anymore (unless you insert a clone or etc.). So in this case at least, the inability to reason about code can be resolved by banning mutable aliasing, without eliminating mutability.
Rust's ownership system would prevent example 5 from working (because you have to move the instance into the set).

The borrow checker is about validating that references don't outlive their target, and R^W.

5. Once created, objects and collections must be immutable.

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.

I'm not going to claim that mutability is never useful for performance, but many large scale simulations can be expressed quite elegantly using bulk operations on arrays or other structures, with no mutability in sight. Both particle simulations a la n-body and stencil operations are in this category. An efficient low-level implementation of such bulk operations involves mutable updates, just like any functional language is compiled to "impure" assembly code, but the programming model used for application programming can remain pure.
I concur, I should have been more precise in my comment.
Interesting. Can you explain, with a somewhat simple example, how this can be efficiently implemented, or at all? I mean preserving the appearance of immutability at the source language level, while mutating the original structure under the hood for performance.
(comment deleted)
Any vectorised operation in Numpy is an example of this. The pure subset of Numpy can be used to write useful programs, but the Numpy functions/methods are mostly implemented in impure C.

Another example is completely pure array programming such as in Accelerate[0] or Futhark[1].

[0]: http://www.acceleratehs.org/

[1]: https://futhark-lang.org

Not very knowledgable on this myself, unfortunately, but I believe that in graphics programming, shaders written in GLSL often take the form of a series of functional, mathematical transformations of vertices. Those transforms are run in the GPU as highly parallelized array operations, probably using a lot of mutable state. But those details are mostly hidden from the shader programmer.
Thanks to all who replied.
A somewhat related idea is called "benign effects." The idea is that you write code with an immutable interface that uses mutation in its implementation.

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.

Isn’t it possible (at least in theory) to make mutability an implementation detail of the compiler/runtime? Rust’s borrow checker approaches this, but the abstraction leaky or nonexistent. Additionally, many high performance computing applications (e.g. Tensorflow) abstract away expensive mutable operations, so at least in theory, it should be possible to isolate mutability to small segments of code where mutability is opt-in.
Yes, Haskell as a pure functional language does this too. A naive copy-by-value handling of lists will usually end up in the same order of magnitude for performance as mutate-in-place linked lists in C. The compiler can track those immutable values and just mutate them in place, when it can guarantee that's a safe operation. The vast majority of the time, you can get away with just copying a pointer or renaming, not the whole variable.

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.)

> 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.

I think if performance is part of the requirements of your code, then performance must be a part of your type signature.

For example, a tail-recursive function needs to have it’s type as tail-recursive.

This is where linear types and in general quantitative type theory comes into play. Also eagerness / laziness annotations.

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.

It’s neccessary to annotate tail recursion because you are making it clear to the compiler that your initial assumption about the performance of this function is that it will not explode the stack.

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.

Rust's im[1] and rpds[2] crates are refcounted pointers to immutable data structures, but support mutable operations on &mut instances. When an instance is cloned, it merely creates another pointer. When an instance is modified, it uses Arc::make_mut() to only clone each tree node if it has other users. This approach has runtime overhead, but makes nested updates (foo[0][0].attr = 1) as simple as mutable structures.

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/

[2]: https://docs.rs/rpds/

F# and OCaml have mutable arrays.
Linear types fix this problem, by letting you prove to the compiler that logically immutable operations can be implemented as in-place updates.
Mutability is an abstraction, it doesn't forbid in place modification of data. What it forbids is other code accessing data that holds references the array prior to the modification, which creates a logical error.
1. Variables should not be allowed to change their type.

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.

> Interpreting the same region as a buffer of 64-bit ints vs 16-bit uints

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.

In descendants of ML (Haskell, OCaml, Rust etc) you can use Algebraic Data Types to condense your wall of methods to one function
Algebraic data types have runtime case information, and won't let you reinterpret the underlying bits of a binary buffer between types. I think grandparent meant they wanted pointer casts, unions, reinterpret_cast, or transmute.
(comment deleted)
I thought if you need to reason about the code you use Ada with Spark?
> Objects containing the same values should be equal by default.

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.

'You cannot program without mutation in the general case. You simply don’t always have the performance/power budget for it.'

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 mean for a programming language to be able to handle the general cases it should also handle the maybe 10% or 20% of programs that might need mutation in their hot paths. Obviously one solution is to call out to a different programing language for only the areas that require it, but I’d consider that a definite deficiency if it’s commonly required.

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.

"Depends on what problems you are solving I suppose. " - well, yes. Hence my question about what you're defining as the general case; that 10-20% seems rather high for cases you'd need mutability.

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?

I’d only advocate using GC’d languages as a general (ie covering as many use cases as possible in one language - this isn’t even necessarily a good idea to begin with!) where it’s easy to dodge it. For example it must be easy and reasonably idiomatic controlling what is and isn’t heap allocated.

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.

How is it avoiding subtle bugs? Looks like a sane default to me
Which default is default though? An ordinal comparison? Or system-local one? (E.g one that considers a German ß equal to “ss” or one that doesn’t?).

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.

To me "avoiding mutations" does not mean "no mutations", but rather "no mutations in-between states in a state machine". The system should model state as an immutable snapshot, while mutations when forming that state are completely acceptable.
There is obviously mutation at some level of abstraction but usually one form of coding is idiomatic and one isn’t. I’m all for using immutable data and pure functions, but I’d discard a language and standard library that doesn’t have a good/idiomatic story for e.g mutating large arrays in place or has a good selection of contiguous (array-backed) collections be too limited for many types of work.
Among reasonable programming languages (as defined in this article), which one compiles to the most optimized JavaScript that can efficiently interoperate with external JS code? Reason or OCaml via BuckleScript? F# via Fable? Scala? Maybe Kotlin is close enough?
I would bet my money on Bucklescript/ReasonML/ReScript.

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)

Is Reason even actively developed these days? Isn't it sort of a dead project, or have I misunderstood something?
I just discovered that the BuckleScript team started a rebranding to ReScript a few months ago: https://rescript-lang.org/
Yeah, that's the issue with this kind of tools, unfortunately. I myself would like a more functional-oriented approach, but realistically JS/TS is a more stable ecosystem. My go-to choices are JS for small individual projects and TS for larger projects or if a team is involved.
it's all about surprises. Programmers hate them. You want to be able to reason about your code

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).
> 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.

> 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).

even with the python `int`s a: int, b:int and the `str` function as the pure deterministic function you still cannot make the conclusion. It's an utter mess
I agree that the more stuff the language lets you do - the more people will abuse it. But there is also a productivity of higher level languages.
In example #6, he gives this as the unreasonable approach:

  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).

No. In languages like F#, you can encode in the type system that the result can be either a customer or an error. And the compiler won't let you "access" the customer until you've safely checked that the result is, indeed, the customer rather than an error.

In this way you end up encoding more intent within your types and behaviours rather than implicitly letting the consumer figure it out themselves.

Adding to this, it's nice to have the compiler enforce these things rather than rely on the dev to see the variable name and treat it accordingly. In the latter case, you're one sleepy afternoon away from having things blow up in production because you "thought there was no way that that variable could be a null pointer"
> the compiler won't let you "access" the customer until you've safely checked that the result is, indeed, the customer rather than an error.

/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.

> Do most (or any) languages in which people take this approach actually enforce handling of all 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.
Typescript allows this

  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
  }
Well Typescript is really a static code analyser for Javascript and not a language, since it does nothing at runtime, and that's fine, its type system can catch a broad range of issues and improve code quality drastically. It's a huge godsend for big codebases. But afaik it doesn't guard against null by default.

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.
> Do most (or any) languages in which people take this approach actually enforce handling of all cases?

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) }
    );
> 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: [...]

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.

So which languages apart from F# are reasonable according to the authors requirements?
> The fundamental paradigm of OO (object-identity, behavior-based) is not compatible with “reasonability”, and so it will be hard to retrofit existing OO languages to add this quality.

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.