Readit News logoReadit News
wavemode · a year ago
The author's assertion is true - complexity has to live somewhere. The nuance, though, is that all places complexity can live are not created equal.

Let's take the example of memory management: by pushing that complexity into the type system, Rust forces the programmer to deal with it and design around it. At the expense of some performance, we could instead push this complexity into a runtime garbage collection system. Since the runtime system understands things about the runtime characteristics of the program that can't be proven via static analysis, it can also handle more things without the programmer having to intervene, thus reducing the difficulty of the programming language. For most programmers this is a positive tradeoff (since most programmers are not writing software where every microsecond matters).

Similar tradeoffs exist in many different areas of software engineering. One monolith, where all the information is in one place, is easier to write code in than two microservices, which keep having to ask each other questions via API call. Yet, sometimes we need microservices. Rendering your web application entirely on the frontend in React, or entirely on the backend with templates, where all the logic lives in one place, is much easier than doing server-sided rendering then hydrating on the frontend. Yet, sometimes we need server-sided rendering and hydration.

Complexity is an irreducible constant, yes, but cognitive load is not. Cognitive load can increase or decrease depending on where you choose to push your complexity.

gorjusborg · a year ago
(I'm not responding directly to parent post, more adding my two cents in agreement)

'Necessary complexity' needs to live somewhere. There is often a core of complexity that is intrinsic to the problem being solved. This cannot be removed, only moved/converted/etc..

That doesn't mean everything needs to be complex, and that you need to 'collect' it somewhere. There is such a thing as unnecessary complexity, and code that has a lot of it is 'bad code'. Don't fall for the trap of thinking that you can't improve code by identifying and eliminating unnecessary complexity.

cle · a year ago
I'd add the additional nuance that cognitive load is highly dependent on the brain of the subject. One person's cognitive load is another person's effortless routine--it largely depends on how often the person works with those concepts and how quickly they can switch into the "mode". Where one person sees a wall of indecipherable matrix variables in Python, another person sees that it's obviously just a Cholesky decomposition as part of a Monte Carlo simulation.

So where to push the complexity should be dependent on who will be interacting with it and what they consider "cognitive load".

As an additional example:

> Rendering your web application entirely on the frontend in React, or entirely on the backend with templates, where all the logic lives in one place, is much easier than doing server-sided rendering then hydrating on the frontend.

For an expert front-end React developer, rendering entirely in the backend with Jinja templates would be higher cognitive load than the other options, even if it is technically simpler.

suraci · a year ago
IMO, cognitive load is the complexity of 'the scale of knowledge you need to build the solution (the problem space).'

However, complexity also comes from the solution itself — caching, microservice architecture, or even poorly chosen variable names.

So, complexity is irreducible, but it’s not a constant.

Certain solutions partition the problem space, thereby partitioning the complexity. This reduces the local complexity and, consequently, the cognitive load. However, the global complexity still remains and can even increase.

clarkmoody · a year ago
The other tradeoff comes with ease of debugging. Compile-time vs runtime errors. Dredging through microservice logs vs stack traces from the monolith.
freehorse · a year ago
I do not think of complexity as one thing. Abstractions are about both hiding and exposing complexity at the same time. Different levels of abstractions can expose or isolate different part of complexity. Exposing parts of it in a way that they become amenable to your tools is as important as isolating other parts somewhere in the background. Essentially, this has to do with how well a given abstraction choice maps into the structure of the problem-space and the relationships there. The choice of which parts of complexity you isolate and which you expose is important. You probably do not want to deal with everything at once, but also usually you cannot avoid dealing with something.

The way I primarily see (and often like) type systems wrt complexity is as choosing which parts of complexity are important and exposing them (and rest being still there to deal with). There is a cognitive aspect to abstractions and complexity, irrespective even of IDEs, debuggers, compilers etc. I personally want my abstractions to make at least some sense in my head or a piece of paper in the way I think about the problem before even I start writing code. If the abstractions do not help me actually cognise about (some part of) the problem, they probably solve other problems, not mine.

chuzz · a year ago
That's why Typescript/Python optional typing hit the best balance for me. Coding in duck-typed language is generally fine when your test suite is as fast and frequent as a type checker. That also explains why TDD is more popular in say Ruby or Python vs. Java. Speaking of Java, the problem with types is when you try to reify every single problem you encounter in your codebase. By the way, python has structured types since 3.8, and I hope they get more popular in Python code: https://docs.python.org/3/library/typing.html#typing.Protoco...
youerbt · a year ago
> That also explains why TDD is more popular in say Ruby or Python vs. Java.

I'd say that TDD being more popular in untyped languages speaks against TDD, as it hints that maybe some of its benefits are covered already by a type system.

bluGill · a year ago
You did clarify latter a bit, but this cannot stand unchallenged. TDD and tests solve different problems from types and so are valuable for that. Tests assert that no matter what you change this one fact remains true. Types assert that you are using the right things in your code.

I don't think it is lack of types at fault for untyped languages liking TDD (though I miss types a lot). I think it is there is no way to find out if functions exist until runtime (most allow self modifying code of some form so a static analysis can't verify without solving the halting problem). Though once you know a function exists the next step of verifying the function (or an overload in some languages) exists does need types.

