Hacker News new | ask | show | jobs
by rpercy 3426 days ago
Do you have an example of the community celebrating mediocrity?

My impression is of a community willing to go without some features, in order to preserve those it values. Namely simplicity, explicitness, terseness, and consistency.

1 comments

Explicitness and terseness seem a bit at odds - I wouldn't call go code very terse at all. It seems verbose and repetitive. Consistency? Aren't only some built in types blessed with generic functions?

Mediocrity? The preference for writing loops over simple maps or folds is a bit mediocre. IIRC in Tim Sweeny's "Next Mainstream Programming Language"[1], he notes that around 90% of all the loops in Unreal are folds or maps. Maps and folds are just simpler than loops, without even appealing to terseness and elegance.

Hey I know I'm in no place to judge -- the creators of go and Google overall have accomplished more than I'll ever do. I just really cannot grasp the mindset and penchant for unexpressive languages.

1: http://lambda-the-ultimate.org/node/1277

Thats not a strength of Go. Structural interfaces, channels and performant M:N green threads are its main strengths, and AFAIK there is no other language that provides a similar combination in a familiar C/ALGOL-like packaging.

If such a language existed and had generics and Swift or Rust style error handling as well as some backing by a large-ish corporation / organization, I think that language would be preferred to Go.

More expresiveness isn't always better. Examples that hit the sweet spot are C# / Swift (C# really needs algebraic data types). Beyond a certain point, you will start to lose users. Haskell is very expressive, but few will ever have the time or patience to learn enough to build their perfect monad transformer stack and take advantage of the mtl typeclasses to easily use it. Even though it does have M:N green threads, channels, STM and everything.

In fact, Haskell could probably compensate for this and beat Go by having excellent library documentation and well written, focused tutorials for the working engineer.

I feel like maybe 10 years ago no one would utter something like "C# needs ADTs" because they didn't have mainstream appeal, despite computer scientists' understanding of them.
Yes, things are moving forward, even if slowly :)
The loop and map are equally readable, it is just that some people are used to one form more then the other.

More importantly, how many character needs to be used for simple idiom like that has zero influence on how maintainable your system is going to be few months later on, how fast it is and so on. The difference between loop and map wont make you do less or more bugs. It may make you read the code few seconds longer first time you encounter form you are less used to - but then you will adjust and read it just fine.

Functions are trivially composable, while imperative loops are not. The difference that makes in terms of maintainability and readability of non-trivial applications is hard to overstate.

Writing code in terms of small, composable, reusable functions with a clear single intent (through good naming), that you can then mix, match, and reuse to compose into larger functions is what makes good functional code so much easier to write, test, and reason about than imperative code.

My favorite intro to functional programming concepts for those used to imperative coding is Sott Sauyet's Functional Programming presentation: http://scott.sauyet.com/Javascript/Talk/FunctionalProgrammin...

The presentation makes heavy use of JavaScript and the RamdaJS library in its examples, but the concepts are universally applicable to any language with the necessary functional programming primitives. It also does a great job of comparing imperative and OO implementations of a solution to a problem vs the functional implementation. I highly recommend taking a look if you're even slightly interested in why so many people are starting join the functional programming bandwagon.

So what I'm trying to say is, it's not just a matter of readability. Although if anyone still wants to argue that imperative looping is anywhere close in readability compared to functional composition for non-trivial cases (think multi-level nested loops vs composing multiple functions), then we should just agree to disagree since I don't foresee that becoming a productive discussion.

"Writing code in terms of small, composable, reusable functions with a clear single intent (through good naming), that you can then mix, match, and reuse to compose into larger functions is what makes good functional code so much easier to write, test, and reason about than imperative code."

Absolute, that holds for non functional code as well.

It just does not matter whether the smallest function inside that system of functions has a loop or a map inside it. Loops are as easy to tuck into reusable functions as maps or anything else. Notably examples in the presentation you link have functional version shorter, but harder to read - they have more features however. Even the first one pipe(..., reduce(add, 0)) just does not read fluently.

But again, this low level has very little influence on maintainability of the larger program. In anything that is not computational library, how you compose them matters more. It is as if you assumed code with loops can not be split into smaller composable units.

I worked with codebase written in largely functional style. It was hard to read at first, but then I got used to it. However, it never became all that much easier to read them loops nor easier to work with. It gets bad when people get clever with composing functions.

> It just does not matter whether the smallest function inside that system of functions has a loop or a map inside it. Loops are as easy to tuck into reusable functions as maps or anything else.

This is certainly true! You can definitely wrap all your loops in functions that take in a collection as argument, and return another collection or value, and as long as you don't produce any externally observable side effects with your loops, these functions are as good as any other as building blocks of a functional program.

Consider the age old adage: "If a tree falls in the forest and nobody hears it, did it actually fall?", similarly, "If a function mutates some internal state only visible within its own scope (a counter or accumulator in an imperative loop, for instance), did it actually mutate anything?" The practical functional programmer would answer with a resounding "no". In fact, RamdaJS itself is implemented mostly imperatively internally for performance/compatibility reasons, but exposes a functional API for the consumer. Similarly, Clojure is implemented the same way for many of its core functions, and exposes an easy way for users to perform mutations for similar purposes within the functional, immutable-by-default language using transients: https://clojure.org/reference/transients

If you use imperative loops by wrapping them in this way, however, note that you're essentially implementing the same function signature of a function that calls map/filter/reduce on a collection, but with imperative primitives, and that's definitely a valid approach. If you build your programs by writing and composing these kinds of functions, you are in fact doing functional programming, and can reap all the composability, maintainability, and testability benefits that come with it.

