Join the discussion

Write your take first — we'll ask for email only when you're ready to publish.

  • Hacker News
  • Honestly, having watched people argue about what simple is for about the last 10 years, I've pretty much settled on it not being a well-defined term. We know complex when we see it for sure, at least when it is present in quantity, but simplicity is not just the absense of complexity. There's at least three concepts we're all trying to stuff into the same word, and they are not only not "orthogonal" they are often in conflict with each other. I don't even think it can be rehabilitated, it can only really be abandoned, to clear the way to trying to characterize the multiple concepts we're trying to stuff into this one word.

    It is especially dangerous when something is "good" and people try to appropriate the term to appropriate the goodness of the term, as if goodness flows from a term to the thing it is attached to rather than the other way around. "Simple" is good so my good thing must be "simple" to be "good". But it doesn't. Simple can even be bad, in the wrong place or in the wrong sort of "simple" for a given job.

    by jerf
  • Could you try to define these three separate concept?
  • > We know complex when we see it for sure, at least when it is present in quantity

    Because familiarity is a confound for intuition about complexity, even this is not always true.

    Maxwell's equations will look complex to the uninitiated, and can represent the pinnacle of simplicity to those who already understand them.

  • Complexity (the opposite of simplicity) has nothing to do with the size of a program, but usually there is a high chance that a large program is more complex than smaller one, purely because the complexity multiplies, not just adds up.

    A single regex line could be far more complex than a 100-line java program.

  • Simple never means it is easy and it is always the biggest misconception in software.
  • That is an understatement! And not only that, "simple" is relative. The common saying that gets push back is "Do the simplest thing possible." People always seem to ignore the "possible" part of that phrase. Forgive me for being a little cute here, but a complex solution is simpler than a really complex solution.
  • Depends on what you mean by "simple". In my head, specifically with software, simple is defined as "does less", which is definitely somewhat easier to make, than a complex program that "does more".
  • The word simple is used here a way I'm having trouble wrapping my head around.

    This specific usage appears to come from this linked talk, Simple Made Easy:

    https://www.youtube.com/watch?v=SxdOUGdseq4

    My reaction to the Unix pipeline was that, the reason it exploded in complexity is because the pieces were too simple. They were insufficiently expressive.

    But the word is used in a different way here, and I'll have to watch the talk to understand what exactly is meant. (Something like orthogonality?)

  • Agreed. There are multiple senses for "simple" and on of those senses is "small". It is actually a very useful sense too, as Solomonov/Levin/Kolmogorov/Chaitin-style inductive reasoning has shown.

    And it is important not to just make snappy quips by equivocating.

  • I don’t know. This reads more like a “Clojure is great” post, and Clojure is great. But the author takes a swing and a miss on the Rich Hickey magic. The UNIX example is contrived (the number of occurrences in the order they occur?), and trying to redefine simple in a way that excludes UNIX pipelines doesn’t work.
  • This is beside the point, and I'm being pedantic, but the unix pipeline and the clojure expression don't quite do the same thing.

    The clojure expression reads the entire file into memory first, and then operates on that memory representation.

    The pipeline processes the input in chunks. The two `sort`s might read the entire contents into memory, but an implementation like GNU sort will instead, for large inputs, create temporary files for sections of the input and then merge sort them to the output.

    You could make clojure do the same thing, but it might not be as "simple" as the pipeline.

  • This bothered me too. The author even said:

    > Now, let's say we want to make a small change: show the output in the original file order. In Clojure, this is fairly straightforward: store an ordered sequence of the words in word_seq, store a map from each word to its frequency in freq_map, iterate over the sequence, and look up each word in the map:

    Emphasis on just "storing" something. The author seems to not understand that this change makes the solutions categorically different.

    It's a little like critiquing the efficiency of moving a piano up a stairwell by saying why didn't they just use a crane via the window? Using a crane isn't a better way of getting a piano up the stairwell.

  • The author uses the term “coupling” to describe a simple program from the code’s perspective - “you need to have a good mental model of your program”. I agree, but I’d argue that more importantly you need to have a good domain model. Good design communicates accurately and a high Gulf of Evaluation/Execution are actually what makes programs feel “complex” to a user.

    These concepts simplify for the user these questions: “I know what I want - how do I make the program do it?” (Execution) and “The program did something — what state is it actually in?” (Evaluation)

    I believe the author was getting at these concepts, especially in their Google Drive example - how the large program has a “small” UX. Understanding the domain model provides a much stronger basis for designing user interfaces, and understanding the Gulf of Evaluation/Execution allows you to build incredibly complex-looking, large UX’s without confusing or overwhelming the user.

    by cush
  • Using "length of the correctness statement + length of its proof" works quite well as proxy for complexity of a component (the longer, the more complex).

    Copy pasted functions with subtle changes mean you cannot reuse the proof (DRY). Giant functions with lots of if/else statements however might cause a branch explosion in the proof. The right abstraction removes lots of assumptions that a proof could depend on, limiting the search space and often forcing elegance (this also applies to math, eg. when reasoning with abstract groups instead of integers). The wrong abstraction might force case distinctions on consumers of the abstraction.

  • Important to note as well, is that "simple" isn't "lesser" or "dumber", it can be "more" and "wider", yet still "simpler".

    Expectedly, Rich Hickey explains it best, watch the "Simple Made Easy" talk if you haven't before, one of the few talks I probably watch bi-yearly: https://www.youtube.com/watch?v=SxdOUGdseq4

    Few things, concepts and ideas have changed as much of my programming mind as Hickey's talk and ultimately Clojure have done over the years.

    Wish we had new amazing Hickey talks to link to, maybe it seems he's about the hang up the hammock perhaps?

  • He's speaking at the coming Clojure Conj, so there will be a new talk soon.
  • > The reason for this is that in Rust, a struct couples type-checking to a fixed data representation. You can't get one without the other.

    > Clojure decouples data representations from type checking.

    This is funny to me because seen from the other side, (this) Clojure couples runtime type information to data structures: you're no longer allowed to define a data structure that doesn't have some runtime type information attached. A fixed static structure is just the consequence of not adding dynamic type information.

    Meanwhile in Rust you can get type-checking ‘without’ a fixed structure by using trait objects.

    by Twey
  • Yeah that example was pretty flimsy and contrived.
  • Regardless of whether the type information is static or dynamic, you're still coupling some type to some data. The type is still implicit even after compilation; there still exists a structure to the data, even if that structure isn't easily discerned without the source. Or to put it another way: just because there's no runtime type information, doesn't mean that the data now is entirely decoupled from the type.
  • Great piece, very straightforward examples, although I did have to squint for quite a while to grok the Closure portion.

    I am currently building a piece of very modular software and it has been the hardest-to-design project of my entire career. I would never be allotted this amount of time-effort at any job I have held to make something this robust and clearly defined. Many aspects of this project have taken 3-5 rounds trying-and-trashing to get an abstraction that is uncomplicated.

    This is precisely why vibe coding is so successful for building tiny isolated scripts, and so disastrous for anything else. It's just really dang hard to build something large and simple.

  • Personally, I'm big fan of those pipes mentioned. And the tooling that can be created around those _simple_ "primitives". Comparison to Google drive is obscure, in a way that it compares one gigantic piece of software into these small and simple.

    Bottom line is probably true, but if you are an open-source maintainer mentioned, and you have only so few hours to spend, you just cannot create those gigantic softwares either. You need to choose from the cards on your hand.

  • > There's no native Unix equivalent to frequencies, this sort | uniq -c is the closest we can get. Not only is it less performant (it has to collect the full input into memory before continuing), but it ties aggregation to ordering.

    One of the core features of the Unix command-line is that it is user-extensible. If there's no "native" command equivalent to frequencies, you can write your own, and it will be given the same first-class treatment as any other binary in your PATH. This is entirely in keeping with the Unix philosophy of simple implementations.

  • This was my thought, too. Either you can extend your toolkit—make a utility like dedup (https://codeberg.org/napcakes/dedup) and put in on your path (yay! so easy)—or you can't and thus must define the boundary of the allowed toolkit. In this case, it seems overly disingenuous to not allow awk, which will do the thing for you just fine, no Clojure needed at all. awk is hashmaps galore.

    Honestly, so much of our conceptions of "what's wrong" is more to do with lack of familiarity with history than any actually unsolved problem.

  • I imagine just ask would be enough for most usecases:

    awk '{ freq[$0] += 1 } END { for (n in freq ){ print freq[n] " " n } }' < file.txt

  • Yeah, that point was weak. The argument went from "Unix pipelines are not simple" to "unix based operating systems don't have an equivalent to Clojure's frequency function by default so it's worse"

    OK but how would it look like if you had such a program? Shells are not known for having the extensive set of functions that real programming languages have.

  • Strong resonance with the famous essay "The Rise of Worse is Better" [1], which contrasted the (better) "MIT/Stanford style of design" with the (worse) "New Jersey approach".

    MIT/Stanford:

    > Simplicity -- the design must be simple, both in implementation and interface. It is more important for the interface to be simple than the implementation.

    New Jersey:

    > Simplicity -- the design must be simple, both in implementation and interface. It is more important for the implementation to be simple than the interface. Simplicity is the most important consideration in a design.

    TFA maps "simplicity" to "MIT/Stanford simplicity" (simplicity for the user) and "smallness" to "New Jersey simplicity" (simplicity for the developer).

    I wonder if the root of the tension between the two schools comes down to the ambiguity of the user/developer distinction. Developers are also users. Simplicity of implementation is helpful to developers when they are working directly on implementation, while simplicity of interface is helpful to developers when they are using other developers' work.

    [1] https://dreamsongs.com/RiseOfWorseIsBetter.html

  • I think you're right on that point of contention, it's too far to assert a universal and clear good/bad dynamic here because that line between developer/user is contextual and fuzzy.

    Another interesting irony I'll note, Lisp is the "New Jersey approach" towards symbolic AI. Americans clinging to their Lisp systems were deeply entrenched in a "worse-is-better" mindset. Your interface, the computational model, didn't need to be designed for logic programming, that was wholly secondary. Do everything as much as possible in Lisp, and then offload the relational description to a small (not simple) library. American knowledge engineers were looked at as overpaid procedural hackers with zero mathematical elegance and very little credibility. More or less the same perception these self-same Lisp-machine users had for Unix and C programmers.

    It's all about perspective, at the end of the day. Where we draw the line in the sand on these categories is free-to-choose, yet it also determines everything. We're always someone else's villain under different semantics.

  • I appreciate the New Jersey simplicity as a user (with development skills) too, though. It's usually just a matter of time before I have to dive into the program/library/whatever internals to fix a bug.