Any word on when some of project amber features will come out of preview? I get excited each JVM release for some of those features, but it seems like most of the releases the preview count just gets bumped, and a few more get added to the preview holding pattern.
Text blocks, var, records, sealed classes, and pattern matching in instanceof have been out of preview for some time, but two more features -- record patterns (https://openjdk.org/jeps/440) and pattern matching in switch (https://openjdk.org/jeps/441) -- are about to come out of preview.
The book "How to Measure Anything" by Douglas W. Hubbard has a chapter on using Monte Carlo simulations to do project planning and gather estimates. It's successfully been used for project planning large complex projects like building nuclear power plants, or projects that NASA or the Navy or similar have done.
The approach is slightly different then the article above describes. Instead it has each engineer go through calibration exercises until they can fairly accurately produce 5th and 95th confidence interval estimates. Then each engineer provides 5th and 95th confidence interval estimates for each item that needs to be worked on. Those confidence intervals are kept separate. You can then run a Monte Carlo simulation where each piece of work is weighted randomly assigned to each engineer that provided estimates for that particular item, and randomly picks a number based off their provided confidence interval estimate on how long it ends up taking them to complete the item for this particular simulation.
I was on a small team that used the above technique. We were in a large company trying to launch a new product, and given manufacturing lead time, and seasonality of the market demand, it was very important that we could provide a good estimate to the business when we could have the software portion of the MVP completed. The business provided us with what they thought the MVP features were, we further added in engineering tasks that weren't business facing, but needed to be completed. We confidence interval estimated those, and then also confidence interval estimated our personal vacation days, sick days, as well as a bucket of "unidentified work". The 80th percentile Monte Carlo simulation put us out a little more then a year. Our actual delivery was off by only a week from the Monte Carlo tp80; I don't remember in which direction, but it wasn't consequential to the business.
> it's hard to find a project from years ago which doesn't "just work" with `go run`
That's not been my experience. On a team I was on, even code from six months prior would sometimes be difficult to compile. They keep changing how GOPRIVATE works, or how modules work, or how vendoring works with modules.
They had the limbo language in plan9, which has similar features and quirks. It seems most probable that the outcome of the 45 minute conversation wasn't "let's build a language from scratch", but rather "let's iterate on limbo".
+ By declaring a field/variable []Thing vs []*Thing you get different for loop semantics. Way to easy to think your mutating the array item, but only mutating a local copy or vice versa. If you change the field/variable you need to audit all your code to make sure you haven't broken things.
+ gofmt feels way out of date. These days clang-format (c++), prettier (typescript), black (python), scalafmt (scala) take care of wrapping/unwrapping lines such as function definitions or function calls. They basically cover all formatting needs so you never have to manually format anything.
+ Scope of element in for-range loop isn't right, so capturing that scope in a lambda does the wrong thing with no warning.
+ Encourages use of indexes; which is error prone, most modern languages allow writing most code without needing indexes using map/filter/reduce or comprehensions.
+ No help from type-system for use of pointer without nil check.
+ Very easy to get nils in places one would hope to be able to prohibit them in. EG Using pointer as a poor man's unique_ptr<> means that I also get optional<> symantics (without the type checking) when I don't want or expect such. Also allows for aliasing when I don't want or expect such.
+ Difference between '=' and ':=' is silly, especially since ':=' can be used to reassign values. Even more frustrating that ':=' creates shadowing in nested scopes, so doesn't always do what one would expect it would do, such as accidentally creating a shadowed 'err' that doesn't get checked.
+ if/switch should be allowed to be expressions, allowing much safer single-expression initialization of variables, rather then requiring default initialization and mutation, which is much easier to get wrong.
> By declaring a field/variable []Thing vs []* Thing you get different for loop semantics. Way to easy to think your mutating the array item, but only mutating a local copy or vice versa.
The loop semantics are always the same, the element variable is a copy of the value at the current index. Yes that means that if the value is a pointer, the copy of that pointer can be used to modify the pointed data. This is something a good Go programmer should understand well because this behavior goes way beyond for loops. For example functions - func (t Thing) vs func (t * Thing) - with the pointer version the body of the function can modify the pointed data. Side-effects! Just like the for loop.
> gofmt feels way out of date.
I like to think of it as stable. The biggest strength of gofmt is how universal it is. Everyone uses it and all code out there looks the same. There's a growing collection of Go code out there. If gofmt would keep changing its style, then all the already published code would no longer match the standard. Thus, I think there needs to be a real strong reason to modify anything about it.
I actually like Go's simplicity a lot, it was very easy for me to learn and very easy to programming with it because of that. But I agree strongly with some of your points. = vs := specifically feels like a plain mistake, and even against Go philosophy of having just 1 way to do things.
I think any new language should be designed around options instead of nils. F#, Rust, Zig show different ways to do this, and often any performance penalty can be compiled away.
if/switch being expressions is a simple and helpful idea, languages should allow this.
using map/filter/reduce as the idiomatic way to do things I am less sure about. This can come in handy but also would add a lot of complexity to Go, and in most languages these have a performance penalty.
its important to remember that not all programmers are interested in languages, they just want to get their project done. So being able to hop into a code base and have low cognitive overhead, because there are no mysterious features they have to learn, having quick compile times, and explicit semantics can be really helpful there. That can save you more time than typing less because of generics and meta programming sometimes.
'and in most languages these have a performance penalty' - pretty sure this is due to either bad implementation or because of additional guarantees they provide. Because fundamentally these constructs can be rewritten to be loops by the compiler, except where you're wanting to violate the guarantees they enforce (i.e., maybe you want to mutate every item in the array, rather than treating it as immutable; these won't do that). For those few situations you want to violate those guarantees, you wouldn't reach for these higher order functions. There's not really any reason not to include them except for language design ethos.
> Encourages use of indexes; which is error prone, most modern languages allow writing most code without needing indexes using map/filter/reduce or comprehensions.
Sometimes (most of the time, actually) I want exactly that. Please, take a look at cryptography libraries and try to implement them without using indexes. Horrifying. Try to create a 3-dimensional array in Erlang, for example, and try to work with it![1] No thank you. I do like my arrays and indexes.
[1] I do use and like Erlang for stuff where I do not have to use arrays though.
This doesn't contradict the post you're replying to. Most times (as evident by the wide prevalence of map/filter/reduce functions) there is no need to access indexes. Other times, practically all languages that offer these functions allow you to write a plain for loop.
> + gofmt feels way out of date. These days clang-format (c++), prettier (typescript), black (python), scalafmt (scala) take care of wrapping/unwrapping lines such as function definitions or function calls. They basically cover all formatting needs so you never have to manually format anything.
gofmt is the best formatter out there because it is opinionated. The fact that people constantly tune their formatter (if there is one) in other languages makes it a nightmare to read different codebases. As someone who used to read code for a living, Golang is a pure joy to read, you always feel like you're in the same codebase.
> + No help from type-system for use of pointer without nil check.
I do wish they had an Option type
> + Difference between '=' and ':=' is silly, especially since ':=' can be used to reassign values. Even more frustrating that ':=' creates shadowing in nested scopes, so doesn't always do what one would expect it would do, such as accidentally creating a shadowed 'err' that doesn't get checked.
I too am not a fan of shadowing via :=
> + if/switch should be allowed to be expressions, allowing much safer single-expression initialization of variables, rather then requiring default initialization and mutation, which is much easier to get wrong.
These complaints may be valid, though for me, the upsides of Go makes it worth it, depending on what you want to do of course.
Arrays/slices/maps/pointers: Go abstracts over the inherent safety-limitations in ways that do not make much sense. They may make sense from a blend of safety- and performance perspective.
Index usage: Go allows to loop over elements instead of indexes and also provides the correct range of indexes in for-loops. More functional expressions would mystify execution, while Go is more WYSIWYG of languages.
Shadowing: Yes bad, but also bad to have many layers of deep scopes for this to become problematic. Best practices of Error-variable has problems.
Initialization in if/switch: Go being a niche lower level ("system") language, it's closer to the actual physical layer. A good idea not to do too much in the same expression/line, making it easier to read and making correct assumptions.
> Index usage: Go allows to loop over elements instead of indexes and also provides the correct range of indexes in for-loops. More functional expressions would mystify execution, while Go is more WYSIWYG of languages.
Indexes are the default though, you have to explicitly ignore them if you want to use the values directly.
More importantly, there are several simple operations that simply require indexes: most error prone is trying to create a pointer to an element in a slice. The natural, high level way of doing that would be
pointers := []*element{}
for _,value := rang elements {
pointers = append(pointers, &value)
}
Which looks very nice, but does the completely wrong thing. You absolutely must use the index version of you want to do this. Same would be true if you were to capture the value in a closure.
By productive you mean having developers repeatedly manually create loops that are the poor and verbose equivalents of map(), filter(), and reduce()? Out of go, scala, c++, java, python, typescript; go is the least productive language I've used in the last decade.
I tried my hand at writing generic map/filter/reduce once, and it turned out to be more nuanced than I thought. Do you want to modify the array in-place, or return new memory? If your reduce operation is associative, do you want to do it parallel, and if so, with what granularity? If you're setting up a map->filter->reduce pipeline on a single array, the compiler needs to use some kind of stream fusion to avoid unnecessary allocations; how can the programmer be sure that it was optimized correctly? And so on. If you want to write code that's "closer to the metal," these things become increasingly important, and it's probably impossible to create a generic API that satisfies everyone. That said, I wouldn't mind having a stdlib package with map/filter/reduce implementations that are "good enough" for non-performance-critical code.
Indeed, and they are even worse than that, because Streams in Java can even be parallel streams and be processed by a thread pool. So it’s not enough to know that it’s a Stream, you have to know what kind of Stream, and if it’s a parallel Stream, what threadpool is it using? How big is it? What else uses it? What’s the CPU and memory overhead of the pool? What happens when a worker thread throws an exception? Etc. These are all hidden by the abstraction but are usually things we always care about as consumers of the abstraction.
I've seen more bugs due to the cognitive overhead of reduce than writing a for loop. And then you don't have to wonder "Hmm is this a lazy stream? Concurrent?" You just look at the code, and know.
(And I almost did a spittake thinking about C++ being more productive than Go.)
> I've seen more bugs due to the cognitive overhead of reduce than writing a for loop. And then you don't have to wonder "Hmm is this a lazy stream? Concurrent?" You just look at the code, and know.
Out of curiosity, in which language(s) were those written in?
I’ve seen confusion over reduce happen in Java, Ruby, Groovy, JavaScript, Scala, and Python.
Reduce is quite elegant for certain kinds of problems. But in most practical settings, knowing when to reach for reduce vs something else is hard to know, and everyone has different opinions on it. Personally, I like to sidestep those kinds of decisions/discussions whenever I can, because it just gets in the way of actually delivering stuff.
For loops and their map/filter/reduce functional equivalents to me are just that: equivalent constructs, two styles/paradigms of doing the same thing. Can you elaborate on why one is poorer and the other much more productive in absolute terms?
Not OP. I work with TypeScript and Go, and switching back and forth, I find it much easier to express my thoughts in TypeScript and just write them out without getting bogged down in the tiniest details every single time I want to map, filter or reduce something. Go is verbose in that way which makes me lose my train of thought because of a lot more typing, and it makes it harder for me get the gist of code because I have to actually read and not gloss over the Go loops to make sure they do what I think they do.
TypeScript/JavaScript:
const itemIDs = items.map((item) => item.ID)
Go:
itemIDs := make([]uint64, 0, len(items))
for _, item := range items {
itemIDs = append(itemIDs, item.ID)
}
My understanding is with EKS or similar you still have to manually size your kubernetes cluster, that is you have to make sure there's enough hardware instances in your kubernetes cluster for whatever scaling you'll need. With fargate you're effectively using AWS's own cluster. Your service can scale up and down, and you only pay for the resources that your service actually uses.