Hacker News new | ask | show | jobs
by dandotway 1611 days ago
So whenever I have to study someone else's 'dynamic' python I encounter this sort of thing:

  def foo(bar, baz):
      bar(baz)
      ...
What the heck is 'bar' and 'baz'? I deduce no more than 'bar' can be called with a single 'baz'. I can't use my editor/IDE to "go to definition" of bar/baz to figure out what is going on because everything is dynamically determined at runtime, and even

  grep -ri '\(foo\|bar\|baz\)' --include \*.py
Won't tell me much about foo/bar/baz, it will only start a hound dog on a long and windy scent trail.
10 comments

Yup, I find this completely insane behavior to think that you somehow benefit from types not being there.

You just make it way harder for people to understand your code and contribute to it.

To be fair, in recent years I've worked on a number of Typescript projects and it was common for developers to use `any`, `Object`, `() -> Promise<void>`, etc. Not super helpful.

Though in my experience, sane code structure and informative comments trump everything else when it comes to understanding big and unknown codebase. I still shudder when I think about working years ago on various Java codebases (mostly business IT systems). What a convoluted mess of n-levels deep interface hierarchies. Types? Yeah, but good luck unraveling what exactly is happening in the runtime.

The example is entirely ridiculous. Types are names too. Does foo(bar: baz) solve the issue? Languages are there to convey meaning.
At least I can navigate to the definition.
It doesn't just make the code harder to read, it makes it run slower, too. Static typing provides some compile-time guarantees about what's going to go where, so the compiler can make a lot of simplifying assumptions that speed things up.
Insane, no - it's just a tradeoff.

I agree that on balance type signatures are better -- and that's why modern Python has evolved to incorporate them. But they aren't a magic cure-all, and do they impose a significant tax of their own.

Oh yeah the easiest code in the world to read is some contorted type system and function signatures that look like hieroglyphics that you need a PHd in CS to comprehend.

Python is easy to grok, and if you have programmers writing code like bar(foo,baz) then the problem is not Python. You can write crap in any language.

Unit tests do much of what typing checks anyway ... and here's the thing ... you NEED unit tests no matter what. No typing system can tell you that you wrote > when you should have written <.

> Python is easy to grok

It’s what you’re used to. I personally find Python horrible to read because I used to a whole different class of programming languages. But I’m sure some of my code might be hard to read by others who aren’t used to that particular programming language too.

> Unit tests do much of what typing checks anyway ... and here's the thing ... you NEED unit tests no matter what.

Some, not all. Strictly typed languages are handy when it comes to refactoring and unit tests can sometimes fail there if the design is being changed enough that the unit tests need rewriting too.

> No typing system can tell you that you wrote > when you should have written <.

Not technically true. Some languages with a richer set of types and operator overloading could have code written to detect that sort of thing. But I do get your point that unit tests are import too.

I’ve been programming for > 30 years and in dozens of different languages. In that time I’ve felt strictly typed languages make larger and more mature code based slightly easier to maintain. While loosely typed languages are easier for smaller and/or younger code based. But largely it boils more down to personal preference than anything.

I will caveat that by saying the fact that Python supports type annotations should be telling that even dynamic languages benefit from a stricter approach to typing.

I'm glad to see someone else that finds Python unreadable. I keep seeing people saying that it's one of the most readable languages out there, and each time I feel like I'm from another planet.
> Oh yeah the easiest code in the world to read is some contorted type system and function signatures that look like hieroglyphics that you need a PHd in CS to comprehend.

People who just learn programming probably think the same about whatever language they are learning.

> No typing system can tell you that you wrote > when you should have written <.

The more the compiler can figure out for you, the quicker problems can be identified and fixed. I stopped using python altogether, because it was just infuriating to have the tiniest mistakes blowing up in spectacular and inscrutable ways. Mixing up values of complex types often does not fail at the actual site of the error, but much, much later. Sometimes literally later in time, as in hours, days, or months until you get an obscure "FooType does not Bar" error, and how the thing in question ever became a FooType is inscrutable at that point. If the result even is a runtime error at all! (Bonus points if your production database is now full of junk as well.[1])

The unit test did not catch it because it did not test the offending composition of classes and functions. Meanwhile, a compiler would have caught it immediately: "The thing you're doing here leads to your data structures being nested wrong."