dlahoda · a year ago
types are just autoverified logic. tdd just tests logic which cannot be typed in given type system. in lean4 one can type a lot(dependant types to test integration shapes and proofs are proptests).
marcosdumay · a year ago
It's blatantly obvious that some of the benefits of extensive testing are covered by a type system. Even by a mostly useless one like Java's.

If you look at any well tested program in a dynamic language, almost all the tests check the same properties that a type system would also check by default. If you remove those, usually only a few remain that test non-trivial properties.

EDIT: And I just love that in the time I took to write this, somebody wrote a comment about how it isn't so. No, it is still blatantly obvious.

hitchstory · a year ago
Id say if you think tests and types are doing the same thing in the same way you are badly abusing at least one of them.

One attacks the problem of bugs from the bottom up and the other from the top down. They both have diminishing returns on investment the closer they get to overlapping on covering the same types of bug.

The haskell bros who think tests dont do anything useful because "a good type system covers all bugs" themselves havent really delivered anything useful.

sevensor · a year ago
The article makes one of my favorite points about types: types aren’t there to constrain you; what they constrain is the complexity of the program.
dartos · a year ago
I don’t think I agree that either typescript nor rust successfully hide the complexity in their type systems.

By the nature of type systems, they are tightly coupled with the code written around them.

Rust has rich features to handle this coupling (traits and derives), but typescript does not.

andrewflnr · a year ago
It's not about hiding the complexity in the type system, that is, the complexity of the type system. At least for Rust, it's about that (yes, complex) type system isolating the even worse complexity of tracking lifetimes and aliasing and such, for all possible control flow paths, in your head.

It's harder to summarize what Typescript is isolating, except that JavaScript function signatures are the flipping wild west and the type system has to model most of that complexity. It tends to produce very leaky abstractions in my experience unless you put in a lot of work.

mmis1000 · a year ago
Sometimes the original js function isn't safe at all. So does the typescript definition.

For example, `Object.assign` overrides all property with same name. Sometimes you use it to construct a new object, so it is a safe usage. But what about using it to override the buildin object's property? It is definitely going to explode the whole program. However there isn't really a mechanism for typescript to differ the usage is safe or not. So in order to maintain compatibility, typescript just allow both of them.

And typescript in my opinion don't really isolate very much complexity. But it does document what the 'complexity' is. So you can offload your memory tax to it. Put it away, do something else, and resume later by looking at what definition you write before. In this way. It can make managing a big project much easier if you make proper use of it.

tomnipotent · a year ago
The argument isn't that complexity is being hidden, but how it's managed and where it shows up in your experience of solving other problems. OP mentions:

> The complexity was always there... it merely shone a light on the existing complexity, and gave us the opportunity — and a tool with which — to start grappling with it

It's not about Rust vs. TypeScript per se but uses garbage collection and borrow checker as examples of two solutions to the same problem. For whatever task you have at hand, what abstractions offer the best value that lets you finish the solution to the satisfaction of constraints?

> they are tightly coupled with the code written around them

Which is where the cost of the abstractions comes in. Part of the struggle is when the software becomes more complicated to manage than the problems solved and abstractions move from benefit to liability. The abstractions of the stack prevent solving problems in a way that isn't bound to our dancing around them.

If I'm working on a high-throughput networked service shuffling bytes using Protobuf, I'm going to be fighting Node to get the most out of CPU and memory. If I'm writing CRUD code in Rust shuffling JSON into an RDBMS I'm going to spending more time writing and thinking about types than I would just shuffling around arbitrarily nested bag-of-bags in Python with compute to spare.

I always thought this was why microservices became popular, because it constrained the problem space of any one project so language abstractions remained net-positives.

dartos · a year ago
> how it's managed and where it shows up in your experience of solving other problems