I don't think there's any room to debate that most developers don't use imperative loops in this way, however. And my point was that the way they're usually used was not trivially composable. Of course, you could always refactor them into small functions that are trivially composable, but that they need to be to become composable is why I prefer using trivially composable primitives like map/reduce/filter to implement my programs as a default, and optimize specific functions with imperative implementations on an as-needed basis after profiling and identifying all the critical paths (premature optimization, and whatnot).

RE: `var sumOfSquares = pipe(map(square), reduce(add, 0));` not reading fluently.

This is where we'll have to agree to disagree. To me that reads quite literally as "given a collection, return the square of each item, and add the result of each to the next, starting from 0, to return the final value". The functional approach could definitely look more intimidating to readers without any general knowledge of functional primitives and what they do, but that's an issue of familiarity, rather than one of inherent readability.

Yes, people can get crazy with nesting multiple inline composed functions inside of other inline composed functions on one insanely long line, and that can quickly get out of hand in terms of maintainability and readability, just as people can get crazy with nesting loops. But that's just a case of bad functional code that needs to be refactored using a composition of smaller, well-named functions broken down into multiple lines. Nobody is claiming functional programming is a panacea for bad code.

It is not necessarily easy to recognize when a loop is a map or filter -- I think that's the issue. It's why someone like McSweeney would bring it up.

The Python approach -- where maps and loops share a lot of syntax -- is maybe the middle way. On the one hand, it's not an approach that gives rise to `.map()`, `.collect()`, `.flat_map()` and so forth; on the other, it's marked out as something returning a result.

The preference for maps and folds looks like a fad to me. I can use them but I don't think it makes the code any easier to understand, just different.
I dunno, ruby has some nice tendencies here, like `out = [1,2,3].map(&:to_s)` which is small and descriptive[1].

Compare that to:

    out = []
    [1,2,3].each do |x|
      out.append(x.to_s)
    end
IMO that's not fad material - that's progress. Maps and folds can totally be abused to produce inscrutable nonsense, but for small, common operations they're often much more obviously-correct.

[1]: `&:method_name` is extremely-common shorthand for "call this method"

I had no idea what you were trying to do in the first paragraph whereas without knowing much ruby I can easily read the intent of the second.
That has more to do with familiarity than anything else.

A tale of two cities is an easier book for an English speaker than Les Aventures de Tintin.

Map applies a function to each item in a collection.

As pseudo-code:

Collection.map(function)

And in the parents example we have:

[1,2,3].map(&:to_s)

The only Ruby specific part is &:.

It does though. Maps and folds - and functional programming in general - focuses on the /what/. For-loops on the other hand tell the compiler / cpu the /how/.

Summing is a nice example. A naive for-loop will just iterate through the list, keep a variable around, add the next item in the list to it. In functional programming (or with a reduce), you tell it to sum in a more concise way - but more importantly, you don't tell the compiler how exactly it should do it. It's trivial with functional programming to make the task (for example) multithreaded or to use advanced underlying cpu tricks, without you as a developer needing to know how exactly it does what you ask it to.

Consider product as a counter-example. With an imperative loop, it's easy to add an early-out condition if zero is encountered. But this is harder to do in a (strict) functional language.
This is exactly why lazy evaluation is often described as control-flow. Lazy evaluation permits efficient composition, without doing extra work (although the constant factors become much larger).
Yes, that's exactly right. And Go takes a relatively strict stance on making the _how_ obvious to the programmer. It goes out of its way to avoid hiding O(n^k) loops behind language sugar like map or fold, for example.
> O(n^k) loops

I'm unclear where algorithmic complexity is coming into this. Can you clarify what you're trying to imply?

map doesn't do any magic behind the scenes, it goes through a sequence exactly once.

Can you given an example where map might increase algorithmic complexity like that where the imperative version would not?

Map hides a loop behind a statement. That's all I meant.
A 70 year fad.
Maps and folds are implicit loops. For some explicit is simpler.
Maps are explicit transformations of data. For some, making the business logic explicit is simpler.
Loops are just implicit goto...

The argument for maps and folds is the same as the argument for structured programming in general: using common/reusable idioms brings clarity and familiarity.

Terseness doesn't necessarily bring clarity.
It's the recognizability, not the terseness, that brings clarity.

Although Python's approach is not terse -- `fstyle = [f(x) for x in arr]` -- it is eminently recognizable.

Your argument is not really about anything I specifically said; and without something to counterbalance, it argues against structured programming, too.

How does it argue against structured programming?

My argument is that "recognizability" is in the eye of the beholder. One programmer's "map is a powerful abstraction over transforms, for example loops" is another's "this is some cutesy code/math creole by a CS graduate who desperately wants to find nails to apply his functional programming and applied math hammer on".

That's a fairly harsh way of saying that at some point the abstraction isn't clarifying, whether it's recognizable or not.

Disclaimer: I'm a big fan of functional idioms (they're one of my favorite parts of Rust, for example). I'm not so much a big fan of absolutism or over-generalization.

No, maps are conversions between types containing other types. Maps are much more powerful than just doing something over an array in languages that support a functional style.
The original comment was comparing maps and folds to loops, not the general application of maps.
I don't know go, can't you write a map in it?
Not a type safe one that works on your own types.
You can't define a polymorphic function map, but you definitely could definitely define a map method for your own monomorphic container type.