When I started using async/await in python, at first it was just over, since in plain python that introduces another layer of typing without any assistance whatsoever. Then I discovered mypy which actually lets me do some amount of static typing in python, and it was very enjoyable and now python is back on the table for smaller projects.

There is a reason Haskell has the reputation of "if it compiles, it works". There is a reason why system programmers that work on critical systems are jealously eyeing Rust if their shop still does C.

By the way, dependent type systems absolutely can tell you if you wrote > instead of <. But since that usually comes at the expense of not being Turing complete anymore, it's more used for very critical systems, or for theorem provers.

[1] Yes sqlite, I'm looking at you. The decision to make database column dynamically typed, and hence have for example an INTEGER column silently accept data that is very much not an INTEGER at all, caused me some grief on a widely deployed system once.

FWIW, sqlite now has 'strict' tables.
Thanks! I still like sqlite a lot and plan to use it again someday, so I will be happy knowing that in advance.
Typing allows you to specify the expected behavior in terms of input/output structure of algorithms in such a way that they can be statically verified without writing unit tests or manual checking code in the source, allowing your unit tests to check behavior by value rather than by value and structure. The equivalent of type checking is not unit testing, but fuzzing.

Python is not easy to grok at all, when you consider you have to grok implementations to understand what they are supposed to do, and require extensive runtime debugging to figure out if it is behaving as expected before you can even write unit tests.

Compare to decent statically typed languages, which have quicker write/debug cycles since checking type definitions is faster than checking code behavior, and the structural unit testing is covered automatically by the compiler.

It's like getting more than 50% of your programs' test coverage, for free!

> Python is easy to grok,

It's not, though, it just gives you that illusion.

The code might be easier to read but it's harder to understand and to modify safely because of the absence of type annotations.

> Oh yeah the easiest code in the world to read is some contorted type system and function signatures that look like hieroglyphics that you need a PHd in CS to comprehend.

You can also make a book easier to read by ripping out all its pages.

If you eliminate the content you need to read to understand something, what have you actually made easier?

> No typing system can tell you that you wrote > when you should have written <.

There are many that can; e.g. via SMT-decidable refinement types, or even full undecidable dependent types coupled with automated solvers and manual proofs.

Yeah, I always say that python is an amazing language to prototype and terrible language to scale precisely because it lets people write the usual terrible code and then gives you the freedom to make it even worse.
In Clojure, I tend to put pre and post assertions on most of my functions, which is useful for checking errors in the schema of runtime data (very useful when dealing with 3rd party APIs) but it also offers the documentation that you are seeking:

    (defn advisories
    [config]
    {:pre [
         (map? config)
         (:download-advisories-dir config)
         ]
    :post [
            (map? %)
           ]
    }
    (let [
        dir (:download-advisories-dir config)
        ]

    ;; more code here
And now imagine the compiler would actually enforce that practice, and you have static typing, with less boilerplate.
How is this any better than static types?
Pre/post conditions are complementary to a type system. They can ensure logical properties that may not be encodable in your underlying type system (that is, essentially every mainstream statically typed language). Such as the relationship between two values in a collection. Trivial example, if you have a range such as [x,y] where x < y must hold, how would you convey that in any mainstream type system?
The Haskell-y way to do this is to use a smart constructor[0].

[0]: https://wiki.haskell.org/Smart_constructors

The first part of that page demonstrates what amounts to pre/post conditions, but placed in the constructor. The range is checked dynamically, not statically.

The second part is using Peano numbers to enforce the constraint. I guess you could try and force that into some mainstream languages, probably C++. With its template programming you could get something going in this vein, though I'm not sure how well it would work if the number were calculated at runtime rather than compile time. You'd still end up with a dynamic check somewhere.

The way that the value floats through the system is checked statically, and the program can (and should) be designed so that the value with the appropriate type cannot be constructed unsafely.

If you need to statically check the construction of values in Haskell, there are things like refinement types[0].

[0]: http://nikita-volkov.github.io/refined/

This is one of my favourite blog posts on the Internet and I implore every programmer to read it.

> The claim is simple: in a static type system, you must declare the shape of data ahead of time, but in a dynamic type system, the type can be, well, dynamic! It sounds self-evident, so much so that Rich Hickey has practically built a speaking career upon its emotional appeal. The only problem is it isn’t true.

Static languages unfortunately don't save you from that. You find automatically inferred types, or types that refer to some abstract interface or template-class-mess but you have no idea where the actual implementation lives until you compile with RTTI and run it under a debugger... and as tfa posits, people working with the limitations of static languages often end up reinventing a dynamic structure.

Is this somehow supposed to relevant to the posted article or did you just want to start a tangentially related dynamic-vs-static flame war here in the comments section?

> Static languages unfortunately don't save you from that.

While you still can make a static language that is confusing, it's a lot harder... I challenge you to write a function signature in Rust that is both:

1) Useful

2) As opaque as the python signature above.

