Reducing cognitive load is an excellent goal, but this seems more about distrusting that some other code does what it is meant to do. Pi is pi. If you're worried about it, command-click into it, or whatever the equivalent is in your tools. Or more generally: it's completely fine to be reading something at an abstract or symbolic level and use some mechanism to drill in when desired. The idea that you wouldn't have higher and lower levels doesn't seem right to me, and I don't think there's any way not having abstractions would lead to more readable code. (Now, it's certainly true that not all abstractions are good, and that vaguely defined and haphazardly layered abstractions make understanding code difficult but that's a whole different question.)
For readability, personally, lemme have some spaces between those tokens.
If you need to pass the value of PI because you somehow do not trust it's origin, how on earth can you trust the exported function named "circumference" to calculate the actual circumference? Also, what if the rule of physics and mathematics change in future and we need to alter the definition of circumference? Maybe we need to pass a function that does the actual multiplication too?
I also find it difficult to understand the example. Why would "pi" be a configurable value? And if something /is/ configurable, the whole point is that the consuming code doesn't care where it's set.
Can anyone explain the argument in another way for me?
Root issue is that Go's "packages" are in fact stateful modules, and since Go does not support 'finals' or 'vals', you actually never know if somewhere malice or mishap has changed an imported package's global variables.
I wonder what a marriage of Go's light touch syntax, the golden nail of go/select (but generalized to include IO), and a proper type system would look like.
All in all, I like Go, but the abscence of proper const/readonly/immutable data is one thing that I am unhappy with.
Go's const is closest to C's, which is ... terrible. const-correctness is one of the things where C++ offers a huge improvement over C.
At work, I use C# from time to time, which has two types of const: const as in compile-time-constant, along with all the restrictions that implies. And readonly which qualifies a variable as "once it has been initialized, it may not be changed".
To be fair, I have not run into major problems for a lack of it, but then again I mostly use Go for toy projects in my free time. Were I to use Go in production code, I would sleep a lot better if I could declare data as immutable.
Wait, but the argument of the post isn't "some unrelated piece of code could go and change the value of config.Pi, affecting everyone". Rather "I don't know if the code of the config package set config.Pi to be actually pi".
Also the code should just use math.Pi. There are not that many use-cases I can come up with that you would want to use anything different. So I do agree that it is a terrible example.
>To reduce cognitive load, I want to be able to read a function/method without having to think about where some variable comes from.
To reduce cognitive load for me, I should be able to treat the function/method as a black box. Care about what it receives, care about what it returns, don't care about how it goes about accomplishing it.
>When I read the code above, my first question is: How is config.Pi defined?
That line really hammers home the difference. I trust the previous author, the test suite and ultimately, the project. Programmers seem to fall into two camps. Those that need to know details to understand a method and those that need to know the contract to understand a method.
I think there is an implied context here that the author didn't do a good job of communicating. Usually I will also assume the function does what it says when I use it in code I'm writing.
But what happens when I'm debugging a problem in code I may or may not have written myself? Or when I'm debugging code that I wrote so long ago that I no longer remember all the details. That's when I need to be able to follow the code in the function I'm consuming. You are right that when you are consuming code it's good when you can treat that code as a black box. But that's not actually where I spend the most time as a developer. I spend far more time reading code I don't have full state about and trying to figure why something is broken.
Pi was probably a bad example here since we are conditioned to think of it as a constant. But there are similar uses in code where a function is doing bad things and it's not clear why because you didn't realize the function had an undeclared input from global mutable state. I think that is what the author was trying to say in his post.
> To reduce cognitive load for me, I should be able to treat the function/method as a black box. Care about what it receives, care about what it returns, don't care about how it goes about accomplishing it.
I couldn't agree more.
This was the massive reason I loved Go when I switched from Node. My cognitive load was massive in Node, because I could not trust a function just by looking at it. Did it return a value? Did it take a callback? Did it return a promise? etc. I always had to have the documentation up. Now with Go, not only is the function signature easily accessible from Code (due to the static nature of it), but most things return values. Async doesn't propagate in an almost sinister way, like it does in JS.
In fact, I'm switching back to JavaScript due to needing to write some ReactNative stuff, and I'm having PTSD of sorts. React & Redux are great, I love them, but they're designed quite synchronously. Async just piles on another layer of complexity, for such a simple task.
Typescript and as mentioned below Flow are good solutions to this problem. Add appropriate linting to force types to be defined on methods and you just hover over (or however you prefer to open a method definition window) and get a breakdown of types that you can click to go to if you need further definition.
> I trust the previous author, the test suite and ultimately, the project.
I agree with your first point (functions should abstract some things away), but I disagree here. I feel like this falls into a "let's just not do stupid things" category of stuff that we just can't achieve. If people didn't make mistakes, we wouldn't need tests or monitoring.
I trust people but that doesn't mean I assume everything they do will be flawless. What if you're working on this project and you're debugging an issue where the function's not working right?
Or, what if you want to change how the function works? Pi is admittedly a poor example of this but the general concept is sound I think.
> Programmers seem to fall into two camps. Those that need to know details to understand a method and those that need to know the contract to understand a method.
A user of a software module shouldn't need to know the details of a function, but readability is much more for other engineers working on the codebase than it is for users of the codebase.
> Programmers seem to fall into two camps. Those that need to know details to understand a method and those that need to know the contract to understand a method.
This is an interesting observation. I seem to fall into the former category, but I'm not sure if that's a side effect of working with proprietary codebases that have less than stellar interfaces.
When stuff ultimately goes sideways, I value being able to relatively quickly reason about the flow without firing up a debugger. Information hiding cuts both ways.
Even if you don't trust the previous author, you can still use your "go to definition" shortcut and see the real thing: who is it who is coding one file at a time?
You shouldn't be considering the implementation details every time you want calculate the circumference. Even that pi is involved is WAY more information than the caller needs to know. And if you are debugging you need to know where to look for the answer, not the answer itself.
Passing arguments to a function instead of using global state is generally a good idea, but that's a very poor example. Having to pass constants to a function would increase the cognitive load rather than reduce it.
You can imagine a case where the function has to verify an answer to a problem where the pi is assumed to be 3. Say e.g. writing a maths quiz, an online course, etc.
I think this is example is too simple and misses the point of a config package. If you have config options being read from several different sources (files, args, env) and need to initialize subsystems which need more than one or two parameters (think server with several timeouts, paths, handlers) then you want to pass this as a struct and you need to manage that struct somewhere.
Configuration is also an external contract since you don't want to break installations because you want to rename a variable. Therefore, you have to distinguish between argument parsing and the actual runtime configuration and it makes a lot of sense to have that in a single package since you can then test it properly.
Configuration is a complex issue for non-trivial projects. Managing this in one place allows to have one source of truth.
This isn't particular to Go, and you should just pass in any variables that you intend to change, rather than depending on a global mutable variable for your state (especially in a concurrency-heavy language like Go).
Generally yes. But pi is one of those occasions where it only needs to be a constant somewhere and then forgotten about. The last thing you need is developers accidentally mistyping the value of pi causing the output calculations to be wrong.
If altering the precision of pi is something that matters, then the function should take a parameter for the precision rather than the value of pi.
Pi is not a variable. I was very careful with my wording to say things like "variable", "expect to change", and "mutable state" so my statement would apply generally. :)
Your wording might have been careful but the author did use pi as a variable in his example and discussed wanting the ability to change it. Since you were essentially agreeing with her/his point, it's forgivable if one then assumes you were also agreeing with his example as well. Hence my counterexample (and hence why I opened with "Generally yes" to signify that I don't disagree with your point but the example provided needs tweaking)
I'm not sure I was agreeing with his point, since I only consider one of his configurations 'valid', and even then only insofar as Pi is a variable (which it isn't). My original statement holds generally and specifically--pass in the things you may want to vary; don't depend on global mutable state.
I'm not sure who "he" is, the OP or me? Neither of us are remarking on pure functions, and the OP's suggestions were largely impure (e.g., a global mutable variable to change whenever you might want a different Pi value?). Generally I prefer pure functions, but I'm not religious about it. For example, if the function in question is a method on an object which needs to modify some state in the object, that's all well and good. Just make it explicit that the function has side effects and those side effects are scoped to the object and its methods.
For me choosing great names for packages, functions, and variables is what reduces the most cognitive load. I continue to work on my food side project BestFoodNearMe.com for just a few hours a week. It is critical that I squeeze every ounce of productivity out of this time.
Another thing that helps quite a bit is very detailed explicit logging messages when something goes wrong. I can read the message and know exactly in the code where I need to fix the issue.
So basically you're saying to use pure functions. Something that you see in a functional language, where given the same input, you can expect the same output every time.
I like that idea, don't get me wrong. I think you are just using a bad example. If a package name is "config", I'd expect things in that package to be a reference point for developers if needed. But most of the time, "config" is just a place variable for something that will change when the config is set, not something the developer needs to add cognitive load for.
Now if it was something else and I wanted to make sure that the output of the function changes someday unexpectedly because something used inside the function has changed even when my inputs are the same, yes. That is now ideal in any language.
I was so confused with `type DefaultPi float64 = 3.141592`... I thought Go had started supporting defining custom default values for types and then interpreting `type` as its default value when used in expressions....
The funny thing is that there are languages that allow constants into the type system. The feature has a few names, but one of them is pi-types (well, π-types) [1]. Go even uses them for its array types. However like generics, it's not a feature of the type system that can be used outside the built-in types.
There is a parallel in C++, where one school of thought says you should not use using directives, but instead fully qualify identifiers from outside the current namespace, to make it clear that they are external dependencies.
More generally, this is the sort of thing that should be addressed through our tools. While it is useful to be able to write code in a basic text editor, we should usually be using more powerful tools, especially as source code is so amenable to analysis. Syntax and style are not the only way to make things better, and we should not be the cobblers whose children go shoeless.
Alan Kay made this point when the US DoD was looking to improve its software development (and picked Ada.)
Like everyone else here, I don't think this is a very good example. Even if it were, other languages can do a better job of accomplishing the same goal. For example in Python:
def circumference(r: float, *args, pi=3.14) -> float:
return 2 * pi * r
Off topic... I tried Google search for 'backend workers' and not much information came up. What are backend workers? Where are they hosted and how do they integrate with APIs or triggered periodically? Or, do they just listen for events?
"Backend worker" is a pretty generic term, and I think in this context it just means any server that isn't a web server or API server. Examples:
* A "users" microservice in a microservice architecture might load relevant information and permissions about the current user. A web or API server that's directly handling a user request would call that microservice for more information.
* In response to some user action, a task is sent to a task queue to do follow-up processing, e.g. reindexing any changed data. The task queue system sends the task to some worker process that specifically is there to handle tasks.
* A daily cron job on a dedicated machine sends an email to everyone who signed up between three and four days ago.
It seems that people are using different phrases and words for the similar things, i.e. message broker, message queue, message pipline, message bus, microsevice, service, backend worker, task queue.
I don't like this example because I'm not sure what kind of realistic situation it's meant to represent. If config.Pi is really a config value, a value set at runtime by whatever configuration method is used by the application (environment variables, config service, whatever) then config.Pi is a very clear way to communicate that fact. If you want to communicate something more concrete in the code about how config.Pi is set and what its default value is, the right place for that information is where it's defined, not in every file where it's used.
It's possible that the right thing to do is make pi a function parameter, but it wouldn't really change the readability of this file. It would change the readability of the code where Circumference is called, maybe for the better, maybe for the worse.
While I really like readability and having things close at hand, I really hate making a change to some constant only to discover later that it had to be updated in a gazillion different places with slightly different names
last time i tried using funcs a lot it turned out that it's not possible to typedef a function signature and use it to pass funcs as arguments which is very unfortunate. is this by design or is it getting fixed?
Not sure what this has to do with generics, Go requires types for function arguments. If your anonymous function gets clumsy and too big in place, just use a named function not an anonymous one:
No, it's just a syntax issue, not a generics issue.
Are there any manifestly-typed languages that work that way, though? I'm having trouble thinking of one, though given the number out there I won't be surprised if there is.
This has nothing to do with Go, it's just basic software engineering. It reminds me how ignorant the Go community is of the decades of research in programming language and software engineering theory.
Reducing cognitive load is an excellent goal, but this seems more about distrusting that some other code does what it is meant to do. Pi is pi. If you're worried about it, command-click into it, or whatever the equivalent is in your tools. Or more generally: it's completely fine to be reading something at an abstract or symbolic level and use some mechanism to drill in when desired. The idea that you wouldn't have higher and lower levels doesn't seem right to me, and I don't think there's any way not having abstractions would lead to more readable code. (Now, it's certainly true that not all abstractions are good, and that vaguely defined and haphazardly layered abstractions make understanding code difficult but that's a whole different question.)
For readability, personally, lemme have some spaces between those tokens.