That’s what I’m talking about. Encoding complexity in your types does not manage where that complexity lives or where you have to deal with it.

It forces you to deal with that complexity everywhere in your codebase.

knome · a year ago
I didn't get the general idea that the author thought they hid the complexity, but rather that they exposed and codified it. They gave the complexity that would previously live in your head somewhere it could be expressed. And once expressed, it can be iterated on.
dartos · a year ago
Encoding complexity in your type system forces you to deal with that complexity throughout your codebase. It doesn’t give complexity a specific place to live.
atoav · a year ago
Type systems like in Rust may introduce their own complexities, but they also help you tackle the complexity of bigger programs if wielded correctly.

Typesystems can be complex to use, but in the end they constrain the degrees of freedom exposed by any given piece of code. With a type systems only very specific things can happen with any part of your code, most of which the programmer may have had in mind — without a type system the number of ways any piece of code could act within the program is way larger. Reducing the possible states of your program in the case of programming error is a reduction of complexity.

Now I don't say type systems may introduce their own complexity, but in the case of Rust the complexity exposed is what systems programmers should handle. E.g. using different String types to signify to the programmer that your OS will not allow all possible strings as file names is the appropriate amount of complexity. Knowing how your program handles these is again reducing complexity.

Imagine you wrote a module in a language where you don't handle these. Every now and then the module crashes specifically because it came across a malformed filename. Or phrased differently: The program does more than you intended, namely crashing when it encounters certain filenames. Good luck figuring that out and preventing it from happening again. With a type system the choice had to be explicitly made during programming already. Less things you code can do, less complexity.

Many developers confuse complexity of the internal workings of a program with the complexity of the program exposed at the interface. These are separate properties that could become linked, but shouldn't.

earth_walker · a year ago
Abstractions are a way to manage complexity - hiding things is only one way to do that. Deciding how to organize it, when and how to expose it, and when to get out of the way, are all important aspects of designing abstractions.
agentultra · a year ago
Isolating complexity, I would say, is a consequence of using good abstractions... not necessarily the essence of abstraction however. The essence of abstractions are in semantics. I define a type and an algebra of relations on that type which gives me theorems I can use. That is the essence. The consequence is that I can now think in terms of the theorems and definitions I've established rather than all of the details at the lower-level.

However, sometimes it's a bit over-rated when all that's needed is some information hiding and indirection, which is what this article appears to be discussing. These tools are the ones that are "leaky" in the sense that the complexity they attempt to hide often escapes the confines of their interface. It tends to give "abstraction" a bad reputation among programmers who have to deal with such systems.

Essential complexity does have to live somewhere. Best to be upfront about it.

IronRod · a year ago
I find this topic particularly interesting. I've often said to others that software, in itself, is a general abstraction of one or more complex tasks. The whole point of software is to hide complexity and make possible, in a hopefully simpler manner, doing things that would otherwise be very difficult or impossible. Despite what users may experience, the complexity remains but becomes hidden.
bb88 · a year ago
Python showed what relaxed types could do. And we could go a long way as it turns out without types. But there are use cases for types, and even python admitted such when they added type annotations.

However, when I was a kid a would put a firecracker next to an object. I didn't bother running the scenario through a compiler to see if the object was of type Explodable() and had an explode() method that would be called.

saghm · a year ago
> However, when I was a kid a would put a firecracker next to an object. I didn't bother running the scenario through a compiler to see if the object was of type Explodable() and had an explode() method that would be called.

Duck typing: if it quacks like a duck, and it explodes objects next to it, it's a firequacker

cjfd · a year ago
Duck typing. If it quacks like a duck and swims like a duck it might be a duck. But it might also be a nuclear submarine doing a duck impersonation. The question is whether you want a nuclear submarine in your pond.
stuartaxelowen · a year ago
Python showed that you can be wrong about your types and still build a successful product.
yakshaving_jgt · a year ago
Unless your product is a Boeing 737 MAX.
rapjr9 · a year ago
One of the problems with abstraction is that while it hides complexity, it makes changes that must reach into that complexity difficult. Abstraction is great if the code is never going to change again. If the people using the code want new features, then abstraction is a barrier. Getting around the abstraction barrier makes the code more complex. You have to think about the entire life cycle of the code, not just what looks pretty when you first write it. Most developers have no idea what their code will be used for 5 or 10 years into the future. As an example people have been trying to abstract away the complexity of network connections for decades, without a lot of success in keeping the complexity hidden. Someone always needs direct intervention in a layer in the network stack to make their product work right.