> You find automatically inferred types

A minority of static languages do type inference in function signatures. I think it's a bad idea for exactly the same reason the python code is bad. On the other hand, every dynamic language allows you to omit any information about a type signature.

They usually save you from that particular pitfall, but not always of course.

Static vs dynamic makes for such difference in the detailed workflow, both in terms of changing existing code & in terms of writing new/(more) from scratch code, yet they can both be quite fruitful, and can both be abused in absurdum.

It seems like people naturally fall into one of the two camps (either by personality or by training), and the other side just seems kind of insane: "how can you even work that way!?". Then culture and idioms emerge over time and strengthen the tribalism.

I've gone back and forth between the two over the course of my career, and it's quite a mind-shift when switching, with a fair bit of pain involved ("but it would be so easy to do this in [old language]", or "what the hell is this garbage anyway!?") and then eventually it settles in and it's not all painful, all the time ;)

(Going back and forth between Scala and Python right now, so this hit a bit of a nerve)

> Static languages unfortunately don't save you from that. You find automatically inferred types,

Oh yes, they do. Even inferred, the types are there and pretty easy to locate, even if you're not using an IDE.

The types are there.. but you don't know which one it is that your program is dealing with. You could have dozens of implementations for any given abstract interface. One gets picked up at run time.
You don't need to know which one, because the abstract interface tells you how to use it...
That's the theory. Works great when there are no bugs and everything's been designed just right. In that world you could wipe implementations from memory because you won't ever need to dive in..

Very often I'm looking at code and "how to use the interface" is not a question I'm looking to find answers for.

Some information is a lot better than none. In some cases you might want to know what implements the interface: that information is also statically available. In Rust, you can look at a trait and see what types implement that trait.

If you need to know exactly which implementation is being used in a particular context then maybe you shouldn't be using an interface, but should be using the concrete type?

> to some abstract interface or template-class-mess

And traits! "Oh look, this functionality is implemented in a trait implemented by a trait implemented by a trait implemented by what you're looking at. Maybe"

The real power of dynamic languages is being able to do:

    const foo = JSON.parse(arbitraryJsonString);
and not having to worry about the structure up front.
When that JSON payload changes (intentionally or not), you will run into a mysterious problem in some unrelated area of your code. It will be significantly more expensive to fix than failing fast at the point of parsing.
But depending on the situation, that problem may never happen. I'm not a big fan of introducing complexity to guard against code screwups in the same codebase.
The hardest bugs are the rare bugs. Common and frequent problems are easy bugs to fix. These subtle or rare changes are those which should be feared. If you model only that which you support, finding the source of the problem becomes much easier.
But the thing is, don't you have to worry about structure? You have to unpack the elements from the JSON, so you will need to encode its structure explicitly, which includes type information. The only reason this would be useful is if you're just shuttling data to another API that expects a dict structure (that will then validate everything) and not JSON and you aren't really doing any real work yourself.
> The real power of dynamic languages is being able to do: const foo = JSON.parse(arbitraryJsonString); and not having to worry about the structure up front.

That's not power, that's a shotgun aimed at your crotch whose trigger is connected to a cosmic ray detector.

+NaN For comments that make me laugh out loud for duration T>2.0 seconds, I wish HN provided a way to transmute/sacrifice one's past karma points into additional +1 mod points.
Every static language that I know of also supports this -- you can parse into a sum type of `JsonAny` (or whatever), where `JsonAny` is one of `Null | Number | String | List[Any] | Dict[String, JsonAny]`.

The API then becomes a runtime fallible one, which is perfectly sound.

In C# I can just parse it to a Dictionary<string, object>, and I can do that without destroying the usability of the rest of the language.
What if the JSON represents a list, or an int?

Also, how do you then access nested objects, like data['key'][0]['attr'] in Python?

