Greppability is an underrated code metric
morizbuesing.com
morizbuesing.com
For the past decade-plus I have mostly only searched for user facing strings. Those have the advantage of being longer, so are more easily searched.
Honestly, posts like this sound like the author needs to invest some time in learning about better tools for his language. A good IDE alone will save you so much time.
All of that is trivial to search for with a tool that understands the language.
When you string interpolate to build the name you lose searchability.
Isn't string search, or grepping for patterns, even more trivial? So what is your argument? You found an alternative method, good, but how is it any better?
In my own case, I wrote a library that we used in many projects, and I often wanted to know where and how functions from my lib were used in those projects. For example, to be able to tell how much of an effort it would be for the users to refactor when I changed something. However, your method of choice at least with my IDE (Webstorm) only worked locally within the project. Only string search would let me reliably and easily search all projects.
I actually experimented creating a "meta" project of all projects, but while it worked that lead to too many problems, and the main method to find anything still was string search (CTRL-SHIFT-F Find dialog in IDEA IDEs is string search and it's a wonderful dialog in that IDE family). I also had to open that meta project. Instead, I created a gitignored folder with symlinks to the sources of all the other projects and created a search scope for that folder, in which the search dialog let me string-search all projects' sources at once right from within the library project and still being able to use the excellent Find dialog.
In addition, I found that sometimes the IDE would not find a usage even within the project. I only noticed because I used both methods, and string search showed me one or two places more than the method that relied on the underlying code-parsing. Unfortunately IDEs have bugs, and the method you suggests relies on much more work of the IDE in parsing and indexing compared to the much more mundane string or string pattern search.
It's not trivial when you looking for symbols in context.
> the method you suggests relies on much more work of the IDE in parsing and indexing compared to
...compared to parsing and indexing you have to do manually because a full-text search (especially in a large codebase) will return a lot of irrelevant info?
Funnily enough I also have a personal anecdote. We had a huge PHP code base based on Symfony. We were in the middle of a huge refactoring spree. I saw my colleagues switch from vim/emacs to Idea/WebStorm looking at how I easily found symbols in the code base, found their usages, refactored them etc. compared to the full-text search they were always stuck with.
This was 5-6 years ago, before LSP became ubiquitous.
Did you miss the comparison? The "more trivial"? The context of my response? Please read the parent comment I responded to, treating my comment as standalone and adding some new meaning makers no sense.
String search is more trivial than a search that involves an interpretation of the code structure and meaning. I have no idea why you wish to start a discussion about such trivial statement.
> * because a full-text search (especially in a large codebase) will return a lot of irrelevant info?*
It doesn't do that for me but instead works very well. I don't know what you do with your symbol names, but I have barely any generic function names, the vast majority of them are pretty unique.
No idea how you use search, but I'm never looking for "doSomething(", it's always "doSomethingVerySpecific()", or some equally specific string constant.
I don't have the problems you tell me I should have, and my use case was the subject of my comment, as should be clear, as well as my comment being a response to a specific point made by the parent comment.
Some literal in a log message may come from the code or it might be remapped in some config file outside the language the LSP is looking at, or an environment variable etc.. I just go back and forth with grep and IDE tools, both have different tradeoffs.
Just because I have 20 usage of 'shipping_address' doesn't mean I'll have this string 20 times in different places.
Grep has its place and I often need to grep code base which have been written without much thoughts towards DX. But writing it nicely allows LSP to take over.
And grep cuts right through that in a pretty universal way. What the post describes are just ways to not work against grep to optimize for something ephemeral.
For example if I want to rename all “Dog” (DogModel, DogView, DogController) symbols to “Wolf”, find/replace is much better at that because it will tell me about symbols I had forgotten about.
However, it does suggest that there is an opportunity for factoring "Dog" out in the code, at least by name spacing (e.g. Dog.Model).
Personally I value uniqueness/canonicalness over conciseness. I would rather have DogView because then there is one name for the symbol regardless of where I am in the codebase. If the same symbol is used with differently qualified names it is confusing - I want the least qualified name to be more descriptive than “View”.
The other culture is to lean heavily on namespaces and to not worry about uniqueness. In this case you have View and Dog.View that may be used interchangeably in different files. This is the dominant culture in Java and C#.
Some language servers support modifying the symbols in contexts like docstrings as well.
Do you have an example?
It does usually make that kind of DogModel -> WolfModel refactoring.
But I don't see why LSP servers shouldn't support this, still. I'm not sure if the LSP specification allows for this as of current, though.
Something that lets me say that I want to replace “Dog\(.*\)” with “Wolf\1”, but where each substitution is performed only within single “symbols” as identified by TS or LSP.
[1] https://ast-grep.github.io/guide/rule-config/atomic-rule.htm... [2] https://ast-grep.github.io/guide/rule-config/atomic-rule.htm...
A kind of in-between I've found for some search and replace action is comby (https://comby.dev/). Having a matching braces feature is a godsend for doing some kind of replacements properly.
They're either incomplete (you don't get ALL references or you get false references) or way too slow (>10 seconds when rg takes 1-2).
Recommendations are most welcome.
If your feelings are anemic when tasked with doing a grep, its because you have lost a very valuable skill by delegating it to a computer. There are some things the IDE is never going to be able to find - lest it becomes the development environment - so keeping your grep fu sharpened is wise beyond the decades.
(Disclaimer: 40 years of software development, and vim+cscope+grep/silversearcher are all I really need, next to my compiler..)
Sure you can replace most uses of a knife with power tools, but there is a reason why most top chefs still rely on that knife for most of those tasks.
A hand powered drill is more like a hand powered meatgrinder. It has the same limitation as the powered versions, and is simply a more primitive version.
At the end of the day, I'm here to solve problems, and there's no end to them -- might as well get a head start.
> lazy... programmers
Since when was that a bad thing? Since time immemorial, it has been hailed as a universal good for programmers to be lazy. I'm pretty sure Larry Wall has lots of jokes about this on Usenet.Also, I can clearly remember switching from vim/emacs to Microsoft Visual Studio (please, don't throw your tomatoes just yet!). I was blown away by IntelliSense. Suddenly, I was focusing more on writing business logic, and less time searching for APIs.
Command line tools like grep are force multipliers for programmers. GUI's come with the risk of not being able to learn how to leverage this power. In the end, that often leads to more manual work.
And today, bash is a lingua franca that you can bring with you almost everywhere. Even Windows "speaks" bash these days, with WSL.
In itself, there's nothing wrong with using the built-in features of a GUI. Right-clicking a method (or using a keyboard shortcut) to find the definition in a given code base IS nice for that particular operation.
But by knowing grep/awk/find/git command line and so on, combined with bash scripting and advanced regular expressions, you open up a new world of possibilities.
All those things CAN be done using Python/C#/Java or whatever your language is. But a 1-liner in bash can be 10-100 lines of C#.
I use grep multiple times a day. I write bash scripts quite often. I'm not speaking from a position of ignorance of these tools. They have their place as a lowest common denominator of programming tools. But settling for the lowest common denominator is not a path to productivity.
Doesn't mean you should forget your skills, but it does mean you should investigate better tools. And leverage them. A lot.
> But a 1-liner in bash can be 10-100 lines of C#.
Yes. And the reverse is also true. bash is fast and easy if there's an existing tool you can leverage, and slow and hard when there's not.
The OP was saying "use IDEs but don't stop using lower tech tools, because they are powerful"
Every person, including devlopers, have some constraints to what they're able to learn and use effectively. Those limits vary a lot from person to person, though.
For developers who learn technology a bit slowly (compared to some other developers, not the general population), some of these tools may not be worth the effort.
Also, these developers aren't necessarily low tier in terms of business value. They may have talents when it comes to understanding and communicating business requirements with other stakeholders in their organization, and their technical skills may be secondary to those skills and abilities.
BUT: For the general audience at HN, technical capability is central to their identity. Most people here have some capacity to learn technologies that go somewhat beyond the minimum skills required for a tech job. And for this audience, being confident on the linux/unix command line is generally worth the effort.
I'm not feeling anemic. The tool is anemic, as in, underpowered. It returns crap you don't want, and doesn't return stuff you do want.
My grep-fu is fine. It's a perfectly good tool if you have nothing better. But usually you do have something better.
Using the wrong tool to make yourself feel cool is stupid. Using the wrong tool because a good tool could make you lazy shows a lack of respect for the end result.
Unfortunately sometimes you can't, and sometimes you can but people can't be arsed, so this is still a consideration.
But being able to grep is really nice when trying to figure out something out about a source tree that I don't yet have set up to compile, nor am I a developer of. I.e., I've downloaded the source for a tool I've been using pre-built binaries of and am now trying to trace why I might be getting a particular error.
I only use grep to filter the output of CLI tools.
For code, I use my IDE or repository features.
- It's your day to day project and you expect to be working in it for a long time.
Scenarios where grepping is more useful:
- Your language has #ifdef or equivalent syntax which does conditional compilation making syntactic tools incomplete.
- You just opened the project for the first time.
- It's in a language you don't daily drive (you write backend but have to delve in frontend code, it's a 3rd party library, it's configuration files, random json/xml files or data)
- You're editing or searching through documentation.
- You haven't even downloaded the project and are checking things out in github (or some similar site for your project).
- You're providing remote assistance to someone and you are not at your main development machine.
- You're remoting via SSH and have access to code there (say it's a python server).
Yes, an IDE will save you time daily driving. But there's no reason to sabotage all the other usecases.
We also had a livegrep instance that we could use to grep any corporate repo, regardless of where it was hosted. That was extremely useful for investigating failures in build scripts that spanned multiple repositories (e.g. building a Go sidecar that relies on a service config in the Java monorepo).
I seem to have found the 64-bit mode under "Tools > Options" then "Text Editor > C/C++ > IntelliSense". The top option is [] Enable 64-bit IntelliSense.
But I can't seem to find the ram limit you mentioned and searching for it just keeps bringing up stuff related to vscode. Do you know where it is off the top of your head or a page that might describe it?
Edit: I take that back, this was a first-principles comment. There's a setting 'C_Cpp: Intelli Sense Memory Limit' (space included).
- greppability does not preclude IDE or language server tooling; there's often special cases where only certain e.g. context-dependant usages matter, and sometimes grep is the easiest way to find those.
- projects that include multiple languages, such as for instance the fairly common setup of HTML, JS, CSS, SQL, and some server-side language.
- performance in scenarios with huge amounts of code, or where you're searching very often (e.g. in each git commit for some amount of history)
- ease of use across repositories (e.g. a client app, a spec, and a server app in separate repos).
I treat greppability as an almost universal default. I'd much rather have code in a "weird" naming style in some language but have consistent identifiers across languages, than have normal-style-guide default identifiers in each language, but differing identifiers across languages. If code "looks weird", if anything that's often actually a _benefit_ in such cases, not a downside - most serialization libraries I use for this kind of stuff tend to do a lot of automagic mapping that can break in ways that are sometimes hard to detect at compile time if somebody renames something, or sometimes even just for a casing change or type change. Having a hint as to this fragility immediate at a glance even in dynamically typed languages is sometimes a nice side-effect. Very speculatively, I wouldn't be surprised if AI coding tools can deal with consistent names better than context-dependent ones too; greppability is likely not specifically about merely the tool grep.
And the best part is that there's almost no downside; it's not like you need to pick either a language server, IDE or grep - just use whatever is most convenient for each task.
```ts const getTableName = (addressType: 'shipping' | 'billing') => { if (addressType === 'shipping') { return 'shipping_addresses' } if (addressType === 'billing') { return 'billing_addresses' } throw new TypeError('addressType must be billing or shipping') } ```
I also had `j2p` and `p2j` that would convert between python (formatted via black) and json (formatted via jq), and the `j2p_clip`/`p2j_clip` versions that would pipe from clipboard and back into clipboards.
It's worth taking the time to build a few simple scripts for things you do a lot. I used to open up the repl and import json to convert between json and python dicts multiple times a day, so spending a few minutes throwing together a simple script to do it was well worth the effort.
{'country': ['25', '32', '6', '37', '72', '22', '17', '39', '14', '10',
'35', '43', '56', '36', '110', '11', '26', '12', '4', '5'],
'timeZone': '8', 'dateFrom': '2024-05-01', 'dateTo': '2024-05-30',
black is the opposite extreme from what i wanted; https://black.readthedocs.io/en/stable/the_black_code_style/... explains:> If a data structure literal (tuple, list, set, dict) or a line of “from” imports cannot fit in the allotted length, it’s always split into one element per line.
i'm not interested in minimizing diffs. i'm interested in being able to see all the fields of one record on one screen—moreover, i'd like to be able to see more than one record at a time so i can compare what's the same and what's different
black seems to be designed for the kind of person who always eats at mcdonald's when they travel because they value predictability over quality
For aligned column readability and other scenarios, # fmt: off and # fmt: on become crucial. The problem is that like # type: ignore, those start spreading if you're not careful.
Other than that, I actually quite like its formatting choices.
[tool.black]
line-length = 100
Aside from matrix-like or column aligned data, the only truly awful thing I've encountered has been broken f-string handling[2].[1]: Example from https://github.com/pythonarcade/arcade/blob/808e1dafcf1da30f...
Bold of everyone here to assume that everyone has a day to day project. If you're a consultant or for other reasons you're switching projects on a month to month basis, greppability is probably the top metric second to UT coverage.
I don't think anyone is assuming anything here. I've contracted for most of my career and this didn't seem like an outlandish statement.
Also, if you're working in a project for a month, odds are you could set up an IDE in the first few hours. Not sure how any of this rises to the level of being "bold".
I don't think we need to restrict the benefits quite that much—if it's a project that isn't my day-to-day but is in a language I already have set up in my IDE, I'd much prefer to open it up in my IDE and use jump to definition and friends than to try to grep and hope that the developers made it grepable.
Going further, I'd equally rather have plugins ready to go for every language my company works in and use them for exploring a foreign codebase. The navigation tools all work more or less the same, so it's not like I need to invest effort learning a new tool in order to benefit from navigation.
> Yes, an IDE will save you time daily driving. But there's no reason to sabotage all the other usecases.
Certainly don't sabotage, but some of these suggestions are bad for other reasons that aren't about grep.
For example: breaking the naming conventions of your language in order to avoid remapping is questionable at best. Operating like that binds your business logic way too tightly to the database representation, and while "just return the db object" sounds like a good optimization in theory, I've never not regretted having frontend code that assumes it's operating directly on database objects.
It's funny, because my preference and actual use is the exact opposite: for a project that isn't my day-to-day, I'm much more likely to try to grep through it rather than open it in an IDE.
Another overlooked advantage of greppability is to be able to fuzzy the search, or discover related code that wasn't directly linked to what you were looking for.
For instance if you were hunting for the method updating a `foo_bar` instance, grepping it will also give you instances of `generic_foo_bar` and `shim_foo_bar`. It can be noise, as it can be stuff you wouldn't have seen otherwise and save your bacon. If you're not familiar with a project I think it's quite an advantage.
> hope that the developers made it grepable
hopefully it's enforced at an organization level.
- the project is large enough that the IDE can't cope.
- you want to also match comments, commented out code or in-project documentation
- you want fuzzy search and match similarly named functions
I use clangd integration in my IDE all the time, but often brute force is the right solution.
Plus it means your engs don't learn how to deal with open source code concerns, e.g. libraries, forking, dependency management. Which gradually screws over the whole ecosystem.
If you're willing to put Google-scale effort into building your tooling, sure. Every problem is solvable. Only Google does that though, everyone else is getting by with a tiny fraction of the resources and doesn't already have a solid foundation to reduce those maintenance costs.
Your other points make sense, but in this case, at least for C/C++, you can generate a compile_commands.json that will let clangd interpret your code accurately.
If building with make just do `bear -- make` instead of `make`. If building with cmake pass `-DCMAKE_EXPORT_COMPILE_COMMANDS=1`.
x.so_something()
you might not know what is the type of x and what method does do_something refers to.You need a better IDE.
> - You just opened the project for the first time.
Go grab a coffee
> - It's in a language you don't daily drive
Jetbrains all products pack, baby.
> - You haven't even downloaded the project and are checking things out in github (or some similar site for your project).
On GitHub, press `.` to open it in a web-based vscode. Download it & open it in your IDE while you are doing this.
> - You're remoting via SSH and have access to code there (say it's a python server).
Don't do this. Check the git hash that was deployed and checkout the code locally.
VSCode SSH Extension for the win.
No IDE will resolve this if the same code is preprocessed more than once to produce different files; or if you often build with different and conflicting preprocessing values; or if your build uses a tool your IDE doesn't know about; or if some of the preprocessing and compilation occurs at runtime.
> Go grab a coffee
So, you're saying "wait".
> Jetbrains all products pack, baby.
JetBrains CLion won't even try to properly index C/C++ files which aren't officially part of the project.
Plus, if you have errors in some places - which occurs while you're editing your code - then that breaks very badly with JetBrains IDEs. e.g. missing closing paren or #endif
LSP-based tools are fine with this, generally. A syntactic understanding is an incomplete solution. I suspect GP meant LSP. (as long as compile_commands.json or equivalent is avilable).
Many of those other caveats are non-issues once LSPs are widespread. Even Github has lsp-like go-to-def/go-to-ref, though it's not perfect.
Plus there are cases where grep is really what you need, for example after updating a particular command line tool whose output changed, I was able to find all scripts which grepped the output of the tool in a way that was broken.
I attribute that mostly to my several decades of experience with vi(m) and command line tools, not to anything inherently bad about VSCode.
What counts as “better” tools has a lot of subjectivity and circumstances implied. No one set of tools works for everyone. I very often have to work over ssh on servers that don’t allow installing anything, much less Node and npm for VSCode, so I invest my time in the tools that always work everywhere, for the work I do.
The main project I’ve worked on for the last few years has a little less than 500,000 lines of code. VSCode’s LSP takes a few seconds fairly often to maintain the LSP indexes. Running ctags over the same code takes about a second and I can control when that happens. vim has no delays at all, and ripgrep can search all of the files in a second or two.
That won’t make LSP go any faster though. There’s still something interesting in the fact that a ripgrep of every line in the codebase can still be faster than a dedicated tool.
In the environments I often work in I can't install anything or run processes like node. I ssh into a server and have to use whatever came with the Linux distro, which means sticking with the tools I will find everywhere. I can't copy the code from the server either. If I get lucky they used version control. I know not everyone works with those constraints. I specialize in working on abandoned and legacy code.
i mean, i doubt i'm going to come up with some brilliant breakthrough that makes your life easier that you've somehow overlooked, but i'd like to understand what kinds of constraints people like you often confront
i'm just glad you don't have to use teamviewer
You can transfer any kind of file over ssh. scp, sftp, rsync will all copy binaries. Mainly the issues come down to policy and billable time. Many of my customers simply don't allow installing anything on their servers without a tedious approval process. Even if I can install things I might spin my wheels trying to get it to work in an environment I don't have root privileges on, with no one willing to help, and I can't bill for that time. I don't work for free to get an editor installed. I use the tools I know I can find on any Linux/BSD server.
With some customers I have root privileges and manage the server for them. With others their IT dept has rules I have to follow (I freelance) if I want to keep a good relationship. Since I juggle multiple customers and environments I find it simpler not having to manage different editors and environments, so I mostly stick with the defaults. I do have a .profile and .vimrc I copy around if allowed to, that's about it.
I can't lose time/money and possibly goodwill whining about not having everything just-so for me. I recently worked on a server over ssh that didn't have tmux installed. Fortunately it did have screen, and I can use that too, no big deal. I spent less than 60 seconds figuring that out and getting to work rather than wasting hours of non-billable time annoying someone about how I needed tmux installed.
wrt rdp, i feel like rdp is actually better than vnc or x11-over-ssh, but for cases where regular ssh works, i'd rather use ssh
i wasn't thinking in terms of installing tmux, more like a self-contained binary that doesn't require any kind of 'installation'
I pretty much live in ssh. Remote Desktop means a lot of clicking and watching a GUI visibly repaint. Not efficient. Every so often I have customers using applications that only run on Windows, no API, no command line, so they will enable RDP to that, usually through a VPN.
if awk, python, or perl is accepted, the possibilities expand further
(One-off small scripts for things, sure. But I'm not going to implement something like ctags or cscope or a LSP server in bash.)
and i wasn't suggesting trying to bill for doing it, but rather, if you were frequently in this situation, it might be reasonable to spend non-billable time between clients doing it
Advantages of Helix are pretty straightforward:
1. Very little configuration bullshit to deal with. There's not even a plugin system yet! You just paste your favorite config file and language/LSP config file and you're good to go. For anything else, submit a pull request.
2. Built in LSP support for basically anything an LSP exists for.
3. There's a bit of a new generation command line IDE forming itself around zellij (tmux that doesn't suck) + helix + yazi (basically nnn or mc on crack, highly recommended).
That whole zellij+helix+yazi environment is frankly a joy to work in, and might be the 2-3x improvement over neovim that makes the switch worth it.
I only recently moved from screen to tmux, and I still have to fall back to screen sometimes because tmux doesn't come with every Linux distro. I expect I will retire before I think tmux (or screen, for that matter) "sucks" to the point I would look at something else. And again I very often can't install things on customer servers anyway.
It conflicts with the clipboard and a bunch of hotkeys, and configuring it never works because they have breaking change in how their config file works ever 6months or so.
These days I only use it to launch a long running job in ssh to detach the session it's on and leave.
I know I could get more out of tmux but haven’t really needed to. I use it with the default config. I have learned from experience that the less I try to customize my environment the less non-billable time I waste trying to get that working and maintaining it.
You may feel 99% certain, but you got it wrong. I have quite a bit of experience with IDEs, you shouldn't assume I use vim out of ignorance. I have worked as a programmer for 40+ years, with development tools (integrated or not) that I have forgotten the names of. That includes "real" IDEs like Visual Studio, Metrowerks CodeWarrior, Symantec Think C, MPW, Oracle SQL Developer, Turbo Pascal, XCode, etc. and so on. When I started programming every mainframe and minicomputer came with an IDE for the platform. Unix came along with the tools broken out after I had worked for several years. In high school I learned programming on an HP-2000 BASIC minicomputer -- an IDE.
So I have spent more than "a couple of months in real IDEs" and I still use vim day to day. If I went back to C++ or C# for Windows I would use Visual Studio, but I don't do that anymore. For the kind of work I do now vim + ctags + ripgrep (and awk, sed, bash, etc.) get my work done. At my very first real job I used PWB/Unix[2] -- PWB means Programmer's Work Bench -- an IDE of sorts. I still use the same tools (on Linux) because they work and I can always count on finding a large subset of them on any server I have to work with.
I don't dislike or mean to crap on IDEs. I have used my share of IDEs and would again if the work called for that. I get what I need from the tools I've chosen, other people make different choices, no perfect language, editor, IDE, what have you exists.
[1] https://en.wikipedia.org/wiki/Integrated_development_environ...
The HP 2000 [1] had a timeshared BASIC system that the school district made available to schools, over ASR-33 teletypes with dial-up modems. The BASIC system could edit, run (translate to byte code and execute), manage files. No version control or debuggers back then. The HP 2000 had another layer of of the operating system accessible to administrators (the A000 account if I remember right) but it was the same timeshared BASIC system with some additional commands for managing user accounts and files.
No one familiar with modern IDEs would recognize the HP 2000 BASIC system as an IDE, but it was self-contained and fully integrated around writing BASIC programs. HP also offered FORTRAN for it but not under the timeshared BASIC system. A friend wrote an assembler (in BASIC!) and taking advantage of a glitch in the bytecode interpreter we could load and run programs written in assembly language.
After high school I got a job as night computer operator with the Multnomah County ESD (school district) so I had admin access to the HP 2000, and their two HP 3000 systems, and an IBM computer they used for crunching class registrations. Good times.
Someone had an emulator online for a while, accessible over telnet, but I can't find it now.
HP TSB did not have a REPL. If your program crashed or stopped you could not examine variables from the terminal. You could not peek or poke memory locations as you could with microcomputer BASICs (which didn't support multiple users, so didn't have the security concern). You had to insert PRINT statements to debug the code. TSB BASIC didn't have compile/link steps, it tokenized the code as you entered the lines, and the interpreter amounted to a big switch statements on the tokens. P. J Brown's book Writing Interactive Compilers and Interpreters (1981) describes how TSB works. Eventually I got the source code to TSB (written in assembler) and figured it out for myself.
Other BASIC implementations that popped up around the same time had richer feature sets. In my senior year at high school I got (unauthorized) access to a couple of Unix systems in Portland, ordered the Bell Labs Technical Journal issues that described Unix and C, and taught myself from those. I didn't get paid to work on a Unix system until several years later (detours into RSTS-11, TOPS-20, VMS, Microdata, Pr1me, others) but I caught the Unix and C bugs young and I still work with those tools every day.
Some programmer friends and more than a few colleagues over the years have made fun of my continued use of what they call obsolete and arcane tools. I don't mind, I have never felt like I did less or sloppier work than my co-workers, and my freelance customers don't care what I use as long as I can solve their problems. Most of the work in programming doesn't happen at the keyboard anyway. I do pay attention and experiment with all kinds of tools but I usually end up going back to the Unix tools I have long familiarity with. That said I did spend many years in Visual Studio, MPW, CodeWarrior, and MPW writing C and C++ code, and I do think those tools (well, maybe not MPW) offered a lot of benefits over coding with vim and grep, for the projects I did back then.
Maybe ironically I use an iPad Pro, I don't have a desktop or laptop anymore. So I have the most modern hardware and a touch-based (soon) AI-assisted operating system that runs a terminal emulator during my work time.
what kind of obsolete and arcane tools do you use? vim seems to be pretty popular among the youngsters these days. a friend of mine a bit younger than you prefers ex
On the iPad I use Blink shell, the Github client, Apple Notes. I always have a paper notebook and pen, my short-term memory gets less reliable every year.
I have noticed a lot of younger people using or at least learning vim and CLI tools, maybe retreating from the complexity of modern software development setups. Maybe just a retro fad, I don’t know.
I also see terminal setups described online that try to reproduce the GUI experience — lots of colors and planes and widgets. Neovim and zellij both look like that to me — a lot of extraneous functionality and visual clutter that mimics VSCode. I prefer a more minimalist environment. I don’t even use syntax coloring.
Everyone has to find the tools that work for them, that takes time. I think most programmers figure out at some point in their career that continually experimenting and polishing their tools doesn’t always help get the work done, and when you freelance you get more conscious of billable vs. non-billable time.
i don't think younger people using vim is a fad, but we'll see. vim to me, especially neovim, seems mostly like emacs in vi clothing
Something like vim is designed as a general text-editing tool. Sure, you can load it up with plugins and scripts that give you a bunch of features you'd find in an IDE, but the experience is not the same, and the "integrated" bit of "IDE" is still just not there.
(And I say this as someone who does most of his coding in vim, with LSP plugins installed, only reaching for a "proper" IDE for Java and Scala.)
One metric I would use: if I can sit down at a random co-worker's desk and feel more or less at home in their editor of choice, then it's probably an IDE that has reasonable defaults and is geared for software development. IDEA and VSCode would qualify... vim would certainly not.
I dont map out projects in terms of semantics, I map out projects in files and code - That makes querying intuitive and I can easily compose queries that match the specificity of what I care about (e.g. I might want to find a `Server` but I want to show both classes, interfaces and abstract classes).
For the specific toolchain I'm using - typescript - the symbol search is also unusable once it hits a certain project size, it's just way too slow for it to be part of my core workflow
- dynamically built identifiers is 100% correct, never do this. Breaks both text search and symbol search, results in complete garbage code. I had to deal with bugs in early versions of docker-compose because of this.
- same name for things across the stack? Shouldn't matter, just use find usages on `getAddressById`. Also easy way to bait yourself because database fields aren't 1:1 with front-end fields in anything but the simplest of CRUD webshit.
- translation example: the fundamental problem is using strings as keys when they should be symbols. Flat vs nested is irrelevant here because you should be using neither.
- react component example: As I mentioned in another comment, trivially managed with Find Usages.
Nothing in here strikes me as "routinely solves harder problems," it's just standard web dev.
advice to the effect of 'you should not make a mess' is obviously correct but also, in many situations, unhelpful. sometimes i'm not smart enough to figure out how to solve a problem without making a mess, and sometimes i inherit other people's messes. in those situations that advice decays into 'you should not try to solve hard problems'
Funny.
But since you asked. The hardest problems I've solved haven't been technical problems for years. Not that I stopped solving technical problems, or that I started solving only the easier problems. I just learned to solve people problems more.
People problems are much harder than technical problems.
The author showed a simple people problem: someone who needs to know about better tooling. If we were working together, showing them some tricks wouldn't take much time and would improve their productivity.
An example of a harder problem is when someone tries to play aggressive little word games with you. For example, trying to put you down by loudly making assumptions about your career and skills. One way to deal with that is to just laugh it off. Maybe even make a self-deprecating joke. And then continuing as if nothing happened.
But that assumes you want or have to continue working productively with them. If you don't, it can be quite enjoyable to just laugh in their face. After all, it's never the sharpest tool in the shed, or the brightest light that does that. In fact, it's usually the least useful person around, who is just trying to hide that fact. Of course, once you realize that, it becomes hard to laugh, because it's no longer funny. Just sad and pitiful.
(office space, https://www.youtube.com/watch?v=hNuu9CpdjIo)
look, lucumo, i'm sure you have excellent people skills. which is why you're writing five-paragraph power-trip-fantasy comments on hn about laughing in people's faces as you demonstrate your undeniable dominance over them, then take pity on them. but i'm not sure those comments really represent a contribution to the conversation about code greppability; they're just ego defense. you probably should not have posted them
you aren't the first person i've seen expressing the transparently nonsensical sentiment that 'people problems are much harder than technical problems'. i've seen it over and over again for decades, but i've never seen a clear and convincing explanation of why it's nonsense; i think this is worth discussing in some depth
an obvious thing about both people problems and technical problems is that they both cover a full spectrum of difficulty from trivial to impossible. a trivial people problem is buying a soft drink at a convenience store†. a trivial technical problem is tying your shoes. an impossible people problem is ending poverty. an impossible technical problem might be finding a polynomial-time decision procedure for an np-complete problem, or perhaps building a perpetual-motion machine, or a black-hole generator. both kinds of problems have every degree of difficulty in between, too. stable blue leds seemed like an impossible technical problem until shuji nakamura figured out how to make them. conquering asia seemed like an impossible people problem until genghis khan did it
even within the ambit of modifying a software system, figuring out what parts of the code are affected by a possible change, there are trivial technical problems and problems that far exceed current human capacities. nobody knows how to write a bug-free web browser or how to maintain the linux kernel without introducing new bugs
given this obvious fact, what are we to make of someone saying, 'people problems are much harder than technical problems'? obviously it isn't the case that all people problems are much harder than all technical problems, given that some people problems are easy, and some technical problems are impossible. and if we interpret it as meaning that some people problems are much harder than some technical problems, it's a trivial tautology which would be just as true if we reversed the terms to say '[some] technical problems are much harder than [some] people problems'. so nobody would bother making the effort to say it unless they thought someone was asserting the equally ridiculous position that all people problems were easier than technical problems
the most plausible interpretation is that it means that the people problems the speaker is most familiar with, and therefore considers typical, are much harder than the technical problems the speaker is most familiar with. it's not a statement about the world; it's a statement about the author and the environment they're familiar with
we can immediately deduce from this that you are not andrew wiles, who spent six years working alone on a technical problem which had eluded the world's leading mathematicians for some 350 years, for the solution of which he was appointed a knight commander of the order of the british empire and awarded the abel prize, along with a long list of other prizes. you give the appearance of being so unfamiliar with such difficult technical problems that you cannot imagine that they even exist, though surely with a little thought you can see that they do. in any case, for a very long time, you have not been working on any technical problems that seem impossible to you. i believe you that it's not that you started solving only the easier problems; that means that all the problems you ever solved were the easier problems
or, more briefly, you aren't accustomed to dealing with difficult technical problems
perhaps we can also infer that you frequently handle very difficult people problems—perhaps you are a politician or a clinical psychologist in a mental institution, or you have family members with serious mental illness. however, other aspects of your comment make that seem relatively unlikely
______
† if you have no money or don't speak the local language, this people problem becomes less trivial
No, it's not "transparently nonsensical" -- it expresses a common human experience that techies who are at least somewhat self-aware (obviously that excludes you) have had at work. Their education gave them a toolbox for approaching technical problems, but no training in the people problems.
It's not remotely saying that all technical problems are easy.
Completely agreed. The React component example in the article is trivial solvable with any modern IDE; right click on class name, "Find Usages" (or use the appropriate hotkey, of course). Trying to grep for a class name when you could just do that is insane.
I mainly see this from juniors who don't know any better, but as seen in this thread and the article, there are also experienced engineers who are stubborn and refuse to use tools made after 1990 for some reason.
Before calling people stubborn or assuming they got left behind out of ignorance, consider your assumptions. 40+ years experience, senior in both experience and age at this point. Long-term vim + command line tools user.
Do you have any evidence that shows "A good IDE alone will save you so much time?" Have you seen studies comparing productivity or code quality or any metric written by people using IDEs vs those using a plain editor with grep?
By "so much faster" what do you mean exactly? I have decades of experience with vim + ctags + grep (rg these days, because I don't want to get called a stubborn stick in the mud). I can find and change things in large codebases pretty fast. I used VSCode for a year on the same codebases and I didn't feel "so much faster," and I committed to it and watched numerous how-to videos and learned the tool well enough to train other programmers on it. No 10x improvement, not even 1.5x. For most tasks I would call it close to the same in terms of time taken to write code. After getting burned a couple times with "Replace symbol" in VSCode I stopped trusting it. After noticing the LSP failed to find some references I trusted it less. I know grep/ack/rg/ctags aren't perfect, but I also know their weaknesses and how to work with them to get them to do what I want. After a year I went back to vim + ctags + rg.
We might have more productive (and friendly) interactions as programmers if we remembered that not everyone works the same way, or on the same kind of code and projects. What we call "best practices" or "modern tools" largely come down to familiarity, received wisdom, opinion, and fashion -- almost never from rigorous metrics and testing. You like your IDE? Great! I like my tools too. Would either of us get "so much faster" using a different set of tools? Probably not. Trying to find the silver bullet that reduces accidental complexity in software development presents an ongoing challenge, but history shows that editors and IDEs don't do much because if they did programmers today would outperform old guys like me by 10x in a measurable way.
At the last full-time job I had, at an educational software company with 30+ programmers, everyone used Eclipse. My first day I got a new desktop with two big monitors, Eclipse installed, ready to go. I installed vim and the CLI subversion client and some other stuff and worked from the command line, as I usually do. I left one of the monitors off, I don't need that much screen space, and I don't have Twitter and Facebook and other junk running on a second monitor all day like most of the other people did. I got made fun of, old man using old tools. Then once a week, like clockwork, Eclipse would auto-install some updates and everyone came to a halt trying to resolve plugin version conflicts, getting the team in sync. Hours and hours wasted regularly just getting the IDE to work. That didn't affect me, I never opened Eclipse. Watching the other programmers it seemed really slow. So just maybe Eclipse could jump to a definition faster than vim + ctags (I doubt it), but amortized over a month Eclipse all by itself wasted more time than anyone possibly saved with the more powerful tool. Anecdote, I know, but I've seen this play out in similar ways at more than one shop.
Just last year a new hire at a place I freelance for spent days trying to get Jetbrains PHPStorm working on a shared remote dev server. Like VSCode it runs a heavy process on the server (including the LSP). Unlike VSCode, PHPStorm can actually kill the whole server, wasting everyone's time and maybe losing work. I have never seen vim or grep bring a whole server down. I could add up how much "faster" PHPStorm might turn out compared to vim, but it will have to recoup the days lost trying to get it to work at all first.
1. do you have the application working locally (for some context-specific meaning of locally, as this heavily depends on what you're doing);
2. can you run the tests, or at least some meaningful subset of tests;
3. does the source code not report errors (it is INSANE how many juniors will ignore big red squiggly lines from imported libraries which haven't been set up properly);
4. can you attach a debugger;
5. can you navigate the source code (i.e. symbols, go to definition, find usages).
If you can do all this, it doesn't matter if you're using vim or VSCode or JetBrains. The OP of the article failed at least number 5 and probably others as well because they haven't set up their development environment properly and are resorting to string-based tooling to get around that. Trying to enforce nonstandard naming conventions to get around this (per the JavaScript example) shows lack of experience.
I work with legacy code that very often doesn’t have a development environment or unit tests, often not even version control. Searching the code for strings and symbols (a subset of strings) gets more important in an unfamiliar code base. So does jumping to symbols and debugging, but nothing the OP wrote implies they only use grep
Consistent and searchable code is great, but the article author picked awful ways to achieve that. Adding verbosity and nonstandard field names (which will require a bunch of custom linter rules...) to support someone who doesn't have a proper development environment set up is silly. It reminds me of the arguments from 00s Java developers who insisted that having to type out class names on the left, i.e. `EnterpriseBeanFactoryServerImpl foo = new EnterpriseBeanFactoryServerImpl()` made code more readable, but it's really because they didn't have a dev environment with working type inspection set up. It's a real problem, but other than their point about not using dynamically generated identifiers, the author presents totally the wrong solution.
> I work with legacy code that very often doesn’t have a development environment or unit tests, often not even version control. Searching the code for strings and symbols (a subset of strings) gets more important in an unfamiliar code base. So does jumping to symbols and debugging, but nothing the OP wrote implies they only use grep
Very true, but IMO if you're working with legacy code where tests don't run, there's no source control, and you can't get a LSP working, worrying about whether a class is called `SpecificAttributeAlertDialog` or just `Dialog` is plugging holes in the Titanic at that point.
The OP described naming things consistently so you don’t have Invoice and customer_inv etc. scattered around referring to the same thing. I think most programmers understand that as a good practice. And the OP mentioned the problem with constructing symbols dynamically. I agree with that. Your examples address different problems.
I took issue with your statement about “experienced engineers who are stubborn and refuse to use tools made after 1990 for some reason.” If the tools made after 1990 have clear and measurable advantages, then I would agree that not using them might come from stubbornness. But you didn’t offer any examples of that, or metrics and experiments, just your opinions and preferences. I’m skeptical of such opinions and prescriptions —. the “clean code” type rules presented as scripture without supporting evidence. OP wrote about some practical considerations that make sense to me.
with a legacy codebase, or a fork of a dependency that had to be patched which uses an incompatible buildsystem, or any C/C++/obj-c/etc that heavily uses the preprocessor or nonstandard build practices, or codebases that mix lots of different languages over awkward FFI boundaries and so on and so forth -- there are so many situations where sometimes an IDE just can't get you 100% of the way there and you have to revert to grepping to do any real work
that being said, I don't fully support the idea of handcuffing your code in the name of greppability, but I think dismissing it as a metric under the premise that IDEs make grepping "obsolete" is a little bit hasty
I wish, but no. I've found people will make a mess of everything. Which is why I don't trust solutions that rely on humans having more discipline, like what this article advocates.
In any situation where grep is your last saviour, you cannot rely on the greppability of the code. You'll have to check and double check everything, and still accept the risk of errors.
You can't imagine how much faster I was than everybody else at answering questions about a large codebase just because I knew how to use ripgrep (on Windows). "Knowing how to grep" is a superpower.
Sometimes it's because I don't completely trust the IDE to find everything I'm interested in (justifiably; sometimes it doesn't). Sometimes it's because I'm not looking to dive into the code and do serious work on it; I'm just doing a quick drive-by check/lookup for something. Sometimes it's because I'm ssh'd into another machine and I don't have the ability to easily open the sources in an IDE.
I've used this technique on auditing many code bases including the C family, perl, Visual Basic, C# and SQL.
With this sort of tool, I don't need to look for language-particular parsers--so long as the source is in a text file, this works well.
The great IDEs IntelliJ and Webstorm stopped autosuggesting completions from the symlinked project.
Open up Sublime Text again. Worked perfectly. That is why Jetbrains and their behemoth IDEs are utter shite.
Write your code to have symmetry and make it easy to grep.
Having dealt with IntelliJ for 3 years due to education stuff - I laughed out here. Even VS is better than ideaj.
I am also waiting for world peace! ; )
You can maybe skip the greppability if the code base is of a size that you can hold the rough shape and names in your head, but a "get a list of things that sound like they might be related to my problem" operation is still extremely helpful. And it's also worth keeping in mind that greppability matters to onboarding.
Does that mean it should be an overriding design concern? No. But it does mean that if it's cheap to build greppable, you probably should, because it's a net positive.
Basically in vim to substitute text you'd usually do something with :substitute (or :s), like:
:%s/textToSubstitute/replacementText/g
...and have to add a pattern for each differently-cased version of the text.
With the :Subvert command (or :S) you can do all three at once, while maintaining the casing for each replacement. So this:
textToSubstitute
TextToSubstitute
texttosubstitute
:%S/textToSubstitute/replacementText/g
...results in:
replacementText
ReplacementText
replacementtext
:S/textToFind
matching all of textToFind TextToFind texttofind TEXTTOFIND
But not TeXttOfFiND.
Golly!
In my setup, `/foo` will match `FoO` and so on, but `/Foo` will only match `Foo`
[1] https://www.gnu.org/software/emacs/manual/html_node/emacs/Re...
One minor inconvenience is that the scoring should ideally be different per filetype. For instance, Python would count "foo-bar" as two symbols ("foo minus bar") whereas Lisp would count it was one symbol, and that should ideally result in different scores when searching for "foobar" in both. Similarly, foo(bar) should ideally have a lower different score than "foo_bar" for symbol search even though the keywords are separated by the same number of characters.
I think this can be accomodated by keeping a per-language list of symbols and associated "penalties", which can be used to calculate "how far" keywords are from each other in the search results weighted by language semantics :)
lets say someone would make a plugin for their favorite IDE for this kind of search. How would the details look like?
To keep it simple, lets assume we just do the super-case-insensitivity, without the other regex condition. Lets say the user searches for "first_name" and wants to find "FirstName".
one simple solution would be to have a convention where a word starts or ends, e.g. with " ". So the user would enter "first name" into the plugin's search field. The plugin turns it into "/first[-_]?name/i" and gives this regexp to the normal search of the IDE.
another simple solution would be to ignore all word boundaries. So when the user enters "first name", the regexp would become "/f[-_]?i[-_]?r[-_]?s[-_]?t[-_]?n[-_]?a[-_]?m[-_]?e[-_]?/i". Then the search would not only be super-case-insensitive, but super-duper-case-insensitive. I guess the biggest downside would be, that this could get very slow.
I think implementing a plugin like this would be trivial for most IDEs, that support plugins.
Am I missing something?
* my understanding was simply that the regex would (A) recognize `[a-z][A-Z]` and inject optional _'s and -'s between... and (B) notice mid-word hyphens or underscores and switch them to search for both.
If you're going that far, and you're in a context which probably has a parser for the underlying language ready at hand, you might as well just convert all tokens to a common format and do the same with the queries. So searches for foo-bar find strings like FooBar because they both normalize to foo_bar.
Then you can index by more than just line number. For instance you might find "foo" and "bar" even when "foo = 6" shows up in a file called "bar.py" or when they show up on separate lines but still in the same function.
> So the user would enter "first name" into the plugin's search field.
Why wouldn't the user just enter "first_name" or "firstName" or something like that? I'm thinking about situations like, you're looking at backend code that's snake_cased, but you also want it to catch frontend code that's camelCased. So when you search for "first_name" you automagically also match "firstName" (and "FirstName" and "first-name" and so on). I wouldn't personally introduce some convention that adds spaces into the mix, I'd simply convert anything that looks snake/kebab/pascal/camel-cased into a regex that matches all 4 forms.
Could even be as stupid as converting "first_name" or "firstName", or "FirstName" etc into "first_name|firstname|first-name", no character classes needed. That catches pretty much every naming convention right? (assuming it's searched for with case insensitivity)
Ya. Query tokenizer would emit "first" and "name" for both. That'd be neat.
So you's search for "/first\_name/i".
/first[-_]?name/i
Or to use your example, just checking for underscores and not also dashes: /first_?name/i
Backslash is already used to change special characters like "?" from these meanings into just "use this character without interpreting it" (or the reverse, in some dialects).Improving the IDE to find one or the other by searching for one or the other is missing the point or the article, that consistency is important.
I'd rather have a simple IDE and a good codebase than the opposite. In the example that I gave the worst thing is that it's the framework which forces you do use these two names for the same thing.
I didn't miss the point, I disagreed with the point because I think it's a tool problem, not a code problem. I agree with most other points in the article.
The other killer feature of nimgrep is that instead of regex, you can use PEG grammar [1]
[0] - https://nim-lang.github.io/Nim/nimgrep.html
[1] - https://nim-lang.org/docs/pegs.html(`first\S?name` is usually better, by ignoring whitespace -> better ignores comments describing a thing, but `.` is easier to remember and type so I usually just do that)
I implemented a format-agnostic search that can match patterns across various naming conventions like camelCase, snake_case, PascalCase, kebab-case. If needed, I'll integrate in space-separated words.
I've just published the tool to PyPI, so you can easily install it using pip (`pip install super-grep`), and then you just run it from the command line with `super-grep`. You can let me know if you think there's a smarter name for it.
If you do, email a link to hn@ycombinator.com and we'll put it in the second-chance pool (https://news.ycombinator.com/pool, explained at https://news.ycombinator.com/item?id=26998308), so it will get a random placement on HN's front page.
So if I'm trying to locate the error message "because the disk is full" but it's in the code as:
... + " because the " +
"disk is full")
then it will fail.So really, combining both our use cases, what would be great is to simply search for a given case-insensitive alphanumeric string in files that skips all non-alphanumeric characters.
So if I search for:
Foobar2
it would match all of: FooBar2
foo_bar[2]
"Foo " + \
("bar 2")
foo.bar.2
And then in the search results, even if you get some accidental hits, you can be happy knowing that you didn't miss anything.But the string split thing you mentioned happens a lot when searching for OpenStack error messages in Python that is often split across lines like you showed. My current solution is to randomly shift what I'm searching for, or try pick the most unique line.
(Norwegian here. Our languages are similar, but we miss this one.)
I know that they invented "curl". Do you tar xfz?
The german equivalent of the word would be probably "greifbar". Being able to hold something, usually used metaphorically.
Would "grasp" work?
The best dual purpose translation of "zu greifen"/"gripe" (German/Scandinavian) meaning "zu begreifen"/"begripe"/"understand" would be "to grasp", which covers both physically grabbing into something and also to understand it intellectually.
All these words stem back to the Proto-Indo-European gʰrebʰ, which more or less completes the circle back to "grep".
Origin 1960s: a word invented by Robert Heinlein (1907–88), American author.
Anyway, to answer your question:
$ grep -Fxf <(ls -1 /bin) /usr/share/dict/swedish
ack
ar
as
black
dialog
dig
du
ebb
ed
editor
finger
flock
gem
glade
grep
id
import
last
less
make
man
montage
pager
pass
pc
plog
red
reset
rev
sed
sort
sorter
split
stat
tar
test
transform
vi
:)[edit]: Ironically, grep in that list is not the same word as the one OP is talking about. That one is actually based on grepp, with the double p. grep means pitchfork.
Another is for turning soil at a small scale by hand (also called a cultivator, I think).
But they all have somewhat long prongs.
(To go for 99%, add t for testing an archive to your repertoire. This is all I ever use; anything else I do with the relevant tools that I already know, like compression settings `tar c . | zstd -19 > my.tar.zstd` or extracting to a folder `cd /to/here && tar x ~/Downloads/ar.tar`. I'm sure tar has options for all this but that's not the one thing it should do and do well.)
I hadn't heard of the German option but I love it, shame really that z is obsolete :(
I know v is just the verbose option, though I didn't know z was autodetected.
Way back (~15y or so?) I was reading bash.org just for the jokes cause I was on IRC, I knew what a tar/tar.gz file is, but I had never needed to extract one from the command line (might've been on Windows back then). However, because I remembered the funny joke, the first time I was on a Linux system confronted with a tgz, I knew exactly what to type :)
Honestly to this day, I've never needed to create a tar archive, only to unpack them (when I need to archive+compress files it's usually to send to other people, and I pick zip cause everyone can deal with it). But `tar --help` and `man tar` are there in case I ever might.
But "begrijpelijk" has a clone: "begriplig". An adverb based on a verb in a foreign dictionary. There is no verb that goes "begreppa", it's just "greppa".
More customarily: intelligibility.
Grijpbaarheid
I never saw grep as grijp
I guess I do now
(Dutch btw)
When I was first onboarding to this project, I was tasked with updating a component and simply tried to find three of the words I saw in the UI, and this was before we implemented a straightforward path-based routing system. It took me far too long just to find what I was going to be working on, and that's the day I distinctly remember learning this lesson. I was pretty junior, but I'd later return to this code and threw it all away for a number of easily greppable strings.
As opposed to "1 objects" or "1 object(s)". A UI filled with "(s)", ughh
This is roughly the logic:
function strFromNumOfObjects(n) {
if (n === 1) {
return "obiekt";
}
let last_digit = (n%10);
let penultimate_digit = Math.trunc((n%100)/10);
if ((penultimate_digit == 0 || penultimate_digit >= 2) && last_digit > 1 && last_digit <= 4) {
return "obiekty";
}
return "obiektów";
}
Basically pluralizing words in Polish is a fizz-buzz problem :) In other Slavic languages it should be similar BTWhttps://devpal.co/icu-message-editor/?data=Zarezerwowa%C5%82...
Even if you have, say, 5 sequential related structs, that are all virtually identical, all written on one line so that the similarities and differences are obvious at a mere glance... Then someone comes through and touches my file, and while they're at it, "fix" the line that went 2 characters past the 80 mark by reformatting the 4th struct to span several lines. Now when you see that list of structs, you wonder "why is this one different?" and you have to read carefully to determine, nope, it just contained one longer string. Or god forbid the reformat all the structs to match, turning a 1-page file into 3 pages, and making it so you have to read and understand each element of each struct just to see what's going on.
If I could have written the rule of thumb, I would have said "No logic or control shall happen after the end of the gutter." But if there's a paragraph-long string on one line- who cares?? We all have a single keystroke that can toggle soft-wrap, and the odds that you're going to need to know anything about that string other than "it's a long string" are virtually nil.
Sorry. I got triggered. :-)
From Zen of Python: ``` Special cases aren't special enough to break the rules. Although practicality beats purity. ``` https://peps.python.org/pep-0020/
I'm fairly firmly in the "wrap at 80" camp by the way; but sometimes a tad longer just makes sense. Or shorter for that matter: forced removal of line breaks is just as bad.
Autoformatting collectively saves probably close to millions of work hours per year in our industry, and that’s at the current adoption. Do you think it’s productive to manually space things out, clean up missing trailing commas and what not? Machines do it better.
Autoformatters absolutely do this. They do not understand considerations like symmetry.
I am doubtful as to the costs of "somewhere in the codebase there is a missing trailing comma".
Also, autoformatters can be remarkably good. For instance, rustfmt will do things like:
x.func(Some(MyStruct {
field: big + long + expr,
field2,
}));
rather than mindlessly introducing three levels of indentation.The bad examples of arrow functions I saw initially were of:
1. Devs trying to mix them in with OOP code as a bandaid over OOP headahes (e.g. bind/this) instead of just not using OOP in the first place.
2. Devs trying to stick functional programming everywhere because they had seen a trivial example where a `.map()` made more semantic sense than a for/for-in/for-of loop. Despite the fact that for/for-in/for-of loops were easier to read for anything non-trivial and also had better performance because you had access to the `break`, `continue` and `return` keywords.
But many teams will have it as a rule to always use array fns.
let results;
try {
results = await Promise.all(vals.map(someAsyncOp))
} catch (err) {
console.error(err)
}
While you could pull that promises mapping into a variable and keep it thenable, 99% of the time I see the above instead. Promises have some rough edges because they are stateful, so I think it might be easier to recommend swapping that Promise.all for an Promise.allSettled, and using a shared utility for parsing the promise result.I consider this issue akin to the relationship between `sort`, `reverse`, `splice`, the mutating operation APIs, and their non mutating counterparts `toSorted`, `toReversed`, `toSpliced`. Promise.all is kind of the mutating version of allSettled.
> also had better performance because you had access to the `break`, `continue` and `return` keywords.
This is a great point.One more: Debugging `.map()` is also much harder than a for loop.
Should be a judgment call, and the author needs to be used to doing both looping and mapping constructs, so that they are unafraid of the bit of extra typing needed for the loop.
One reason is exactly what the subject of discussion is here, it's easier to string-search with that keyword in front of the name, but I don't need that for trivial inline functions (whenever I do I make it an actual function that I declare normally and not inline).
Then there's the different handling of "this", depending on how you write your code this may be an important reason to use an arrow function in some places.
Arrow functions are also far more concise and ergonomic when working with higher order functions or simple expressions
The main thing to be wary of with arrow functions is when they are used anonymously inline without it being clear what the function is doing at a glance. That and Error stack traces but the latter is exacerbated by there being no actual standard regarding Error.prototype.stack
To me arrow functions mostly just decrease readability and makes them blend in too much, when it should be important distinction what is a function and what is not.
My experience is that newcomers are often thrown off and confused by higher order functions. I think partly because, well let's be honest they just are more confusing than normal functions, but I think it's also because languages often bind functions differently from everything else.
`const cool = () => 5`
Makes it obvious and transparent, that `cool' is just a variable where as:
`function cool() {return 5}`
looks very different from other variable bindings.
const arrow = (a) => (b) => `${a}-${b}`
function verbose(a) {
return function (b) {
return `${a}-${b}`
}
}
function uncurried(a, b) {
return `${a}-${b}`
}
const values = ['foo', 'bar', 'baz']
values.map(arrow('qux'))
values.map(verbose('qux'))
values.map(uncurried.bind(null, 'qux'))
values.map((b) => uncurried('qux', b))code is to express logic clearly to the reader. We should assess it for that purpose, before assess for any derivative, secondary concern such as whether categories of things in code (function etc) visually pops out when you use some specific tool like vim, or grep. There are syntax highlighters for a reason. And maybe if grep sucks with code then build the proper tool for code searching, instead of writing code after the tool.
But it really pains me when I see
export const foo = () => {}
instead of
export function foo() {}
But everywhere else they reduce readability of the code with no tangible benefit I am aware of.
One that could enforce these styles. Because not only is the export const foo = () {}
painful on itself, it will quite certainly get intermixed with the
function foo() {}
and then in the next library a
const foo = function() {}
and so on. I'd rather have a consistently irritating style, than this willy-nilly yolo style that the JS community seems to embrace.
[1] https://eslint.org/docs/latest/rules/func-style
[2] https://eslint.org/docs/latest/rules/prefer-arrow-callback
It's not opinionated, but requiring you to form your own opinion or at least choose from a palette of opinions.
It requires effort to opt-in rather than effort to opt-out.
The community doesn't frown on code that's not adhering to the common standard or code that doesn't pass the "out of the box" linter.
So, if I have a typescript project with a tree of some 20 dependencies (which is, unfortunately, a tiny project), I'll have at least five styles of code when it browse through it. Some JS, some TS, some strictly linted with "no-bikeshedding", some linted with configs that are bigger than the codebase itself. Some linted with outdated. Many not linted at all. It's really a mess. Even if each of the 20 dependencies themselves are clean, beauties, the whole is an inconsistent mess.
> Some JS, some TS
I think the JS community has done remarkably well amongst dynamically typed languages in settling on one form of gradual typing and adopting it fervently (Flow no longer has any market share at all). Whereas the last time I checked Python still had the Mypy/Pywright divide, and Ruby had the Sorbet/RBS dichotomy.
Ultimately though, most of your critique boils down to the fact that JS (unlike Rust and Go) isn't maintained by a single monolithic entity, and therefore there's no one to dictate the standards you're looking for. If Deno were the sole caretaker of JS for example, we'd have a standard linter and formatter devoid of complex configuration, but Deno doesn't control JS.
This is a consequence of JS being a collaborative product of the various browser vendors, TC39, and the server side JS runtimes that have adapted JS to run on servers. The advantage of this of course though is that JS can run natively in the browser. I think that's a decent tradeoff to make in exchange for having to wade through dependencies with different ideas about when it's appropriate to use arrow functions.
myArray.sort(function(a,b){return a-b})
People for some reason treat this syntactic sugar like it gives them some new fundamental ability.`function(a,b){return a-b;}` is different from `(a,b) => a - b`
And `function diff(a,b) {return a-b;}` is different from `const diff(a,b) => a - b;`.
Really, all you need is `<keyword>` and if the first result is a call to that function, just jump to its definition.
Just search the definition.
Any time that a function doesn't have a definition, it's never the target of a search anyway.
It's 2024 and HN still suggests using regular expressions to search through a code base.
Your special tool might not work on plattform X, fails for edge case - and you generally don't know how it works. With regex or simple string search - I am in control. And can understand why results show up, or investigate when they don't, but should.
As always, people come out with the weirdest of excuses to not use actual tools in the 99.9999% of the cases when they are available, and work.
When that tools doesn't work, or isn't sufficient, use another one like fuzzy text search or regexps.
> and you generally don't know how it works.
Do you know how your stove works? Or do you truly understand what the device you're typing this comment on truly works?
Only in programming I see people deliberately avoid useful tools because <some fringe edge case that comes up once in a millenium in their daily work>
But I prefer tools, that I can use wherever I go. To not be dependant and chained to that environment.
"Do you know how your stove works? Or do you truly understand what the device you're typing this comment on truly works?"
Also yes, I do.
" people deliberately avoid useful tools because <some fringe edge case that comes up once in a millenium in their daily work>"
Well, or I did already changed tools often enough, to be fed up with it and rather invest in tech that does not loose its value in the next iteration of the innovation cycle.
I specialize in one thing only: programming
> But I prefer tools, that I can use wherever I go.
Do you always walk everywhere, or do you use a tool available at the time, like cars, planes, bycicles, public transport?
> rather invest in tech that does not loose its value in the next iteration of the innovation cycle.
Things like "fund symbol", "find usages", "find implementation" have been available in actual tools for close to two decades now.
With search you end up grepping the code twice:
- first grepping for the name
We're literally in a thread where people invent regexes for how to search the same thing (a function) defined in two different ways (as a function or as a const)
- secondly, manually grepping through search results deducing if it's relevant to what you're looking for
It becomes significantly worse if you want to include third-party libs in your search.
There are countless times when I would just Cmd+B/Cmd+Click a symbol in IDEA and continue my exploration down to Java's own libraries. There are next to zero cases when IDEA would fail to recognise a function and find its usages if it was defined as a const, not as a function. Why would I willingly deny myself these tools as so many in this thread do?
gr<bs><bs>ion name<cr>
vs
grname<cr>
or for the current identifier, simply gr<m-w><cr>
I could even make my own useful tools like “\[fvm]gr” for function, variable or field search and brag about it watching miserable ide guys from the high balcony, but ain’t that unnecessary as well.And then you proceed to... invent several pale imitations of a symbol/usages search.
More here: https://news.ycombinator.com/item?id=41435862 so as not to repeat myself
I'm mostly ranting against this weird "we will never use great tools because full-text search" obsession
And yet people are obsessed with never using useful tools in the first place because they can invent scenarios when this tool doesn't work. Even if these scenarios might never actually come up in their daily work.
By using regexps I have an experience that opens many doors, and the fact that they aren’t automatic could make me sad, if only these doors weren’t completely shut without that experience.
And you somehow manage to undersell the rename functionality in an IDE. And I've used move/extract functionality multiple times.
I do however agree that applicable transformations (like upgrading to new syntaxes, or ways of doing stuff as languages evolve) could be applied wholesale to large chunks of code.
I was very careful about how I phrased my comment. Languages can gain points by being greppable, and they can gain points by having a well–implemented mode for my favorite editor, and they can also gain points by having a well–implemented language server. They can do all three to gain the most points (and of course there are many other ways to gain or lose points; this is just one tiny aspect of language design), but they don’t have to. And nobody should try to force every language designer to shave the yak of writing an Emacs mode or a Language Server.
And sometimes you just want to improve some random piece of software written in a language that you’ve never used before, without having to shave a bunch of yaks to get the language server installed. Sometimes all you have is grep and you’ll want to be able to use it on this weird new language.
You can search for both: "function" and "=>" to find all function expressions and arrow function expressions.
All named functions are easily searchable.
All anonymous functions are throw away functions that are only called in one place so you don't need to search for them in the first place.
As soon as an anonymous function becomes important enough to receive a label (i.e. assigning it to a variable, being assigned to a parameter, converting to function expression), it has also become searchable by that label too.
Are you searching through every function, or functions that have a very specific parameter?
And whatever you picked, why?
---------------------------------------------------------------
- If you're searching for every function, then there's no need to search for foo.=>, you only need to search for function and =>.
- If you're searching for a specific parameter, then just search for the parameter. Searching for functions is redundant.
---------------------------------------------------------------
Arrow function expressions and function expressions can both be named or anonymous.
Introducing arrow functions didn't suddenly make JavaScript unsearchable.
JavaScript supported anonymous functions before arrow function expressions were introduced.
Anonymous functions can only ever be:
- run on the spot
- thrown away
- or passed around after they've been given a label
Which means, whenever you actually want to search for something, it's going to be labelled.
So search for the label.
Other than that, functions should be defined by the keyword.
> Other than that, functions should be defined by the keyword.
Says who?
Serious arguments would be:
- readability
- greppability
const foo = () => {}
This function is not anonymous, it's called foo.function foo(){} is also callable if bar is defined before foo.
const foo = function (){}
without its own name before (). These behave like expressions and cannot be hoisted.I haven't figured out if people consider this a best practice, but I love doing it. To me the list of called functions is a high-level explanation of the code, and listing all the definitions first just buries the high-level logic "below the fold". Immediately diving into function contents outside of their broader context is confusing to me.
Hoisting also enables cross-imports without helper unit extraction headaches. Many hate js/ts at the “kids hate == and null” level but in reality these languages have a very practical design that wins so many rounds irl.
const foo = () => {}
console.log( foo.name );
actually outputs 'foo', and not the empty string that I was expecting. const test = () => ( () => {} );
const foo = test();
console.log( foo.name );
outputs the empty string.Is this behavior required by the standard ?
var foo = function() {};
Except nowadays it too does have the name "foo".And always call the function as `funcname(args)`
So definitions have a space between the name and arg parentheses, while calls do not. Seemed to work well, even in languages with extraneous keywords before definitions since space + paren is shorter than most keywords.
Now days I don’t bother since it really isn’t that useful especially with tags or LSP.
I still put the return type on a line of its own, not for search/grep, but because it is cleaner and looks nice to me—overly long lines are the ugliest of coding IMO. Well that and excessive nesting.
However, that's just convention. Lots of modules do metaprogramming tricks that obscure greppability, which can be a pain. This is particularly acute when searching for code that is "import-time polymorphic"--that is, code which picks one of several implementations for a piece of functionality at import time at the module scope. That frequently ends up with some hanky-panky a la "exported_function_name = _implementation1 if platform_supported else _implementation2" at the module scope.
While sometimes annoying, that type of thing is usually done for understandable reasons (picking an optimized/platform-supported implementation of an interface--think select or selectors in the stdlib, or any pypi implementation of filesystem monitoring using fsnotify/fanotify/kqueue/fsevents/ReadDirectoryChangesW). Additionally, good type annotations help with greppability, though they can't fully mitigate this issue.
Much less defensible in Python is code that abuses locals/globals to indirect symbol access, or code that abuses star imports to provide interfaces/implementation switching.
Those, fortunately, are rare, but the elephant in the "no greppability ever" room is not: getattr bullshit in OO code is so often utterly obscure, unnecessary and terrible. And it's distressingly common on PyPi. At first I thought this was Ruby's encouragement of method_missing in the bad old days bleeding into the Python community, but the number of programmers for whom getattr magic is catnip seems to be disproportionate to the number of folks with Ruby experience, and, more concerningly, seems to me to be growing over time.
... the language has been designed to be easy to analyze and can be parsed without a symbol table
Taken from https://go.dev/doc/faqThe "top-level declarations" in source files are exactly: package, import, const, var, type, func. Nothing else. If you're searching for a function, it's always going to start with "func", even if it's an anonymous function. Searching for methods implemented by a struct similarly only needs one to know the "func" keyword and the name of the struct.
Coming from a background of mostly Clojure, Common Lisp, and TypeScript, the "greppability" of Go code is by far the best I have seen.
Of course, in any language, Go included, it's always better to rely on static analysis tools (like the IDE or LSP server) to find references, definitions, etc. But when searching code of some open source library, I always resort to ripgrep rather than setting up a development environment, unless I found something that I want to patch (which in case I set up the devlopment environment and rely on LSP instead of grep to discover definitions and references).
func (s *Recv) foo(fn func(x any) err) func bar(y any) (*Recv, err)
As an exaggerated example. Easy to parse but not always easy to read at a glance.An incremental grep tool with just this one transformation rule gets you a lot more mileage out of grep.
[1] https://github.com/minad/consult/blob/screenshots/consult-li...
EDIT: Better demo https://jumpshare.com/s/zMENBSr2LwwauJVjo1wS
It's not far off from my manually-constructed patterns when I want to make sure I find a function definition (and am willing to tolerate some false positives), but I personally prefer fine-grained control over when it's in use.
For methods: grep -P '^func [^)]+\) methodName\('
Hope that helps.
grep -P '^func [^)]+\) methodName\('
you could say grep 'func [^)]*) methodName('
which is a bit less typinghowever, i have to admit that i sort of ensnared myself in my own noose here by being too clever! i forgot that grep's regexp dialect only supports + if you \ it, and it took me six tries to figure out why i wasn't getting any grep hits. there's a lot to be said for the predictability and consistency of pcre!
grep '\) methodName\(' grep: Unmatched ) or \)
but your main point might be right; the few non-method matches to (pcre) '\)\s*\w+\s*\(' in /usr/share/go-1.19 seem to be uncommon things like this: static void __attribute__ ((constructor)) sigsetup(void) {
void poison() __attribute__ ((weak));
C3 = -(R + I) // ADD(5,6) NEG(-5,-6)If you don't want OOP in the language, but want people to be able to write thing.function(arg), you just make function(thing, arg) and thing.function(arg) equivalent syntax.
Equivalent means that there is no difference at the AST level between o.f(a) and f(o, a), like there is no difference in C among (a + i), a[i], i[a] and (i + a).
However, a this keyword is way better than making the programmers fraction off a parameter and move it to the other side of the function name.
For functions: grep -P '^func funcName\('
For methods: grep -P '^func [^)]+\) methodName\('
grep func | grep functionName
If I see a loop with i or k, v then I can be fairly confident that those are an Index or a Key Value pair. Also I probably don't need to grep them since everything interacting with these variables is probably already on my screen.
Everything that has a wider scope or which would be unclear with a single letter is named with a more descriptive name.
Of course this is highly dependent on the people you work with, but this is the way it works on projects I have worked on.
The convention, not just in Go, is that the smaller the scope, the smaller the variable reference.
So, sure, you're going to see single-letter variables in short functions, inside short block scopes, etc, but that is true of almost any language.
I haven't seen single-letter variables in Go that are in a scope that isn't short.
Of course, this could just mean that I haven't seen enough of other peoples Go source.
You'd be surprised how often language-local cultures break that rule on either side. And a few times it's even an improvement.
Of course, with enough code, someone does everything.
The code bases you've been reading, and even some of the native libraries, don't do it properly. Probably due to legacy reasons that wouldn't pass readability approvals nowadays.
> A piece of Go source code should avoid unnecessary repetition. One common source of this is repetitive names, which often include unnecessary words or repeat their context or type. Code itself can also be unnecessarily repetitive if the same or a similar code segment appears multiple times in close proximity.
https://google.github.io/styleguide/go/decisions#repetitive-...
(see also https://google.github.io/styleguide/go/best-practices#avoid-...)
This is the style rule that motivates the sibling comment about method names being split between method and receiver, for what it's worth.
I don't think this use case has received much attention internally, since it's fairly rare at Google to use grep directly to navigate code. As you suggest, it's much more common to either use your IDE with LSP integration, or Code Search (which you can get a sense of via Chromium's public repository, e.g. https://source.chromium.org/search?q=v8&sq=&ss=chromium%2Fch...).
If you want to search for `url.Parse`, you can find most of the usages just by searching for `url.Parse`, because the package will generally be imported as `url` (and you won’t import Parse into your namespace).
It’s not as good as find references via LSP but it is like 99% accurate and works with just grep.
Lastly, one of the bigger issues is (as aforementioned sibling commenter mentioned) the application of this principle to methods. This is especially bad with method names for established interfaces, like Write or String. Even with a LSP server, you then need to trace up the call stack to figure out what concrete types the function is being called with, then look at the definitions of those types. I can't imagine wanting to do that with only (rip)grep at my disposal.
if err != nil {
return nil, err
}
after every second statement are advising against code repetition.To put it in a way you may understand, it's when you have to press keys in the same order a lot of times and you get sad.
Time spent by authors typing source code characters into their editor is almost entirely irrelevant. The only thing that matters is the time spent by readers parsing and understanding the source code in the VCS.
Regardless, any reasonable LSP will let you "Find implementations" of any interface definition.
on edit: I see someone discussed that you can grep for both arrow functions and named function at the same time and I suppose you can also construct a query that handles a function constructor as well - but this does not really handle curried functions or similar patterns - I guess at that point one is letting the perfect become the enemy of the good.
Most people grepping know the code base and the patterns in use, so they probably only need to grep for one type of function declaration.
Most of us use exuberant ctags to allow jumping to definitions.
To me, that's a much common and worse practice with regards to greppability than splitting identifiers using string which I haven't seen much in the wild.
Even for Lisp, you don't want to be grepping, or at least not all the time for basic things.
For TXR Lisp, I provide a program that will scan code and build (or add to) your tags file (either a Vim or Emacs compatible one).
Given
(defstruct point ()
x
y)
it will let your editor jump to the definition of point, x and y.That’s not really C; that’s a C-based DSL. The same problem exists with Lisp, except even worse, since its preprocessor is much more powerful, and hence encourages DSL-creation much more than C does. But in fact, it can happen with any language - even if a language lacks any built-in processor or macro facility, you can always build a custom one, or use a general purpose macro processor such as M4.
If you are creating a DSL, you need to create custom tooling to go along with it - ideal scenario, your tools are so customisable that supporting a DSL is more about configuration than coding something from scratch.
C's is very weak. Languages with more powerful preprocessors/macros than C's include many Lisp dialects, Rust, and PL/I. If you think everyone using a weak preprocessor is bad, wait until you see what people will do when you give them a powerful one.
Microfocus COBOL has an API for writing custom COBOL preprocessors in COBOL (the Integrated Preprocessor Interface). (Or some other language, if you insist.) I bet there are some bizarre abominations hidden in the bowels of various enterprises based on that ("our business doesn't just run on COBOL, it runs on our own custom dialect of COBOL!")
i can't say i think they were wholly wrong; paging through compiler error messages is not my favorite part of c++ templates. but i have a certain amount of affection for what used to be called gasp, the gas macro system, which i've programmed for example to compute jump offsets for compiling a custom bytecode. and i think m4 is really a pathological case; most hairy macro systems aren't even 10% as bad as m4, due to a combination of several tempting but wrong design decisions. lots of trauma resulted
so when they got a do-over they eliminated the preprocessor entirely in golang, and compensated with reflection, which makes debugging easier rather than harder
probably old hat to you, but i just learned last month how to use x-macros in the c preprocessor to automatically generate serialization and deserialization code for record types (speaking of cobol): http://canonical.org/~kragen/sw/dev3/binmsg_cpp.c (aha, i see you're linking to a page that documents it)
See for example https://github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,...
Poor C preprocessor performance has a negative real world impact, for example recently with the Linux kernel – https://lwn.net/Articles/983965/ – a more powerful preprocessor would enable people to do those things they are doing anyway much more cheaply
I like Rust (tho I have not yet programmed in it) but I think if people get too into macro generated code, there is a risk there to its uptake.
It's hard for smart programmers to really believe this, but the old "if you write your code as cleverly as possible, you will not be able to debug it" is a useful warning.
Not so with DEFINE_FUNCTION(foo) {, I think.
$ cat > foo.lisp
(define-musical-scale g)
$ ctags foo.lisp
$ grep scale tags
g foo.lisp /^(define-musical-scale g)$/;" f
Exuberant Ctags is not even a tool from the Lisp culture. I suspect it is mostly shunned by Lisp programmers. Except maybe for the Emacs one, which is different. (Same ctags command name, completely different software and tag file format.)Working with legacy code — the scenario the author describes — I often can’t install anything on the server.
It's a hassle. But not the end of the world.
I usually search for "doTheThing\(.+?\) \{" first.
If I don't get a hit, or too many hits I move to "doTheThing\([^\)]*?\) \{" and so on.
I don't care which case is used. It's a trivial superficial thing, and tribal zealotry about such doesn't reflect well on the language and community.
[1] The warnings can be turned off, but in some cases it requires ugly hacks, and the community seems to be actively hostile to making it easier)
Yes, it can be turned off. But for e.g. bindgen generated code it was not trivial to find out.
That’s why in my personal projects I follow classic “type\nname” and grep with “^name\>”.
looks ugly
Single line definitions with long, irregular type names and unaligned function names look ugly. Col 1 names are not only greppable but skimmable. I can speedscroll through code and still see where I am.
This is why in C projects libs go in "lib/" and sources go in "src/". If your header files have the same directory structure as libs, then "include/" is a also a decent way to find definitions.
...use source code tagging or LSP.
Many people insist that IDEs make the entire point moot, but that's the kind of thing that make IDEs easier to write and debug, so I disagree.
if (x == 0) { ...
sizeof (buf);
return (-1);
exit(0);int
foo(void) { }
vs the Linux coding style:
int foo(void) { }
The BSD style allows me to find function definitions using git grep ^foo.
I think the benefit of having one symbol exist in only one domain (e.g. “user_request” only showing up in the database-handling code, where it’s used 3 times, and not in the UI code, where it might’ve been used 30 times) reduces more cognitive load than is added by searching for 2 symbols instead of 1 common one.
Additionally, I find that in practice such "unusual" code is actually beneficial - it often makes it easy to see at a glance that the code is somehow in sync with some external spec. Especially when it comes to implicit usages such as in (de)serialization, noticing that quickly is quite valuable.
I'd much rather trash every languages' coding conventions than use subtly different names for objects serialized and shared across languages. It's just a pain.
The upside to doing it this way is it makes your grepping more flexible by allowing you to either only search the one part of the codebase to see say DB code or see all the DB and UI things using the concept.
rg -i ‘foo.?bar’ finds all of foo_bar, fooBar, and FooBar.
Maybe there’s an alternative way to achieve what the author set out but increasing searchability at the cost of increasing brittleness isn’t it for me.
In this example:
const getTableName = (addressType: 'shipping' | 'billing') => { return `${addressType}_addresses` }
The input string and output are coupled. If you add string conditionals as the author did, you introduce the chance of a mismatch between the input and output.
const getTableName = (addressType: 'shipping' | 'billing') => { if (addressType === 'shipping') { return 'shipping_addresses' } if (addressType === 'billing') { return 'billing_addresses' } throw new TypeError('addressType must be billing or shipping') }
Similarly, flattening dictionaries for readability introduces the chance of a random typo making our lives hell. A single typo in the repetitions below will be awful.
{ "auth.login.title": "Login", "auth.login.emailLabel": "Email", "auth.login.passwordLabel": "Password", "auth.register.title": "Login", "auth.register.emailLabel": "Email", "auth.register.passwordLabel": "Password", }
Typos aren’t unlikely. In a codebase I work with, we have a perpetually open ticket about how ARTISTS is mistyped as ATRISTS in a similarly flat enum.
The issue can’t be solved easily because the enum is now copied across several codebases. But the ticket has a counter for the number of developers that independently discovered the bug and it’s in the mid two digits.
I don't think coupling variable names by making sure they contain the same strings is the best way to show they're related, compared to an actual map from address type to table name. There might be a lot of things called 'shipping' in my app, only some of which are coupled to `shipping_addresses`.
Shouldn't a linter be able to catch that there is no enum member called MyEnum.ATRISTS, or is it not an actual enum?
What happens when translation files get too big and you want to split and send only relevant parts? Like send only auth keys when user is unauthenticated?
`return translations[auth][login]` is no longer possible.
Or just imagine you want to iterate through `auth` keys. _shudders_
I certainly would not suggest deliberately mistyping, but there are places where the benefit is approaching the cost. Certain log messages can absolutely benefit from subtle letter garbling that retains readability while adding uniqueness.
I think it depends on whether the repetition is accidental or intrinsic. Does the table name happen to contain the address type as a prefix, or does it intrinsically have to? Greppability aside, when things are incidentally related, it's often better to repeat yourself to not give the wrong impression that they're intrinsically related. Conversely, if they are intrinsically related (i.e. it's an invariant of the system that the table name starts with the address type as a prefix) then it's better for the code to align with that.
I've used this as an organizing principle since forever (https://news.ycombinator.com/item?id=1535916). It's one of the best ways to factor code that I know of.
ubyte c = cast(ubyte)i;
instead of: unsigned char c = (unsigned char)i;
Casts are a blunt instrument that subvert the type system, and so they need to be greppable.Having the cast keyword also removes the grammatical ambiguities in the expression syntax.
I don't normally grep for them, but others have told me they did.
P.S. one thing about D is you can do things like this:
ubyte b = i; // error, losing bits
ubyte b = cast(ubyte)i; // ugly cast
ubyte b = i & 0xFF; // no cast, no error!
It's just one of the nice little details that making programming in D a pleasure.Nobody should keep a checklist of "100 things to grep for when doing a code review".
Also the reason why I try to avoid Gradle when possible:
The possibilities are endless. At one place I think I found 21 wildly different Gradle configs out of 24 that I checked.
(For anyone that wonders, it was combinations of:
- placeholders vs straightforward depency (this is a thing in maven too)
- for loops doing things based on lists or maps instead of just calmly declaring them one after another, maybe to save some characters
- helper functions so you could declare dependencies like azure(<something>(<version>))
- order of declarations
- Kotlin vs Groovy syntax
I have probably forgotten a couple more but this is thankfully already a few years ago.)
That said, I completely agree with the author on not using clever string tricks to compose identifiers. That makes code both harder to search and to read.
However, changing how the code should be written so that grepping becomes easier is optimizing for the wrong target. It is much more important that the code is easily readable and maintainable.
In addition, some tools are designed explicitly for grepping through code (from the top of my head ack is an example). If grep doesn't work, one should try a more sophisticated tool instead of using different coding styles.
I understood “grepping” to mean ripgrep (rg) or ack, not just plain grep. I think programmers who use command line tools or vim know about those. VSCode uses rg.
const getTableName = (addressType: 'shipping' | 'billing') => {
return `${addressType}_addresses`
}
This is a simplified example but in a longer function, readability of the `return` lines would be improved as the reader wouldn't have to reference the union type (which may or may not be defined in the signature). The rewrite is also safer as it errors out if a runtime `addressType` value doesn't match the union type (above code would not throw an error, just return an indeterminate value which would cause undefined behaviour)."Flat is better than nested" also greatly improves readability in both examples: either reading the i18n line, or reading the classname at definition / call will be more readable when the name contains full context of function.
https://nim-lang.org/docs/manual.html#partial-caseminusinsen...
That's more of a "deranged developer" problem than a problem with the guidelines themselves. E.g. I think his `getTableName` example is quite sensible, but also one which some dogmatic engineers would flag and code-golf down to the one-liner.
You don't need to search for local variables, nobody names global variables "i" - so the "ii" advice is pointless.
You often do need to search for places where global stuff is referenced, and while IDEs can help with that - the same things that break grepability often break "find references" in IDE. For example if you dynamically construct function names to call, play with reflections, preproccessor, macros, etc.
So it's a good advice to avoid these things.
> you will encounter deranged developers that become obsessive about such things and make the code base ugly
You can abuse any rule, including
> Code should look good.
and I'd argue the more general a rule is - the more likely it is to be abused. So I prefer specific rules like "don't construct identifiers dynamically" to general "be good" rules.
I've never heard of that advice. I honestly like algebraic names (singular digits) as long as they're well documented in a comment or aliasing another longer-name.
> there are 'YODA Conditions' which make code hard to comprehend which solves an insignificant error that is easily caught with tooling
Yoda conditions [0] are a useful defensive programming technique and does not reduce readability except to someone new to it. I argue it improves readability, particularly for myself.
As for tooling... it doesn't catch every case for every language.
> I loath such advice because it causes the code to become ugly.
Beauty is in the eye of the beholder. While I appreciate your opinion, I also reject it out of hand for professional developers. Instead of deciding whether code is "ugly" perhaps you should decide whether the code is useful. Feel free to keep your pretty code in your personal projects (and show them off so you can highlight how your style really comes together for that one really cool thing you're doing).
> you will encounter deranged developers that become obsessive about such things
I don't like being called deranged but I am definitely obsessed about eliminated whole classes of bugs just by the coding design and style not allowing them to happen. If safe code is "ugly" to you... well then I consider myself to be a better developer than you. I'd rather have ugly code that's easily testable instead of pretty code that's difficult to test in isolation which most developers end up writing.
> Code should look good. Making a piece of text look good for other humans to comprehend I consider to be job #1 or #2 for a good developer.
It depends on the project. Just remember that what looks good to you isn't what looks good to me. So if it's your personal project, then make it look good! If it's something we're both working on... then expect to defend your stylistic choices with numbers and logic instead of arguments about "pretty".
Then, from the article:
> Flat is better than nested
If I'm searching for something in JSON I'm going to use jq [1] instead of grep. Use the right tools for the right job after all. I definitely prefer much richer structured data instead of a flat list of key-value pairs.
\bi\b is the easy way to search for i.
Vim users don't have this issue, where you can * a variable name you're looking for and that'll enforce word boundaries.
PHP developers also don't have this issue: the $ before is a pretty sure indicator you're working with a variable named i and not a random i in a word or comment somewhere
Y'all should just use proper tools. No newfangled Rust and Netbeans that the kids love these days, the 80s had it all!
(Note this is all in jest :) I do use vim and php but it's obviously not a reason to use a certain language or editor; I just wondered to myself why I don't have this problem and realised there's two reasons.)
// #learning: transparent color using color.new(color.white, 100). This is GREAT for hiding plot() lines during inapplicable periods (such as when no trade is on)
But project-specific hashtags are quite useful, too.
// #60within600: bunch API calls to not hit the 60 calls within 10 minutes limit
// This memoizes fn call results to prevent #60within600
The hashtagging was inspired long ago by del.icio.us, if you remember that. https://en.wikipedia.org/wiki/Delicious_(website)
If you need to prevent 60 within 600, write a test.
It's not as easy to use as grep but I think one can script it to be nearly so. It has huge power but without learning it all one can do searches that grep finds difficult. e.g. finding all the locations where a method is called and showing the parameters even if they are on multiple lines. You can then use rewrite rules to do CLI code refactoring.
I think it also has potential in a build toolchain e.g. to look for patterns you want to discourage as a pre-commit hook.
ultragrep - https://github.com/zendesk/ultragrep - I don't love this quite as much but it does have a way to build indexes so you can do fast greps across a big codebase. It also has a text mode UI if you want it and I find that almost worthwhile.
I use ripgrep most of the time but while I like it, there is a limit to how many grep tools I can remember and I should probably cut down to using ultragrep and astgrep.
plain gnu grep itself is something one has to know when one is on an unfamiliar machine.
I've left hardcoded strings (think Kafka event type names) in my source for this very reason, but after a round of code review they get squirreled away as constants in separate files because string repetition is bad or something.
If I have three classes that interact with "MyTable", then I can grep for places that interact with "MyTable" and I get back three classes.
After refactoring, the class which now knows about "MyTable" is Constants.java, which has no business knowing about "MyTable". Grepping it now turns up a false-positive and finds 0 of the actual usage sites (3 false-negatives).
The file name is awful.
At worst it should be 'DbConstants' but probably they should be defined elsewhere.
It's extremely valuable to be able to just `grep -r PREFIX_` on a codebase and be able to visualize all possible configuration values for that application.
This is encouraged by some frameworks like Django, where you are expected to list all the configuration values in a `settings` module, but is not standard for `viper`, `click` and `pydantic-settings`, which try to be too smart and auto-generate the variable names for you. It's one of these cases where "modern" frameworks and applications try to save a minuscule amount of work by automating some task, but end up reducing the maintainability of the code over time.
I'm an advocate for the idea that any aspect of a system which communicates either with end users or with sysadmins should be given high exposure in the code base. Typically, this means constructing abstractions in such a way that higher-level business logic and log messages are easily traceable from a single file. I make it so that the business layer sits above all other layers, as close to the program's entry point as possible.
There are exceptions of course: when searching git logs, comments etc doesn't help what the language or IDE does.
And when searching for an unknown symbol (type, function, variable) you don't know the name of, but you know _should_ look like "DogOrder" or "OrderDog" is a common task too. In this case I'd probably search for " Dog.Order\(" or " Order.Dog\(" if I'm looking for a function. The language trait that enabled it is that method names are Pascal Case and always have an opening ( at the end. But my IDE at least lets me search for members (variables, functions) separate from type names. There should be an index in the IDE though that lets you query this data. E.g. looking for types starting with foo could be done with search t:Foo, instead of having to grep for "(struct|class) Foo" or similar. Tooling is the key.
In static languages, greppability is hardly as much as a factor. Especially with the availability of LSPs and other such tools nowadays.
When I write rust, or Java, I hardly grep, I "go to usages" or "go to definition", "rename symbol" and so on. Similar, but not to that extent, with typescript. But when coding in Javascript, Ruby or Python, no matter how fancy or language-focused an IDE is, I'll be grepping a lot. Decades of Ruby and Rails "black magic" taught me to grep for partial patterns like the author shows, too. Or to just run the code-path entirely (through tests) because the table-definition of the database will change the available methods and behaviour of the code. Yes. I know.
An LSP (or linter, or checker) can only do so much when the available code, methods, classes, behaviour can be changed or added at runtime.
Plain old putting-dependencies-in-the-constructor-instead-of-newing-them is great.
If you 'wire' it yourself, you see the top-level structure of the project in main, e.g.
cache <- createCache "./cache"
workQueue <- createWorkQueue parallelism
projectFinder <- createProjectFinder basePath
gradleBuilder <- createGradleBuilder cache
normaliser <- createNormaliser
gradleParser <- createGradleParser normaliser
relationFinder <- createRelationFinder cache normaliser
At a glance I can see what uses normaliser, and what is used by normaliser.Right now, code search is basically just text search. If you think code search tools “could and should” be a lot better, what kind of improvements are you thinking about? How would those improvements work?
1. The popular "Find Usages" which varies widely in accuracy and reliability by language, IDE, and codebase meta-quirks.
2. Tools that show Callee/Caller trees, and sometimes possible data-flows between variables.
3. DSLs to search hierarchies, like how XPath lets you find XML elements based on nesting, rather than relying on a distinctly greppable single tag-name for the leaf you're interested in. (e.g. `<Product><Name>` vs `<ProductName>`)
When things go well, the actual variable name no longer needs to restate certain aspects and relationships that can instead be found through metadata.
For example, `GiftCard.purchaser_customer_uuid` is nicely greppable, but you could relax that to `GiftCard.purchaser` if it had a static type of `UUID<Customer>`. Or perhaps you could go to the `Customer.uuid` definition and say "Show me all variables that can populate or be-populated-by this one, up to X steps out, and excluding ones that are function scoped."
That said, I do advocate for "greppability" as a general practice, since I seldom trust that languages, tools, or institutions will come together in a way that makes it unnecessary.
The solution—to write code which is less dynamic—helps code search and features like find usages.
It allows you to grep inside source code, but limit the search to e.g. “only docstrings inside class definitions”, among other things. That is, it allows nesting and is syntax aware. That example is for Python, but the tool speaks more languages (thanks to treesitter).
https://github.com/alexpovel/srgn/blob/main/README.md#multip...
We have lots of code search that is much more syntax-aware than just text search, but it tends to be behind very limited UI, because we have all the tech to do much better code search, but no one has come up with a generally-usable UI for it, so we just have very specific instances -- like "go to definition", "find references" , etc.
That takes all the same technological bits that would be need for, say, "find all definitions of functions visible in the current scope whose name starts with 'ban'" or "find all definitions of int8 constants visible in the current scope"...but what's the UI that makes that kind of searching outside of the kind of special cases now behind their own IDE menu items usable?
$ helm get values -n $NS $DEPLOYMENT -o json | gron | grep resources | gron -u | json-to-yaml.py
elasticsearch:
client:
resources:
limits:
cpu: 3
memory: 4Gi
requests:
cpu: 1
memory: 2Gi
data:
resources:
limits:
cpu: 6
memory: 6Gi
requests:
cpu: 200m
memory: 2Gi
fluentd:
resources:
limits:
memory: 768Mi
requests:
memory: 384Mi
That snip could be provided to another team or a customer as a yaml file that could be included with `helm upgrade -f whatever.yaml`. This is soooo much easier than digging that limited set of data out of the much more detailed data.This is self-inflicted.
Grepping at at runtime, if you can call it that, is also very powerful. If you have a binary, either your company or a third party one, but don't have the source code easily available, I have used the `strings` program from GNU binutils which shows tokens in binary code, e.g. hardcoded URLs, credentials and so on. It can also be useful for analyzing certain things in memory.
However, searchable =/= greppable.
> Flat is better than nested
Context matters but, generally speaking, I would say that flatter is anti-grep.
Grep is a simple tool, not too different from a simple string sort. It is better than no tool, but is it better than a tool that understands the notation? A strong side of grep is that it is universal and is not tied to a particular notation. Yet if you could easily define a specific notation and have a tool to immediately understand it, would you still prefer grep?
We tend to organize the code according to the tools we have. E.g. if a tool gives us a list of entities in alphabetic order, we will try to name the entities so that they form “logical” groups. This may pass as a local organizational principle and may be useful but it is always intimately coupled with the underlying tool.
Greppability is also helpful when you start scripting your editor. Vim has `includeexpr` and co. to implement some "intelligence" when trying to find declarations etc. This enabled me to write a couple line snippet that immediately could resolve Bazel starlark symbols even in "imported" (`load()`) files. At one point I realized I have better code navigation than any of my colleagues using IDEs.
This, and tools like ripgrep really help a lot. This is something that VS Code developers also realized when indlcluded ripgrep itself as their "backend" of searching in files.
- I don't always know the return code type, as the calling code assigned a field whose definition I don't know to find either
- I don't know if it's a C function or a preprocessor macro
This often results in me searching for the exact function name, and combing through the uses in the drivers. You then need to re-start all that recursively to understand the function you just read.
I could use clangd for that, but I don't have the ressources on my laptop to compile a kernel
Also, the example completely glosses over the egregious use of magic constants. Wouldn't it be better to declare addressType an enum?
The example for "Use the same names for things across the stack" only works if you control the whole stack. Sometimes you have to get fetch data from a 3rd party. And even if you do own the whole stack, sometimes it's nice to be able to search for 'streetName' when you're looking for the JS symbol, and 'street_name' when you're looking for the DB column. So I don't think this rule is always beneficial.
At the end of the day, if you really want to make your code grepable, add some verbose comments that include useful keywords. (I've found it useful to include keywords that aren't even in the code but which devs may search for.) This lets you grep regardless of how the code is written or formatted.
It's what I like about Rails when it comes to file names too. Having controllers/users_controller.rb as a path might sound wasteful because "you're already in the controllers directory, you don't need _controllers in the path".
But when you want to fuzzy find that file, it's really nice to type "users con" and get that file instead of also picking up views, models and other user related files with just a "users" search.
:label="$translate(getProductSectionLabel('title'))"
where the logic is a bit like: const getProductSectionLabel = (code) => `myapp.sales.sections.products.${code}`
and then the actual values are in a nested structure, like: myapp: {
sales: {
sections: {
products: {
title: "Products"
...
}
...
}
...
}
...
}
People seem to have gone for that because writing that first part is simpler within the component, but I couldn't get across that this makes the codebase harder to navigate.Meanwhile, my personal codebases are more like:
:label"$translate('myapp-sales-products-title')"
and the translation file also has the equivalent of: myapp-sales-products-title: "Products"
which is way simpler at the expense of some more duplication (easily mitigated by compressing the translations).One trade-off you can make is using text-based serialization so you're at least able to grep the yaml or JSON or whatever and get to the right file at least. This of course costs you some editor load time.
On the flip side you're basically always using an IDE to edit the visual script. In theory symantic search should be possible and built in although reality usually falls short.
Someone in a previous HN thread mentioned the idea of a standard graph syntax. Something that game engines and tools could store their graph-based assets in. If there was a standard syntax then standard tools could be made and we could end up seeing something like a graph grep. One one could imagine a visual studio graph editor type app with plug-in support. Even a standard merge tool would be a huge step up for non-text-based code assets.
A man can dream!
Besides the declaration of a graph, there also needs to be a way to define semantics, ie something akin to a Language Server Protocol. That way tooling can enforce validity while editing a graph.
It sounds like you're trying to make something new, in which case: I'd forego standardisation concerns for now, and focus on getting something that works, and where future prototyping can be backwards-compatible.
First, some of these suggestions will make it harder to introduce bugs when updating the code. That's good! Particularly tricky is when somebody splits up identifiers or function names. These types of things often occur at boundaries (calls between servers or to the db) which can make them tricky to test. Even if all your identifier combining is initially done in a single file, it's easy for someone to see the final shape of the identifier and accidentally hard code it somewhere else.
Second, In the spirit of Titus Winters' "software engineering is programming over time", a codebase should be greppable over time.
That means that if you rename a function, you might consider saving the old name of the function in a comment.
I have experienced a number of cases where IDEs were opened but they were useless, and I had to grep+find+vim+tmux for everything in front of ex-colleagues who didn't know better
1. a python codebase written for web scraping, this project can single-handedly be used to teach people how to not write python
2. production service in linux bare-metal servers where nginx + lua files were used for load balancing, backend written in go a few years ago, and there were php files as well
3. php and typescripts large codebases, debuggers were used as well, and no, no one could reason with the problematic areas, particularly on multiple inheritance diamond problems. I was never contented with my successful attempts to adding code in them
I don't know how useful IDEs can be in these cases, but no one other than me in those ex-companies could handle them. I even lent my laptop with IDE and full environment to some supposedly-very-senior colleagues, they could only avoid the problems at best
edit: and for a project like chrome, [Fixing a bug in Google Chrome as a first-time contributor](https://news.ycombinator.com/item?id=41355303)
Keeping things referentially transparent helps a lot here.
Don't:
base = "abc"
something = base + ".some.suffix"
Do:
something = "abc.some.suffix"
I've also had some luck with hard-coded UUIDs at call sites, e.g.: log.Info("something", "callsite", "DECAFBAD-000...")
because it makes it absolutely trivial to find a log, and unlike caller-lines (which are great! use them too!) it doesn't change when you refactor code.As for identifiers, use 'foo.?bar' case-insensitively.
For the JSON keys, I agree somewhat, although it would be easy to compensate by vertically aligning the values.
One of the unexpected extremely powerful things this allows is finding function usage extremely easy in any text editor that supports regex. You just search for ^[functionName] . Since you know that function pretty much will only be used at the beginning of lines. You can thus make edits against the AST with regexes and without parsing the AST at all.
It's pretty amazing, and leads to quite faster development, and allows one to tackle bigger and more complex problems.
.product {
&-promo {
I always felt it was a bad practice but nevertheless it is often used. Glad to find out that I am not the only one who noticed this.Of course, one might say that I should have used a specialized tool for this. But it would be more convenient if I could search anything with text search. Also, there is no tool that allows to search over identifiers that are calculated in runtime in a program.
But that's also what makes me uncomfortable when reading this article. Proper Naming is truly an "art" of balancing trade-offs.
It takes domain expertise (Ubiquitous language), understanding of the users of the code (other devs, not end-users), and a lifetime of coding f*ups where naming something wrong turned out painful to balance these.
The author gives a nice example of a dynamic table naming. But their refactoring didn't keep the behaviour the same (the else/catch). So it's hard to argue the first is better. And in this case, even without the else/catch, I'd say the latter is better. But there will be cases where greppability is to balanced with readability, testability or refactorability. And in these cases, for me, greppability comes last.
1) Eliminate spelling mistakes. Eliminate alternative spellings. UK vs US English? Pick a side and stick to it.
2) Eliminate contractions. Or keep a very short list of allowable ones (We permit “info” for instance.)
The point of this is to increase the predictability of the names you use. If you’ve got “tradeable” and “tradable” in your code base, search for it is going to be a pain. You can supplement these rules with common coding standards like “We call these things providers.” but just getting the spelling consistent is huge.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
just the other week i noticed a change in recommended formatting style in a project i contribute to regularly, and the result was source files got about 20% taller, 20% more of a pain in the ass to edit without some sort of syntax folding. the rationale? diff. making you reach for a syntax-aware editor to compensate for a deficiency in the syntax-awareness of a version control frontend is certainly a choice.
the business end of git as seen by most programmers is in fact diff city, sure, but deep down git is a bunch of snapshots. even deltas behave nothing like diffs. pull up the spec for the pack format and look for the word "line". you will not find it.
things could be so much better, but for now we live in a world where the headline is true.
The basic idea was that if you have a data artifact (columns for dataframes initially), you should be able to ctrl-f and find it in your codebase. 1:1 mapping of data -> function.
People take a long time to figure out that the readability gains from having greppability is worth whatever verbosity that comes, largely because they think of code too much as a craft (make it as small/neat as possible) and not documentation for a live process...
Turned out the shortcode name is based on the file name rather than file contents.
If you know that you need either a shipping or billing address and the user has specified which one they need, just query based on that.
There’s no need to introduce a function (getTableName) to detemplate a string or match on a case.
Instead just create a function that gets the item you want from the DB and has the table name as input.
On your UI make sure when the users specifies billing or shipping address the correct parameter is passed to the API.
That was refreshing.
> set(${VAR}_VERSION ${VERSION})
This is the main reason I don't like CMake.
I suppose the other extreme is to have a program generator with an input spec and you being left to read through the generated code without access to the input spec.
requests.get(f'http://a.b.c.d/wol?device={wol_computer}&grep-id=wake-on-lan', timeout=3)
This way I find `grep-id` in the server logs as a reminder of what to grep for, then `grep-id=wake-on-lan` in the entire codebase to find the actual source of the call.Or I add comments with a grepable token to the code.
I generally work from an IDE anyway, where it is clear that I am working with a value that is part of a JSON object and I can follow it back to the proper structure anyway. In fact, the more I think about it, the more I feel like the article is written for a very specific use case and perspective. Almost to the point where the saying "if all you have is a hammer, everything looks like a nail" is applicable. Where if it doesn't look enough like a nail it should be adjusted to look more like one instead of expanding your toolbox a bit.
IDEs do a decent job but are typically lacking compared to the raw power of grep.
> Black defaults to 88 characters per line
https://black.readthedocs.io/en/stable/the_black_code_style/...
For example:
Added %d users
Vs: Added users (%d)
Then it is much easier to track down where things come from without needing wildcards in the search or to care too much about what might be dynamic in cases where its not obvious.Which will give you grepability and in theory parsability so you can automatically bisect for a value change or something along those lines.
I am unsure if I like the author's approach because there are other cons, but it's a good point.
Even a major refactor is relatively easy if you can find stuff in your codebase. Even a small bugfix can get complicated if there's a ton of ambiguity
This post is very welcome, it sums up my own ideas about grep in a better way.
The more broad understanding here is that in computer programming there is a difference between a symbol and a referent (https://open.maricopa.edu/com110/chapter/3-1-language-and-me....) AKA variable and its value.
(Phrased colloquially: "The finger pointing at the Moon is not the Moon.")
Information about a given computer program may be found in its symbols (variable names) and its referents (values that variables are set to).
The referents may be set in source code (i.e. a string or other constant) and be visable/greppable at design-time -- and/or they may be set at runtime (and be non-greppable!)
The article is good; I have nothing against it, it makes an excellent point.
But if we look at what I've outlined above as three possible places for information (source symbol, source referent, runtime referent), then we observe that one of these possibilities is not covered by grep -- that is, runtime referent.
This leads to the question:
"What if grep could be used to search for runtime referents, aka runtime values of different variables?"
Well, in traditional compiled languages, we can't do that without significant monkeying with the language...
In interpreted and Lisp-like languages, yes, the above is possible -- but without being able to be very specific about the what/how/when of the runtime strings (or more broadly, values) to be grepped, doing the above could generate huge amounts of data and speed degradation, particularly if a value is set and reset multiple times in a long loop!
But could it be done? Definitely! Efficiently? Well, that's one of the questions! Deterministically? If random values are in play, probably not. With full code coverage so that values can be known from code paths not taken can be known? That scenario would be challenging to say the least.
Point is, it may be an interesting future language feature to consider in various scenarios, for future language designers...
"The ability to grep through all runtime referents, aka all runtime values, of all variables, for a given runtime of a program..."
Hmmm... now that I think about it, what if a language was created such that you could pass it a special flag, if you passed it this special flag, then variables would maintain their histories in memory (at great expense to performance -- this would be for debugging only!), and then you could grep that memory?
Yes, I know... given the pace of technology, there probably is some system or systems that might implement some aspect or aspects of this...
rr comes to mind:
https://news.ycombinator.com/item?id=31617600
(Maybe the rr maintainers could be persuaded to implement some interface with grep, either pre or post program run, or maybe implment a timer where the user can grep their program for various strings at various time intervals...)
Anyway, good article!
> "The ability to grep through all runtime referents, aka all runtime values, of all variables, for a given runtime of a program..."
It sounds entirely doable in a garbage-collected run-time. We "stop the world" and then run something similar to the marking pass of a full garbage collection cycle, which looks for matches for a pattern in the entire space of reachable objects. The routine could maintain a path from root pointer to object (easily done on the recursion stack) so that it's reported along with matches.
>"The routine could maintain a path from root pointer to object (easily done on the recursion stack) so that it's reported along with matches."
Yes, it definitely could!
For extra points (if the additional functionality is desired!), also create an API interface for this functionality for coding AI's, present and future, such that they can also perform this form of grepping automatically when needed, for example, when running/debugging a program they wrote, or are assisting a programmer with making changes to...
(Observation: There seem to emerge two different main patterns with respect to variable inspection... One is to set a watchpoint on a variable and stop the program when the variable changes.
Another would be to globally scan all variables (i.e. grep) at specific intervals (a form of a batch pattern) for specific strings/values, and all of that would be/could be -- settable by the programmer and/or an AI...)
Anyway, your idea is a good one!
Yet! There is a way to report all the paths by which an grepped-out object is reachable.
Say we encounter an interesting object O for the first time, at path A B C. So we have A.B.C.O.
We keep objects A, B and C in a hash table. The hash table represents nodes we have visited, which are on the path to a hit.
Then say we encounter object B again, at path X Y Z B. B is a hit in our hash table, and so we report X.Y.Z.B.C.O as another hit for object O, without descending into B again.
(If it is for you, congratulations, you're the janitor in the codebase. It sucks, but that's what you're being paid for. Maintenance is a means, not an end.)
In particular, one of the most important and frequent thing you do with code is read it in order to understand it (locally, at the abstraction level of interest), and the advice from this article compromise it badly - almost as if hoping that, on a greppable enough codebase, you could use grep to avoid reading or thinking entirely.
1. Don't split up identifiers
Don't split them up for the sake of splitting, sure. That's not helping anything. But in the example given, there's likely a good reason for it - for example, it codifies the intended coupling between tables. `billing_address` isn't an independent term in this code, nor are the other `_address` table names. There's a naming pattern there, encoded directly in the initial example. The proposed refactor obscures it and triples the amount of code in the process (all of which is low-value noise) and introduces possibility of making errors (typos, copy-paste) of the kind that isn't picked up by compilers (hope you have good tests!).
FWIW, the author's refactor may be eventually required - if and when the naming pattern in the original code no longer holds. But not before then.
2. Use the same names for things across the stack
Excessive data repackaging is bad, but that tends to be a symptom of having too many layers. A good layer has specific semantics that distinguish it from layers above and below it. This may necessitate renaming some thing, in which case even if such renaming is as trivial as in the example, it should be spelled out explicitly; you can't just return Layer 1 Address object instead of Layer 3 Address object, if the two layers mean something different by "Address"; the triviality of the mapping is incidental and may not hold over time. If it really feels trivial, chances are one of the layers is not necessary in the first place, so go fix that.
3. Flat is better than nested
Now that's just screwing with people, especially wrt. nesting namespaces. It's asking to reintroduce the visual noise that the person reading the code will then have to filter out again mentally.
The way I see it, if you grep for some log message or unrolled identifier and can't find it, you're supposed to keep grepping for parts of the string, until you hit a match. You then go look, and it's usually apparent that you're dealing with a compound identifier or an interpolated string - congratulations, you just learned something important about that part of the legacy codebase, which is the real job you're supposed to be doing.
See https://english.stackexchange.com/questions/30001/why-is-shi...