Go proposal: new package to provide generic slice functions
github.com
github.com
[1] https://ewencp.org/blog/golang-iterators/index.html [2013]
[2] www.catb.org/~esr/reposurgeon/GoNotes.html [2020]
for x := next(); x != nil; x = next() { ... }
Not as nice as a for/range but not a real problem. With generics we can have libraries that operate on iterators like map, filter, reduce although I also think people make way too big a deal about a tiny bit of for loop boilerplate (there are legitimate reasons for generics but I don’t think small amounts of boilerplate are among them).I really like Rust as a language, it's like a lightsaber, an elegant weapon from a more civilized era. Go feels a bit more like a blaster (sorry May 4th was yesterday.)
That said, when it comes to getting things done, the blaster seems more effective. I would chalk that up to garbage collection, goroutines vs async+await, and simplicity of the language in general. That said I use both languages regularly because they have different strengths and weaknesses and one is usually a clearly better option depending on the problem and requirements.
Could you expand / explain what you actually mean here? Because I can think of multiple interpretations of this prompt which are straight up part of the stdlib, but that I can interpret this description in multiple ways shows the issue.
EDIT: I think this is it: https://news.ycombinator.com/item?id=27050861
std::iter::repeat_with(f).take_while(|elt| elt.is_some()).filter_map(|elt| elt)
Where `.filter_map()` removes the Option layer and `repeat_with` creates an initially infinite iterator from your function `f`.Long time ago, `repeat_with` might not have existed. Maybe you'd do something like this instead for that in Rust 1.0.0: `std::iter::repeat(()).map(|_| f())...`
You prefer to use std::iter::from_fn now that it exists, of course!
To be honest, making a simple iterator adaptor is quite easy too, so you could have a knack at making one you are missing - if you can't find it in std or in the crate itertools already.
And you have to, er, attack the rebels with an army of highly variable talent and training, which contains maybe one or two actual Jedi Ninjas, so maybe it's better if everyone just uses blasters and nobody cuts off their own arm.
I'd love to work in some super elegant language for my own intellectual betterment, but I really hope my next real-world gig uses something like Golang if there are more than a half-dozen devs... ever.
Rust is a really, really cool language—perhaps my favorite language, but I’ve worked in C++ shops and Python shops and I fully expect the same amount of bike shedding at a Rust shop.
It’s been a year since I last gave Rust a try, in the mean time I’ve been able to pick Go without much effort. Rust just seem like PHP, it’s fast, featurefull but not thought through.
Golang feels like the opposite, in that none of the parts are actually built to work with one another. Tuple returns exist but there are no language features that allow you to actually work with them. There's a strong type system, but you have to break out of it with interface{} and type punning to solve entire classes of common problems. Type punning exists, but go can't prove exhaustiveness of switch statements so doing this opens further gaping holes. Errors are handled explicitly everywhere, but again there are no tools to handle the repetitive lifting so every program becomes an endless series of error handling occasionally interrupted by bits of program logic. And let's not forget how badly interface pointers interact with nils: comparing a nil interface pointer against nil returns false, so a basic nil-check that works everywhere else fails to actually perform correctly when dealing with interfaces.
I know you're talking about feel here, not about process, but to be clear, while we do have a pretty open and fairly democratic process, and while anyone can propose a feature, only the language team members, which is currently five folks, can decide to say "yes" to a proposal. The team may have been plus or minus a few over the years, but it's never been particularly large.
(We generally feel this process works well to balance all these factors; we want everyone's good ideas, but we also have to maintain coherence. An open suggestion process with a closed selection process has worked well in our eyes, but everyone can feel differently, of course.)
Most of those features don’t make it to stable—or if they do, their syntax has usually been refined by the time they do.
Inconveniently, a lot of the Rust code examples you’ll run into on HN uses incubating features, because a lot of the people posting about Rust on HN are language researchers talking about how they were finally able to implement new language feature/performance optimization/safety guarantee X using incubating language feature Y; or how they think nascent library Foo’s use of incubating language feature Z is/isn’t a good idea.
All that is inside baseball. It looks nothing like day-to-day Rust code, any more than e.g. .NET CLR static analysis code looks like day-to-day C#.
(Also, there’s the code examples of Rust stdlib code, which is like C++ STL code in that you have to use arcane language features “in anger” to bootstrap the language — e.g. safe concurrent data structures must necessarily at some point rely on unsafe code.)
(Or it’s code for a Rust unikernel / OS device driver / etc, because that’s a thing you can do. But it’s a still-nascent thing you can do, that’s not yet as ergonomic as regular code that can rely on virtual memory to exist. You have to build the safety yourself in such cases, or rely on a library that does; and there are no such libraries that have yet reached high code quality + wide cross-platform applicability.)
Additionally, third-party Rust data structure/concurrency crates often contain unsafe code comparable to Rust's stdlib in complexity. (And device drivers, as you mentioned, are probably highly unsafe too.)
You probably already know this, but if you're willing to take a dependency on Boost.Iterator, then boost::iterator_facade[1] makes it much easier and less error-prone to implement a standards-conforming iterator, and the pre-built iterator adapters[2] cover lots of common cases, so you don't need to directly implement an iterator at all.
Not arguing the relative merits of C++ vs. Rust, just leaving a pointer here for anyone who didn't know about it.
[1] https://www.boost.org/doc/libs/1_66_0/libs/iterator/doc/iter...
[2] https://www.boost.org/doc/libs/1_66_0/libs/iterator/doc/inde...
I couldn't even begin to imagine onboarding a Jr Engineer to a complex Rust project. I don't even know if it is possible.
Without that, every adapter adds a function call to the yielding of an element.
If you're going functional, a list is both a functor and a monad, so you definitely need at least map and flatMap for the abstraction to make sense.
On the other hand there are a lot of "in place", "insert", etc, which encourage working with mutation.
You probably need a bit of both, but I'd like a more top-down design better.
The first reply to the thread deems "map" and "filter" borderline-useful, but "reduce" not worth it - that type of misunderstading just shows that the author (and the 60+ people who +1'd it) didn't spend enough time understading functional constructs yet.
I don't think the aim of the package is to move Go towards a more functional style. Rather, it's to provide a set of utility functions that mesh with the style of existing Go code. For example, it would be useful to have a utility to 'pop' an element from a slice, even though that's not a particularly functional idiom.
Despite our wishes around that, functional constructs do have a hierarchy of abstraction vs power, and skipping a layer is the type of design that backfires in the end, like with Java doubling down on the collection APIs without introducing monads explicitly for users.
I'm not necessarily disagreeing with you, but how has that backfired for Java? It seems as popular as ever, and I don't hear many people lamenting the lack of monads in Java.
I meant specifically the lack of flexibility in some regards because of the missing of monads and friends.
For example, we have a few newer languages, TypeScript and Kotlin come to mind, implementing the "null operator":
thing?.fieldA?.fieldB
While this is quite handy, and people unfamiliar with the Option monad will think that this new syntax is really cool, it poses two limitations:
1. It doesn't compose with other abstractions
2. I can't use this for my own abstractionsIf the language designers had chosen to take one step back and implemented it in a more generic way (in some sense, in the same as my other comment in this thread about implementing "map" without "reduce"), the feature could've still been implemented in the language, but without the limitations.
I'm not sure what you meant with this point:
> people lamenting the lack of monads in Java.
Do you mean that, because you don't see people lamenting, it's not a useful feature?
But monads don't compose with each other, do they? I thought you needed something more (monad transformers, algebraic effects, etc.) to get things to compose. Or maybe you meant something else by composition.
> I can't use this for my own abstractions
Yep. But Java doesn't even let you overload addition afaik, so being able to overload ? is pretty much a pipe dream.
> Do you mean that, because you don't see people lamenting, it's not a useful feature?
No, it's just that if something "backfired" for Java, I'd expect to hear people complaining about it.
They do if they're[0] traversable (cf literally `Data.Traversable` in Haskell), which almost all specific monads are. You'd have something like:
newtype Comp f1 f2 a = Comp { unComp :: (f1 (f2 a)) }
instance (Monad f1,Monad f2,Traversable f2) => Monad (Comp f1 f2) where
(Comp a) >>= k = Comp $ a >>= (map join . traverse (unComp . k))
Edit: 0: actually, only the inner monad needs to be traversable.[0]: https://stackoverflow.com/questions/42284879/is-the-composit...
0: in vaguely the same way that Int isn't a legitimate Monoid because you get nonsense by switching back and forth between sum and product (and min, max, xor, and, or, etc)
There is nothing preventing people from writing their own reduce() in their own utilities package, and I expect a thriving ecosystem of "even-more-functional-constructs" packages to spring up as soon as generics hit stable.
The discussion in this proposal is not about this. It's only and entirely about what the stdlib should provide, and that is much more a question of practical and idiomatic usage than it is about one's understanding of a theoretical CS syllabus.
I'm not arguing what's good and bad, nor what should or should not be in the Go language.
I do not understand the people who think that functional-esque programming is the only and best way to do things who are apparently just sitting and waiting for Go to be able to do it. Are you forced to use Go or something? This is an honest question, btw, and I am legitimately interested in answers. If the only reason or the biggest reason you're not using Go is that you can't import map-centric programming into it, why not use any of the many languages that support that rather than griping about a language where it's still ultimately going to be bodged on? Again, honest question, because there's no shortage of such languages.
My perception is that Go isn't that popular that there's a lot of people forced to use it against their will because it's just The Language.
Anyhow, even after generics drop, it still isn't going to be any fun. I expect to use a map here or there often enough that it'll be nice to have it in the standard library but there's no realistic way that it's going to become "the best way to program in Go". If you're waiting to try it out after the generics drop, I would suggest trying something else instead; Elixir, Nim, Crystal, Pony.
I am happy using Go right now when they don't have map.. but including map and not including reduce/flatten is just odd
Have you actually taken the time to write out what it looks like, as I have in that blog post above, though? It's not pretty. Any sane Go programmer is not going to want to use this for very much. (I write that carefully. I anticipate using it a non-zero amount myself. But it's not going to be a lot.)
I understand liking map/filter/reduce, even if it is the beginning of functional programming rather than the end. I don't understand absolutely demanding it in languages where it doesn't fit well and insisting on using it no matter the cost.
for go, a language that idiomizes variable names like `f`, you think that people would be more on board with fewer characters. There are many other areas where go has chosen to avoid tidying up the syntax/making lives easier in the name of simplicity, though I digress.
As for your earlier question of "why don't you pick another language," I think this is a silly question to ask. The vast majority of people working for hire are not picking their language. If you have a few hundred kloc written in go, I doubt very much people are in a position to casually introduce new languages, especially if they're trying to improve the current system.
It's not. It's more. A lot more sometimes, plus the indentation for trying to chain maps together is ugly.
I think more people who think this is going to be great need to clock some time on the Go Generics playground [1], implement some Map and Filter and MapError functions, and convert some real code to this style in real Go. It's awful. I'm probably spoiled by Haskell and how nice it makes all this, but it's awful even against normal Go code. Try it.
The problem is, the thing that makes "functional-style" programming in Go difficult wasn't "missing generics". It was, missing generics, somewhat heavy-weight anonymous function syntax, indentation rules not optimized for deeply-nested functions, a compiler that almost certainly won't optimize through very many layers of these constructs, the inability to put methods on core types and the fact the core types are missing the useful methods/interfaces like "Traversable", no higher kinded types so no monads or anything else like that, a variety of little syntax issues that make this all slick, and probably some things I'm missing. Just getting some generics (and not having generic methods that can add new generic types hurts this style too) just isn't enough to make it slick. Go would need a suite of changes to make this slick and efficient, not just these generics.
I’m writing a complex aggregator in go and the abstraction is buffered channels. There is no map or filter in sight. It is not needed.
I mean, yes, most companies have a relatively small list of supported languages, whether explicitly or practically. If someone at our company wants to write a program that does some binary data munging that's even more frustrating in Java, and needs to do it faster than Python allows, they have to use Go. We will not let them use C or C++, because most of the team will not understand it and it won't be able to integrate with any of our standardized libraries or CI infrastructure.
A program structured as a sequence of short, tight loops over vectors/slices as described is more or less hitting the performance sweet spot of modern microarchitectures.
The general form of a short loop over a vector plays to the strengths of branch prediction and the instruction cache while the temporal and spatial locality of the memory accesses are conducive to caching and pipelining.
Moreover, there's usually no need to allocate or copy the array in that sort of data flow. I mean, unless you're chasing worst case performance to make a point or something. Slice the buffers out of a pool and allow ownership of the data to follow the execution context, then you're free to modify it in place.
The above points cover 3-4 orders of magnitude real world performance.
It is true that there are some issues with the optimizer working in this pattern but I have only seen performance severely degraded in this pattern by compile time type ambiguity -- some but not all interface{} parameters, anonymous functions / closures in scopes with dynamic type. Interface types with a pointer receiver do not typically encounter these specific issues, and I believe the empty interface type has much better performance since ~1.12 or so as well.
Sure, but even faster is not to loop over intermediate arrays at all, by virtue of never constructing them in the first place when they aren't necessary.
"Moreover, there's usually no need to allocate or copy the array in that sort of data flow. I mean, unless you're chasing worst case performance to make a point or something. Slice the buffers out of a pool and allow ownership of the data to follow the execution context, then you're free to modify it in place."
That starts getting into "I'm sure someone can come up with some solution that meets some of these goals". Whatever map you're talking about here isn't one that is defined as creating a new slice based on mapping a function over the old slice. I mean, it kinda sounds like you're saying "well, if you just write a conventional for loop you can do all this in one pass" to me? Which is my point? I'm not the one pitching for lots of array creation, it's people who insist on using maps and filters in a language that, of all the major languages, just isn't going to put in the optimization time to convert them back into loops under the hood.
The sooner we, as engineers, can collectively acknowledge that we're doing things so wrong it's costing us approaching 4 orders of magnitude performance, and agree to stop dismissing knowledge of the hardware as forbidden knowledge, and abusing acceptance of reality as pointless micro-optimization... then I'm sure we'll make progress towards accepting one of the many patterns people have been advocating for upwards of a decade which do solve these problems.
One of the patterns which happens to be able to reclaim most of those missing 4 orders of magnitude performance is functional data flow, which is by unfortunate coincidence essentially the pattern you're denouncing here for its performance. It actually maps almost directly to the ideal implementation because it's ... literally expressing problems in terms of vector operations with no data dependence, which maps perfectly to a parallelized pipeline of the instruction types that achieve near optimal throughput and realized IPC.
I am not trying to be a dick but I am very passionate about this topic and I disagree very strongly with your assessments of the performance here.
> Sure, but even faster is not to loop over intermediate arrays at all, by virtue of never constructing them in the first place when they aren't necessary.
In those examples of this type of pipeline, the vectors you start with are slices of hardware device descriptor queues which reference a DMA region, and by progressive transformation of the receive buffers and composition with additional buffer regions through intermediate decoded states you produce reply buffers.
The hardware is programmed to sample only the packets of interest to this descriptor queue, and copies the packets matching this filter directly to the DMA region referenced in the descriptor it places in the queue.
With sufficiently sophisticated hardware it is possible to offload decode of increasingly large fragments of protocol logic or even entire applications.
Description of the operations in a language of common composable operations over the type of vectors of buffers allows the amount of any given application which is mapped to e.g. general purpose CPU, GPU or other FPGA/ASIC offload feature instructions to vary continuously over time or API surface at runtime pretty neatly, it creates breakpoints in the logic flow that more or less always map exactly to functional hardware boundaries, because everything's pretty much just vectors of buffers all the way down really when you think about it. Your process's entire runtime is just another vector of buffers to the kernel.
Go has a common style across most code bases I've seen. Partly that's because Go is opinionated about style, and doesn't really support doing things any other way. I don't see this as a bad thing, though that may well be because Go's style happens to be similar to my own. I have friends who learned to code on Ruby and JS and who don't like Go at all, because it doesn't really support the way they like to write code.
Adding the ability to write functional-ish code to Go is going to fragment the style. There will be code bases written in functional-ish Go that are difficult to understand for people used to a more imperative style. And vice versa (though I suspect imperative is easier to understand). I don't see this as a good thing. It's kinda "if you want to write functional-ish code, why not pick a language that you can do that in, instead of demanding that Go supports it?". But I realise this is probably a lost cause, and we're going to see that wave of "even-more-functional-constructs" become so common that we end up with JS-like library churn.
Probably I'm looking at it the wrong way, but it looks to me like it's a relatively harmless language extension with the added bonus of correctly typing stuff normally cast as interface{}.
The problem with generics is they're a hammer, and everything is going to look like a nail. Instead of being a rarity, and everyone basically writing idiomatic Go as normal and only pulling out the generics when really needed, this functional-ish style that relies on generics (as in TFA) will become common.
Devs who are used to writing array.map code in JS will start on Go and immediately find the slices package in the standard lib, and create a generic for every single slice they use, because that's what they're used to and how they think it's meant to be used. For...range loops will look as old-skool and primitive as for loops look in modern JS (despite all their advantages).
We already have the "learning path" of newbie Gophers, whose first question is always "which framework should I use?", and it's an uphill battle getting them to accept that they should just use the standard lib. Trying to get them to accept that they basically never need generics is going to be harder.
Old crusty Gophers like me, who are used to and like the simplicity of idiomatic Go, will be left shouting into the void. Again.
edit: typing stuff as interface{} is also one of those things that you learn to get past. I'm working on a ~80Kloc Go code base at the moment, and have not had to resort to interface{} once yet. Interfaces are way more powerful than they appear at first. I'm not saying interface{} doesn't have its place, and it's used heavily in the standard library, but it's not as common as you'd think at first glance.
The two big places this shows up in golang, imo, is the lack of high quality data structure libraries, and the lack of widely used standard library for routine channel management tasks.
The former is a huge opportunity cost. If I'm coding on .net, jvm, c, or c++ I have a variety of state of the art data structures available. Some of these structures are extremely difficult to develop, so the "just write your own boilerplate" approach is completely insufficient. Point to code generation templates all you want, the reality is that this problem is preventing golang from improving much past using mutex protected hash maps and google's interface{} typed btree. Meanwhile, if we look at say .net, you have a very high quality lock free concurrent hash table available that scales to massive heap sizes. This is no niche thing: the bw-tree / cosmosdb storage engine is entirely designed around the capabilities of this lock free map. It's easily responsible for 100's of millions of dollars of value to software in that ecosystem now.
The lack of simple, clear, and useful channel lifetime management functions is another rough edge that bites many go programmers daily. The many abuses of Context make this problem quite clear.
I understand the sentiment of not wanting golang to turn into the Stephanov style C++ generics. But I think that's an imaginary fear. The golang generics design is quite different and far more "gopher" for lack of a better term. The problems golang is facing due to poor type flexibility in this area are not imaginary: they are endemic.
And I get that Go lacks in some areas. That's fine. All languages lack in some areas. Go wasn't designed to be all things to all people. It's very good at writing servers in. And there's a few projects written in Go that have proven to be pretty popular and scaled well ;)
It's not really YAGNI, which Go has been accused of (a lot), with some justification. It's more that simplicity is valuable, but it has a cost. Keeping Go simple is good, but the price of that simplicity is sometimes high. As you say, there is an opportunity cost.
I still think that cost is worth paying, and I'm not convinced (yet - I may be in time) that the reduction in simplicity that comes with generics is a price worth paying for the flexibility it brings.
I really hope this doesn't happen because then you have random utility libraries that "infect" projects for what is essentially basic flow control. I would say stuff like this is best supported in the standard lib and not home rolled.
It’s not very charitable to assume that people who disagree with you must not understand your position. In particular, one person could like reduce() because it’s very abstract and can be used for lots of problems, but critics could dislike it because very high levels of abstraction impede readability. Both parties may understand each other, but they are optimizing for different things (abstraction for its own sake and readability, respectively).
I do think this is about understanding, as it's not about syntax, or what feels nicer. It's about building blocks and what comes before what. Given the nature of the proposed library, to give the users an arbitrarily less abstract construct is a design mistake as I mentioned in another comment in this thread.
For example, I remember when I needed to flatten a bidimensional array in JavaScript, perhaps back in 2005, and the internet told me that I needed to flatten the array. So I learned then that array.flatMap() served that purpose: to flatten a bidimensional array. This is of course incorrect, but it took me many years until I found the concept again and understood what were the building blocks around flatMap, and what kinds of other abstractions it allowed.
Per my earlier comment, it's only "a design mistake" if you assume that it's always better to trade readability for abstraction. This isn't always the case, and in professional software engineering it's often better to optimize for readability--rarely are high degrees of abstraction beneficial (of course, someone will respond to this with some anecdotes of the instances in which high degrees of abstraction are helpful, which is just an enumeration fallacy).
You can see this clearly in job listings looking for Go coders. Almost nobody is looking for junior level Go coders with less than a year of experience. Companies are looking for Go coders with at 2, 3, 5+ years of experience instead.
A simple syntax may provide benefits, but it doesn't seem to be the benefits most companies want in practice after all.
I have never seen a job posting asking for less than a year of experience with anything.
This is very real. We do it all the time in JavaScript:
Object.entries({...spreadMerge, ...secondSpread}).map().filter().find(x)
Not once do I think about how many things are initialized, iterated over, etc. Rarely would I do something like this in Go.
Whether it matters is a good question, but the point is true: language facilities make you care more or less about performance depending on what it does.
https://play.rust-lang.org/?version=nightly&mode=release&edi...
for _, m := range maps { ... }?In general though I think the functions used in map and filter should be total. Most languages are not expressive enough to handle errors fluidly in long chains like that, you can do it with monads but apparently everyone hates those except me.
Agree to disagree I guess.
> Combining a map and filter into one loop body tends to create specific, imperative code that doesn’t need to be specific or imperative. For one, loops require mutation to have the same effect as filter and map, and as soon as you’re maintaining state, things get confusing.
I agree that immutable, declarative code is easier to reason about in general than mutable, imperative code, but it's not a binary proposition. I posit the cost of managing the tiny bit of very-local mutable state for a for loop is a lower cost than trying to model a problem in terms of map and filter. While I can appreciate the elegance in "immutable always", I think it's too puritanical for real world software development, especially in these cases which are simply expressed as for loops.
Note that languages like Python have strong support for immutable expressions and `for` loops, and it seems very common (even idiomatic) to use imperative `for` loops for anything more complicated than the simplest list comprehensions.
> In general though I think the functions used in map and filter should be total. Most languages are not expressive enough to handle errors fluidly in long chains like that, you can do it with monads but apparently everyone hates those except me.
I think it's just that monads tend to allow for very abstract code at the expense of understandability, and the abstraction that monads facilitate is often well in excess of what a given problem requires. If you're writing code to maximize abstraction (which is a lot of fun), monads (and map/filter/etc) are great, but if you're trying to work with a team to build software to solve real problems, you probably care a lot more about readability than gratuitous abstraction and the monad (/map/filter/reduce/etc) juice frequently isn't worth the squeeze.
My workplace uses Elixir exclusively in production for our backend services, so we’re all functional programmers and we do not find that map, filter, and reduce make things harder to read, quite the opposite. Breaking up loops does not introduce cognitive overhead, it makes every step in the chain more explicit. You can call this personal preference but there’s nothing concrete in your belief that for-loops are more readable or easier for teams of programmers to maintain, many people disagree.
The other time it's very useful is when translating algorithms specified in an imperative form. I translated a bunch of algorithms specified as for loops to elixir, and it was annoying to have an extra source of complexity when comparing the implementations for correctness.
Hardly. You can absolutely have multi-line lambdas, just not multi-expression lambdas (the real limitation is that there are no try/except expressions so certain fallible manipulations aren't so easily expressed). Similarly, Python lets you overload operators so you could always override `|` or whatever. Of course, operators are just syntax sugar for function calls so this strikes me as a cop-out. In either case, neither of these excuses are pertinent to the community's rejection of complex comprehensions or higher order functions.
> My workplace uses Elixir exclusively in production for our backend services, so we’re all functional programmers and we do not find that map, filter, and reduce make things harder to read, quite the opposite. Breaking up loops does not introduce cognitive overhead, it makes every step in the chain more explicit. You can call this personal preference but there’s nothing concrete in your belief that for-loops are more readable or easier for teams of programmers to maintain, many people disagree.
You could argue that any two models which evaluate to the same thing are fundamentally equally easy to understand. You could argue that monads are just as easy to understand as a sequence of statements in an imperative language, but experience suggests that people have a harder time understanding the more abstract concept rather than the more concrete concept. This shows up all over--Newtonian physics are more easily understood than the more abstract quantum physics and in mathematics "more advanced" math tends to mean roughly "more abstract".
I can only assume you haven’t done a lot of functional programming because Python is just not expressive as a functional language. Lambdas are extremely cumbersome. JavaScript by contrast has a huge FP culture and does not even have comprehensions. But it does have syntactically lightweight, multi-expression lambdas.
> You could argue that monads are just as easy to understand as a sequence of statements in an imperative language, but experience suggests that people have a harder time understanding the more abstract concept rather than the more concrete concept.
Except I’m not arguing that, because I don’t believe that monads are easier to understand, I believe that functional programming with pure functions is. This is just another restatement of the same argument you’ve been making the whole time. We onboard people who have never used FP into being productive Elixir programmers in two weeks or less. There are no monads in Elixir, nothing more advanced than recursion and function composition. My experience differs from yours. Our new programmers have very little trouble grokking how FP works. They have much more trouble understanding the Erlang concurrency model and the various primitives available, but those are unique to Erlang and uniquely powerful.
You assume wrong, but more importantly you missed the point of the snippet of mine to which you are replying—namely the expressiveness isn’t the reason the Python community rejected those features, but rather the complexity and cognitive burden.
> Except I’m not arguing that,
I wasn’t arguing that you were arguing that, I was using it as an analogous argument. We’re not going to get anywhere if you aren’t going to respond to what I’m actually saying.
But we don't do that because for the entire history of computing, we've come to realize that abstractions enable us to reason about programs at a higher level without having to be bogged down in irrelevant details and that getting those details right once lets us avoid the inevitable bugs that occur while reimplementing them thousands of times.
By absolutely every objective measure we know in software engineering
let triples = ints.map(|x| x * 3)
is strictly superior to var triples = []int
for value := range ints {
triples = append(triples, value * 8)
}
The fact that this even has to be argued is just bonkers to me.The second one is trivial to introduce bugs into, reading it requires mentally filtering out 90% of the code as irrelevant minutiae, and it's even worse from a performance perspective unless you remember each and every time to allocate a result array of the appropriate length to avoid reallocation. None of the details of allocating a result array or iterating over indices/values are important from the purpose of expressing the problem to be solved, and yet every time someone has to read this solution those things are of greater visibility than the actual business logic.
Worse, repetitive and unnecessary boilerplate like this hides bugs. Did you catch the fact that I'm multiplying by the wrong value in the golang version? Did you catch that I'm accidentally tripling the indices and not the values themselves? If you did, would you at least acknowledge that the first bug is much easier to identify in the first example than the second, and that the second bug is strictly impossible?
As an added bonus, the first one (in Rust at least) is automatically vectorized using SIMD for optimal performance. This happens even as you chain additional operations to arbitrary complexity.
If you still somehow think the second version is "better", then whatever your argument is can trivially be repurposed to say this version is better still:
int* triples = malloc(sizeof(int) * array_len);
for (int i = 0; i < array_len; i++) {
triples[i] = array[i] * 3;
}
If those details of creating a result array and explicitly enumerating are important, why isn't maintaining your own counter and directly indexing? At least the C version somewhat encourages you to allocate a result array of the correct size. if res, err := fn(...); err != nil {
fmt.Errorf("Look ma, I'm a human exception handler: ", err)
}
This is held up as a positive thing, because handling errors is explicit and good. Somehow, the Rust equivalent of `?` is implicit and bad, despite desugaring to almost literally the exact same thing and being actually impossible to forget or overlook due to the return type of the function enforcing a Result.In the same breath, devs will assert that the language is "low boilerplate" (despite 50%+ LOC being just one type of boilerplate) because Error Handling is Important. Which seemingly ignores that the important part of error handling is actually ensuring that errors are handled and not the mechanistic act of self-flagellation by wrapping every line of code in nearly-identical error handling ceremony. Which is unfortunate because golang doesn't do so great a job with the first part. It's surprisingly easy to actually forget to do something with the error while carrying on with a return value whose semantics are undefined (unlike Rust's Result, where you can have an error or a value and not both).
I would prefer my language wasn't designed around corporate bureaucracy personally.
It is possible to have company policies against abstraction and cleverness when working in more expressive languages, and this would achieve almost the same intent as Go, it's just that actually using Go is more convenient. The language is corporate bureaucracy. The whole point is what's not there. What can't go off in a weird directions on a per-project basis. What junior engineers in a high-turnover environment can't screw up. It's not by accident that the native toolchain will give you a test coverage report with untested error return branches in red. Google is just happy to pay people for that drudgery, I guess.
I fundamentally disagree with that. the only thing you know in java is that it throws an exception that's maybe caught somewhere. That's an untested branch aswell.
You do have to test cases where exceptions are supposed to be caught and handled/eaten, which I agree is meaningful and relevant in all languages.
In general your org’s policies for what to test seems like a recipe for a low-value, high-effort test suite. Reminds me of the unit tests for getters and setters in Java.
I agree, given that Go is what it is, it's a bad policy.
* it has been actually demonstrated that shorter programs contain less bugs,
* code is read much more frequently than it's written, and to the point code is easier to read than code full of boilerplate and meaninglessness syntactic elements,
* menial tasks and distractions introduce mental fatigue that consumes your limited brain resources that you need to think about the structure of your code that is actually dealing with the problem at hand.
With respect to the second bullet, I agree that code is read more frequently than it is written and that readability is paramount. I strongly disagree that minimizing the character count is the same as optimizing for readability. In particular, I find very syntactically terse languages like Haskell or Ocaml far harder to read—indeed ReasonML exists precisely because so many people find OCaml so difficult to read.
With respect to the third bullet, you should think about your error control flow. The structure of your code doesn’t change whether you’re using Go’s “if err != nil” or Rust’s ? operator. The difference is a dozen characters. I can’t believe that the real cognitive load is in the actual pressing of keys, but if that is your experience then text editors often have a “snippets” feature or extension that will let you map that boilerplate to a hot key.
It was also an oblique reference to this talk which is one of my favorites: https://youtu.be/mZyvIHYn2zk
There’s no universally correct answer to the right balance of “smart compiler” vs “simple language”. Having worked with Go on a daily basis for five years now, I really like the simplicity of the language. Compared to the cognitive load imposed by the language when using C++ and Rust, I much prefer the naive straightforwardness of Go. There’s no devilry about it.
Go vs Rust is not a fair comparison, because Rust's type system is much more powerful that Go's and enables applications that Go's doesn't (like embedded systems programming). Also some of the things I said about C++ apply here.
There are also features of Go absent from C++ and Rust (reflection, garbage collection). So it's really puzzling that you'd pick those two.
To have a fair comparison, you'd need to look at something like Kotlin sans the class system. There Kotlin is a clear winner, and with the same engineering resources put behind it, a cut-down version of Kotlin would easily be able to achieve build times in the ballpark of generic-enabled Go.
Despite its claimed simplicity, Go has inconsistent design and bug-prone unwieldy features (try to write code that merges streams of data from two channels without blocking unnecessarily). It also has unnecessary constructs that contradict its claim to include only what's necessary (interfaces are just structs of function pointers, type methods are just functions with a glorified first argument).
It may be enough to write one-off command line applications, but it doesn't give developers the tools to create complex distributed systems in a systematic, clean way, which was supposed to be its main area of application.
So basically no real example. It is only what would be possible if things were to happen certain way.
> It may be enough to write one-off command line applications, but it doesn't give developers the tools to create complex distributed systems in a systematic, clean way
This is just arrogant opinion. Distributed tools and applications like NATS, Kubernetes, Docker, CockroachDB and so many other exist and written in Go. Developers who can't write distributed systems themselves can ofcourse buy or license tools from vendors. And at that point, honestly, it could have been written in any language.
Rust long compile times are mainly caused by LLVM, and the amount of IR that is given to it.
"Lisp on Small Pieces" from 1994
https://www.amazon.com/exec/obidos/ASIN/0521562473/acmorg-20
It is not 70's, but 26 years should be enough to get hold of such knowledge.
Or "Modern Compiler Implementation" from 1998, a little more young.
https://www.cs.princeton.edu/~appel/modern/
Or rather "The Implementation of Functional Programming Languages", from 1986, now getting closer to 70's.
https://www.microsoft.com/en-us/research/publication/the-imp...
I can still have a look into my SIGPLAN paper collection to dust off some ML papers.
The large amount of IR given to LLVM is closely related to the amount of abstractions that Rust uses. It's especially relevant for iterators where a "simple" `for` loop expands to `into_iter()`, `iter.next()`, `match`, `drop`, and arithmetic expands to calls to overloadable operator methods, all moves are `memcpy` to clean up, and so on.
Speaking of which just use OCaml bytecode backend or Haskell interpreter to see how slow complex languages are.
This is what is missing to Rust and cranelift might help to deliver, alongside not having to keep compiling the whole world from scratch.
Avoiding that means doing the flattening earlier, in a higher level IR.
* GHC does exactly what I suggested rustc needs to do- it does the flattening on an earlier, higher-level IR than LLVM's. (It is also not exactly a fast compiler, though!)
* I'm not sure if this applies to F#, but in C#, LINQ compiles faster by not doing as much (if any) flattening in the first place.
It is also a problem of langage design though: JS’s HoFs are spec-defined as returning Arrays, which makes fusion way more complicated.
Internal / external iteration also have different optimisation profiles e.g. Rust initially used internal iteration, switched to external because that was much more conducive to optimisations, but re-introduced an internal supplement because some combinations / patterns optimisé much better through internal iteration.
Source? I thought conventional wisdom was that internal iteration was much easier for compilers to digest.
With https://www.reddit.com/r/rust/comments/5ez38g/rusts_iterator... as the… not counterpoint but reintroduction of internal iterators.
Linking to the Reddit thread rather that the article directly because lots of comments from important rust folks.
This shouldn't be the case. A HoF-call should be optimized into a JMP or be inlined directly. The issue of performance is more likely to come from non-mutating functions operating on datastructures which had mutation in mind.
People find a language/library too complicated, so they go on their own to write a simpler version, only to find out that they missed very important cases that prevents it from having the same qualities.
Grudgingly, they then gradually re-introduce what they initially strongly opposed against, but now they also have to work around their current design and bogus abstractions that wont let that happen easily.
That's going to create a lot of cruft, edge cases and API complications, to the point where a bystander might decide it's too complicated and, they too, go on their own write a simpler version.
Go hasn't learned a single thing.
There's a lot to love and learn from functional programming.
Borland first attempt to C++ "generics" was the initial release of Borland International Data Structures 1.0 (BIDS) around 1990, where they used the preprocessor to generate multiple copies, something like
#define LIST_T int
#define LIST_TYPE MyIntList
#include <bids/list.h>
Rice and repeat for all required types, when BIDS 2.0 came out this was already replaced by experimental templates support.Around 2005 it was common to use Eclipse EMF framework, alongside plugins to generate Java code (<= 1.4) that would create type safe subclasses from collections with Object.
So learning is a hard process, then again this was their point of view when C was created,
> Although we entertained occasional thoughts about implementing one of the major languages of the time like Fortran, PL/I, or Algol 68, such a project seemed hopelessly large for our resources: much simpler and smaller tools were called for. All these languages influenced our work, but it was more fun to do things on our own.
How do you explain the success then?
Go hasn't tried to be all things to everyone. There are lots of things out there that need simple solutions and Go provides for that with and nice middle ground of dev and computer performance.
If I want to read from one computer, write to another computer, concurrently, with low footprint, and speed, it's a great tool.
If Go has learned nothing, are Go programmers just picking something more difficult that performs worse? I've found it has dislodged services that were previously written in Node, Ruby, or a whole J2EE stack. Sure there are people out there trying to make it all things to everyone and it fails in some of those things, but it does great at others.
A megacorp funding 100 engineers to work on the compiler, libraries and tooling for 10 years
Go itself is not particularly simple, actually. It's simplistic, meaning that you have to jump through more hoops to do common things. For a concise example, consider adding an item to a slice in Go vs adding an item to a collection in pretty much any other modern language.
That's been in non-generic Go for a while (passing a closure for sorting): https://golang.org/pkg/sort/#Slice
How much are slices used in other languages? A little? A lot?
We now have a zillion open source projects. Someone could semgrep that mountain, do some analytics. Figure out if adding slices, or whatever, is worth the bother.