> What if the JSON represents a list, or an int?

Then you write one short operator (and I agree that some static languages make this more cumbersome than it should be) to say so, and either handle the case where it isn't, or explicitly declare yourself partial and not handling it.

> Also, how do you then access nested objects, like data['key'][0]['attr'] in Python?

With lenses, something like:

    data ^? (key "key") >>> (nth 0) >>> (key "attr")
If you do several unsafe operations in a row then this is cumbersome by design - you want to be clear which parts of your program are safe and which are unsafe, so that readers can understand and know where to review. But a good language should let you compose together several unsafe operations in a lightweight way and then execute them as a single unsafe operation, for cases like this where you want to work in the unsafe part of the language for a bit.
Sure there are solutions.

But my main point is that HideousKojima's "statically-typed" solution would result in a runtime type error if it was given unexpected input, just like a dynamically typed solution.

> But my main point is that HideousKojima's "statically-typed" solution would result in a runtime type error if it was given unexpected input, just like a dynamically typed solution.

I don't think HideousKojima ever called it a "statically-typed solution". Their point was that statically-typed languages still let you write unchecked code when you want to - and yes, of course such unchecked code can fail at runtime - but give you the option of having checking in the cases where you want it.

You can parse it to the dynamic type too. Everyone is happy...right?
Well in Python everything is an object, so that type definition holds true for Python as well :P
Rust:

    let foo: serde_json::Value = serde_json::from_str(arbitraryJsonString)?;
There, just as powerful [1]. But you know what's even more powerful? After you've done your dynamic checks, you can do this on the entire JSON tree, or on a subtree:

    let bar: MyStaticType = serde_json::from_value(foo)?;
and you get a fully parsed instance of a static type, with all the guarantees and performance benefits that entails.

[1] Value represents a JSON tree: https://docs.serde.rs/serde_json/enum.Value.html

There's something to this. I love featurefull type systems, but I've seen engineers try to parse JSON the "right" way in Scala, get frustrated, and blame the entire concept of statically typed languages. Elm manages to make this user friendly so perhaps it's "only" a matter of compiler messages and API design?
Structure is a virtue, not a vice. By doing this you're subverting your own interests.
Structure is a tool. Like any tool, it can be misused or overused.

For anything even remotely production-y I'll always prefer explicitly parsing JSON into a known structure, but there's a lot of value in in being able to do some exploratory scripting without those constraints.

Yes! Exploratory scripting is a categorically different thing than programming, though, I think.
not necessarily. there is a bottom up school of thought that encourages people to noodle around and construct primitives by playing in the domain, and then interactively composing those primitives into larger and larger systems.
Yeah, that's true. It's a judgment call for sure, but I've always found that angle on things to be self-subversive. The best programmers I know are all bottom-up learners, not top-down.
You can do that in static languages too by just parsing into a map
What's the type definition of the map out of interest?

  const Json = union(enum) {
      null,
      number: f64,
      bool: bool,
      string: []const u8,
      array: []const Json,
      object: HashMap([]const u8, Json),
  };

Usually it's a tagged union of the base JSON types which can easily be consumed by most statically typed languages or a variant of it.

EDIT: added "tagged"

In TS JSON is usually Record<string, unknown>.
Well unknown is not a type (by definition), so you have just stepped outside of a type system, which is very common in TS if I understand.
Unknown is a top type in the TS type system. It serves the very important role of "here you need to apply some pattern matching and validation" and then you can make sure that you can continue working in a type safe environment. TS has a lot of facilities that help you with this (from the usual narrowing things, typeof and instanceof guards, control flow analysis, and at the end of the list there are the big guns like this thing called type predicates which basically allows you to wrap these checks and "casts" in nice reusable functions).

There are also recursive types that help you model JSON, but knowing that it's an arbitrary deep nested map/list of maps/lists and number and bool and string mixed like a Bloody Mary cocktail doesn't really help :)

With NestJS it's very easy to add decorators/annotations to fields of a class, and the framework handles validation (throws HTTP 422 with nice descriptions of what failed) and then in your controller you can again work in a type safe environment.

https://www.typescriptlang.org/docs/handbook/release-notes/t...

https://www.typescriptlang.org/docs/handbook/2/narrowing.htm...

