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, creat…
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 underst…
Simple Is Not Small
81–90 of 95 posts
Re: Simple Is Not Small
#82I've been thinking about a new AI based dimension to this. If your program is split into smaller decoupled "modules" then all the code for each "module" can fit into an AI context window. In this way the AI can have all the context to edit a "module" by just loading all the code for that "module". You would not need things like vector search as much. If we assume 20 tokens per line of code and the AI context window i…
> then that would argue for having "modules" between 5,000 and 50,000 loc, depending on which AI model you are using. FWIW, I set hard limits to 200 LOC for every single source code file in any AI-related projects, also with restrictions on "formatting hacks" and other golf-like stuff. I think beyond 5000 LOC in a single file and all available models already get lost frequently, even if the "context limit" theoretica…
I already preferred to keep source code files under about 200 LOC, so.
Re: Simple Is Not Small
#83This argument is just based on "I wish the things I need to do were baked into the language." It's nice when that happens, but once programs get sufficiently large and complex, it stops mattering--you're dealing with domain-specific concepts that have zero built-in helpers and you're just building everything yourself regardless.
https://news.ycombinator.com/item?id=49600275 is a great illustration of how to use Bash's "means of combination" more effectively than what OP thought of doing. But notice that it still relies on decorate-sort-undecorate for keyed sorting, and not only that but the undecoration has to be deferred until after `uniq` has made use of it, in a way that magically collapses the first decoration into something that can be used for a second sort, while adding a second decoration (the count). And then it still needs postprocessing because you can't express control over the structure of the decoration (there isn't really any, it's just string concatenation, but I mean you don't even control the order in which the pieces are concatenated).
Re: Simple Is Not Small
#84Great 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 roun…
Brooks, in “the Mythical Man-Month”:
“One occasionally reads newspaper accounts of how two programmers in a remodeled garage have built an important program that surpasses the best efforts of large teams. And every programmer is prepared to believe such tales, for he knows that he could build any program much faster than the 1000 statements/year reported for industrial teams.
Why then have not all industrial programming teams been replaced by dedicated garage duos? One must look at what is being produced.
In the upper left of Fig. 1.1 is a program. It is complete in itself, ready to be run by the author on the system on which it was developed. That is the thing commonly produced in garages, and that is the object the individual programmer uses in estimating productivity.
There are two ways a program can be converted into a more useful, but more costly, object. These two ways are represented by the boundaries in the diagram.
Moving down across the horizontal boundary, a program becomes a programming product. This is a program that can be run, tested, repaired, and extended by anybody. It is usable in many operating environments, for many sets of data. […] As a rule of thumb, I estimate that a programming product costs at least three times as much as a debugged program with the same function.
Moving across the vertical boundary, a program becomes a component in a programming system. This is a collection of interacting programs, coordinated in function and disciplined in format, so that the assemblage constitutes an entire facility for large tasks. To become a programming system component, a program must be written so that every input and output conforms in syntax and semantics with precisely defined interfaces. The program must also be designed so that it uses only a prescribed budget of resources — memory space, input-output devices, computer time. Finally, the program must be tested with other system components, in all expected combinations. […] A programming system component costs at least three times as much as a stand-alone program of the same function. The cost may be greater if the system has many components.
In the lower right-hand corner of Fig. 1.1 stands the programming systems product. This differs from the simple program in all of the above ways. It costs nine times as much. But it is the truly useful object, the intended product of most system programming efforts.”
Re: Simple Is Not Small
#85Earlier quoted context omitted.
Nice to see more people getting nerdsniped by the sh code. ;-) Yes, associative arrays work well. I think it should even be possible to use bash associative arrays. But at that point you're no longer doing classic sh - awk is basically halfway to Perl. (And pretty awesome.)
Yeah, insisting on "only" shell is weird, shell is at heart a process orchestrater, and denying it it's processes is rejecting most of it's functionality. It is equivalent to saying "do this in python, but you are not allowed to use any modules" Without processes shell is just a weird sad little language, with them it turns into this epic data flow language. With some real design stinkers, Most of these are due to it…
I've been working on a design for a Python library to facilitate this sort of thing.
Re: Simple Is Not Small
#86The Unix pipeline and the similar-looking Clojure expression are entirely different in how they do what they do. Pipes are process abstractions. The (perniciously improperly understood) "pipeline" macro is not pipelining anything in any way (not CPU nor process nor memory). It is merely syntax sugar to write a deeply nested call chain as a "flat" list of operations.
And it goes on to compare "ugly" code, again making the category error that "more" code is "worse". Because, one is swapping / splicing entire programs within a pipeline. Also, there are plenty of ways to slice that mango; you don't need temp files.
That said, having intermediate files in one's pipeline is a big help because one can use those to make pipelines idempotent. Plus, one doesn't need flat files, one could swap in a SQLite cache too, at will, without modifying anything else in the pipeline. This kind of design change is not possible with the equivalent Clojure function call chain, as-is. In fact, having such a requirement (one always finds need to restart processes after crashes, and have them pick up from where they died) causes us to write some custom (and therefore design-wise brittle) conditional restart loop on top of the computation to manage its failures inside the running program. An idempotent Unix pipeline simply needs to be... restarted from outside the process.
In this particular case, a valid complexity complaint would be about the lacunae of the Bash / shell programming language (and interpreter model) versus the Clojure language.
Also, again from a program design point of view, I feel there's a bit of "holding it wrong" going on there... Bash / Shell-fu, yes, but not enough to be dangerous (for example, sort | uniq | sort is a standard idiom of pipeline programming). Which claim is personal, and so I'm open to being corrected at the same level. Sources: my code and writing:
https://www.evalapply.org/tags/bash/
https://www.evalapply.org/tags/clojure/
https://github.com/adityaathalye (the pinned repos are Bash and Clojure)
And specifically, this log processing code, for a more apples-to-apples comparison with OP's post.
https://github.com/adityaathalye/bash-toolkit/blob/master/lo...
deduplicate() {}
frequencies() {}
drop_first_n() {}
drop_last_n() {}
drop_header_footer() {
drop_first_n "${1}" |
drop_last_n "${2}"
}
window_from_to_lines() {}
(edit: some clarifications, and references)Re: Simple Is Not Small
#87> 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 informatio…
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.
Re: Simple Is Not Small
#88Great 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 roun…
> 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. Brooks, in “the Mythical Man-Month”: “One occasionally reads newspaper accounts of how two programmers in a remodeled garage have built an important program that surpasses the best efforts of large teams. And every programmer is p…
Re: Simple Is Not Small
#89Earlier quoted context omitted.
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.
I think I struggle to assemble a coherent notion of what it means to (conceptually) decouple a value from its type. You can completely forget the type of a value and treat it as opaque bytes, but then there are no valid operations left on the value. Even moving it around or discarding it may be invalid if it's pointed to elsewhence. The only thing you can meaningfully do is try to recover its (static or dynamic) type…
In this sense, most statically-typed languages conflate how data is structured with how it is restricted. Some overlap is unavoidable, as anything represented by a single byte is always going to be restricted to at most 256 values, but Clojure tends to take the view that the more decoupling (or decomplecting) you can achieve the better.
This can be useful when dealing with data that is in some sense invalid. You might receive data that's outside expected bounds or even of a different type, and it might make sense to handle it in some fashion. This is a common necessity in pharmaceutical trials, for example.
Re: Simple Is Not Small
#90Earlier quoted context omitted.
I think Excel is a good example that everyone can build a clear mental model on how to use it, but implementing one is a daunting task.
Are you nuts? Excel is great, but it's an arcane beast rivaled only by Emacs configs. People pass down Excel formulas by word of mouth like they're magic spells.