... 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.
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.)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.
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.
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.
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.
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.
Most of us use exuberant ctags to allow jumping to definitions.
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.
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.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.
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.
Working with legacy code — the scenario the author describes — I often can’t install anything on the server.
...use source code tagging or LSP.
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.