Json = Map<string, Json>
In Nim you can even convert JSON to static types! https://nim-lang.org/docs/json.html#to%2CJsonNode%2Ctypedesc...

Now you get type checking on JSON at compile time :)

How powerful is this, really?

As soon as you try to do anything useful to foo it's not arbitrary anymore. You have to make some kind of an assumption on the underlying type, check for keys, nulls, maybe it's a number (the right number?), maybe it's a list. So now you have to scatter some boilerplate checks everywhere you touch a part of foo.

If you could parse it into a typed structure up front, you'd only have to deal with this in one spot, and have guarantees for everything else that follows.

Bonus: if your typed language has good support for records, you can even do this in a way that only provides structure to the parts you care about, and is robust to changes to any other parts of the json.

You can trivially do exactly the same thing in Haskell, so I think you’re suggesting that dynamic languages have no “real power”.
I thought the real power of dynamic languages was being able to do things like:

   eval('alert("hello, ' + userInput.name + '!")')
What's 'foo' and what you can do with it?
An arbitrary object? What else would arbitrary JSON parse into? Then you can access its properties, like with any JS object.
That’s not a capability unique to dynamic languages
What the heck is 'bar' and 'baz'?

So there's no docstring? And the actual variables are that random and indecipherable?

Sounds like the problem is that you're tasked with looking at code written by someone who is either inexperienced or fundamentally careless. When dealing with reasonably maintained codebases, this kind of situation would seem pretty rare. In modern python we now have type hints of course, which have helped quite a lot.

In other words, in Python you have to rely on your colleagues manually writing documentation, and if they don't you're out of luck and they're 'bad developers' and potentially the whole product is affected.

In static languages this simply isn't a problem. Types are checked for consistency at compile time and you don't have to rely on people toiling on this busy work.

Not to say documentation isn't necessary, or good, but isn't something you need to create working programs because otherwise no one knows wtf any variable is without running the program.

No, actually I advocate static typing approaches for precisely the reasons you give. As does Python, since years and years ago.

I'm just saying that core problem it solves -- "bad developers" -- is going bite you no matter what (if not addressed at the source). And that supposed magical efficacy of measures designed to protect against it is somewhat overstated.

What I've found is even worse: on medium-size personal projects I inevitably pass the wrong type to a function (or refactor and forget to change a call somewhere). Generally it does not fail until somewhere else, which relies on it being a certain type. So even though I know what my functions should take, I still spend a bunch of time tracking down the problems, which involves a printing out a lot of stuff because the error happened outside the current call stack. This is something a static type system would just prevent for me, and I've basically decided that anything beyond really simple stuff is actually faster for me to write in a statically typed language. (Examples in the past are a parser for a static site generator with a mostly Turing-complete language, and 3D model generators)
> In modern python we now have type hints of course, which have helped quite a lot.

I had to laugh, hard.

If your Python program uses any library whatsoever, chances are that library won't have types, so you can't really use them.

Even super widely used libraries like numpy don't have good support for types, much less any library that consumes numpy for obvious reasons.

I had to laugh, hard.

It's fine if you want to insulate yourself. But I don't see that you're making much of a point here.

They're probably laughing because a) you're suggesting manually doing the work static typing does in a dynamic language because its untenable not to for large projects, and b) you can't easily add type hints to other people's libraries.
No - (a) is not what I'm suggesting. And (b) while disappointing, just doesn't slow one's work down very frequently in daily practice.

Look, I just don't buy the suggestion that static typing magically solves a huge set of problems (or that it does so without imposing negative tradeoffs of its own -- the very topic of the original article). Or that dynamic languages are plainly crippled, and that one has to be a kind of a simpleton not to see this obvious fact.

> just doesn't slow one's work down very frequently in daily practice.

Well, maybe you don't feel it slows you down, but it is manual work you must do to get a reliable product only because of dynamic typing. Not only that, but you have to then refer to these docs to check you're not creating a type calamity at some nebulous point down the run time road. Static languages just won't let you mess that up, and often have utilities to generate this documentation for you at no effort.

> I just don't buy the suggestion that static typing magically solves a huge set of problems

Static typing really does "magically" solve a whole class of problems without any negative tradeoffs, assuming the language has decent type inference.

Not all problems, but a specific class of them that you should do extra work to guard against in dynamic languages. Whether that is extra documentation that has to be reliably updated and checked, or run time code to check the types are what you expect at the point of use.

Take for example JavaScript, where typing is not only dynamic, but weak. Numbers in particular can be quite spicy when mixed with strings as I'm sure you know. Strong, static typing forces you to be explicit in these cases and so removes this problem entirely.

By the way, no one's saying anyone is a simpleton. The reality is our field is wide and varied, and different experiences are valid.

Dynamic languages can do some things that static languages can't. For example, you can return completely different types from different execution paths in the same function.

This has been something that has confused me when reading Python, but it does make it easier for stuff like tree parsing. In a static language you need to specify some variant mechanism that knows all data possibilities ahead of time to allow this. From my perspective the dynamic typing trade off isn't worth these bits of 'free' run time flexibility, but YMMV! It really depends what arena you're working in and what you're doing.

> I just don't buy the suggestion that static typing magically solves a huge set of problems

  // compiles and runs, but does bad things
  function foo(x, y) {
    someDangerousEffect();
    return x + y;
  }

  -- does not compile; huge sets of problems magically solved
  foo :: Int -> Int -> Int
  foo x y = someDangerousEffect >> pure $ x + y
You suggested that Python type hints are useful.

I laughed hard at that suggestion.

Can you maybe just show how to type a Python function such that it does the absurdly simple thing of taking a numpy array of integers?

   def fun(nump_array_of_ints: ???): ...
Just to show everyone just how "useful" type hints in Python _actually_ are.
Coming from any statically typed language to Python or JavaScript codebases this plagues me. Virtually every project I have seen suffers from this.

Function names and doc comments describe behavior, not argument and return types.

Our main codebase is in PHP, but we enforce type hinting for all new code, so it feels more like a static language at this point in practice. However, there are chunks of old code without type hinting or types in PHPDocs. Whenever I have to deal with that code (especially if it's unknown code), my productivity decreases considerably. I have to click through many layers of functions to figure out the data flow to understand what the implicit contract of a function is. In static languages, all you need to care about is a contract, the rest is implementation details. In the dynamic portions of the codebase, there's just too much cognitive load because I have to look at implementation details to get it. PHPDocs and dynamic checks seem to be pretty error-prone, because a dev often forgets to update both the code and the annotations/type checks (and type checks are often ad hoc and random), leading to even more cognitive load. Having static analyzers in the pipeline to have some control of the situation leads to longer build times, so in the end it feels like PHP builds slower than all our Go projects combined.
Not argument and return types.

When conventions are followed they do exactly that, actually. And unless the code is a complete trainwreck, it's pretty easy to tell what the return type is, even without explicit annotation in the docstring.

What conventions are those, besides Hungarian notation (which I don't think I've ever seen in Python)?
PEP 257, and local conventions in certain projects.

What all comes down to is: if you really have people on your project writing code like the foo/bar/baz example way up above - then you have problems way bigger than static type checks can possibly help you with.

Wouldn't it be something if there existed tooling that enforced this level of discipline and checked its validity before executing any code such that you didn't rely on the entire ecosystem to adhere to the same standards and remove that as a source of ambiguity...
I do not want to argument against Python. This is more like off topic / side note.

What I like about Rust is that it even checks code in your 'docstring'. So it is easier to keep it maintained.

> I can't use my editor/IDE to "go to definition" of bar/baz

I use "Find Usages" on foo to see where it is used. Once you see where it is used, you know what the types can be. It's not great, but it's also something that can be progressively remedied.

In the event that the function is not as trivial as your example suggests, the author should have written a docstring to help you understand what it is trying to do, in addition to type annotations that will make it more readable.

In this example, bar can either be a function, a class, or any object with __call__(), so the type information is less important in this case, than actual docstrings that express intent.

My worst developer experience ever was trying to make changes to a custom build system with functions exactly like that. Injector or decorator pattern, or what ever it is called.

I just gave up and introduced globals and used as flags for some places in the code.

To make things even more fun, big parts of the system was written in 2.7 called from runtime generated bat files from 3.4 as remnants of a rewrite and the consultant that had his funding cut.

That is why I like dynamic languages like Julia better because they use type annotations more frequently.
Well, you could put the types on these days.

But also, there’s nothing stopping the code from being much clearer about its intention than this weirdly contrived example (I have a lot of code and most the function names are pretty unique). And surely you want to search for ‘foo(‘ to find invocations.