Hacker News new | past | comments | ask | show | jobs | submit login

I think what you are describing is a replacement of widely known constructs (if, for etc) with an unknown API.

If this API is not understood then the code feels convoluted.

Therefore such projects have to contain a document that will explain the most common usage of the API to the newcomer. I think that this would remove most of the confusion.

Naturally it would be good if we had one most widely standardized API that most of the people are familiar with (like they are with if and for).

> If this API is not understood then the code feels convoluted.

I think there is more going on here. For the sake of eliminating an if, you are lifting everything else into what is effectively a separate language with the original code embedded in that language. Overall, that doesn't look like a win to me.

After all, what is the actual domain logic? Is it flatMap().map().flatMap().map()? Or is it validate().businessLogic().generate() ?

That doesn't mean that what is going in isn't useful, but it seems to me we need to have a way to specify the lifting without writing it down everywhere, so that the actual code can be expressed at the base level again.

I think replacing common usage patterns of generic control structures with named constructs adds clarity:

    for(blah blah blah)
tells the reader nothing more than there's going to be a loop, but

    map(blah blah blah)
tells the reader one sequence is going to be transformed into another by applying a function to each item. That's more informative to the reader if used for its intended purpose. It has the opposite effect if abused to repeatedly call a function mainly for its side-effects.

All programming techniques should be viewed as means to write code that's some combination of readable, reliable and performant, not as ends themselves.

> map() tells the reader one sequence is going to be transformed...

Except in this case it wasn't actually a sequence, but an Optional/Either and it wasn't about transforming that sequence but about error handling. So in a way the code wasn't even not intention-revealing, it was actually deceptive.

No, those functions were transforming the wrapped value. If there were no need to transform anything, you wouldn't need map or flatMap.

Not sure what the "No" is about, because the rest is a restatement of what I wrote. Yes, they are transforming the value that's wrapped in an Either/Optional, just as I wrote, and yes, the map/flatMap is necessary due to that construction.

However, just as I also wrote, that's not an actual sequence of values, it's a single wrapped value (or none).

The "no" was probably aimed at the fact that you were splitting hairs against their use of "sequence".

Optional can generally be thought of as a "sequence" of one or zero values. You could use an array of "[some]" or "[] /* none */" to manage control flow in an almost identical way (you'd just be adopting and managing an API that doesn't statically ensure that these arrays never have more than 1 value).

"map/flatMap" are predictable functions that can be used to manage the transformation of data (which is generally the entire point of code). The type/context that a computation is lifted into communicates a lot: Optional, for example, can be returned for a computation that may or may not return a value, and it can be "mapped" into another value by a pure computation that _always_ returns a value, or "flat-mapped" through a computation that may produce another nullable value. Anyone familiar with these basic functions can jump in, read the code, and generally know that every computation lifted into "map" can't fail, while every computation that is "flat-mapped" can. And in static languages it's all checked by the type system! No testing for "null" everywhere!

Right. I find that the easiest code to read is code written in a data flow style such that it's a series of transforms on data.

I'm not a functional purist, but I find that the functional 'principles' that I benefit the most from, in any context, are 1) massaging my data into the right shape with as few transforms as possible/practical/legible, 2) using pure functions whenever possible (in practice this often means accepting a few extra parameters in lieu of writing the code as a method; avoid 'this' in js, for example), and 3) starting with the data structures and seeing my app as, to a large degree, just a set of transformations on that (which usually is appropriate for the type of stuff I build).

I don't use partial application/currying that much, but more and more over time.

All in all I feel that doing these things have greatly improved my code and efficiency, and I'm kind of waiting for some downtime to get back into learning the more 'hardcore' functional languages.

That's about where I am. I like the idea of being a purist and see the potential gains but practical reasons such as needing to deal with other humans as well as my own lack of intelligence keeps it to this sort of midrange level

I don't think the 'map' is actually telling us anything very different than the 'for'. It is an issue I have with most articles in this vein, and this one is no different: they give simple, obvious examples that do not suggest that there is much practical difference between the approaches. We could do with some articles in which the examples are sufficiently well-developed that the advantages are clearly non-trivial.

It might be said that any such example would be too complex and drawn-out for an article like this, but if so, then to me that is like the drunk looking for his keys under the lamp-post: writing an article that claims a simple example shows a profound difference, when it does not, suggests that the Emperor's wardrobe is threadbare. The thing to do is to provide links to articles where this is worked out in sufficient depth, and I would like to see some of those links here.

Even though I share your concern, at least in my own code 'map' usually does tell me things that 'for' doesn't. Because if I were filtering a list, I'd use filter or reduce, if I were picking an element from a list I'd use pluck or whatnot, etc. So seeing a 'map' means that 99% of the time I'm transforming one set of values into another set without removing or adding elements.

Sure it does. For can be used for any of the following.

* Mapping (producing a data structure of the same size/type)

* Filtering (producing a data structure of the same type buy smaller)

* Side effects

* Collecting (producing a different data structure)

If you're incredibly unlucky or foolish a for loop might be doing more than one operation at a time!

Assuming you're only using the return types, map, filter, and collect are very clear about having only one purpose.

If that's not telling you more I don't know what is.

That's a fair point about map being constrained in the top-level structure of what it returns, but what map actually does depends on the function that is being mapped, and includes the possibility of more than one operation at a time - and if your language allows side-effects it can do that too. The point is that you have to look at the 'blah, blah, blah' to know what is happening (actually, the point of my original post is about extravagant claims of what simple examples demonstrate...)

Sometimes you can use bit-shift operations instead of multiplication and division, but you probably shouldn't unless you're an optimizing compiler or in a situation where you're doing the job of one by hand. Likewise, you probably shouldn't use a side-effecting function with map.

Exactly - the mere fact that map is being used does not automatically make things better; it can be abused just as a loop can (if using a side effect in a map would be a bad idea, the reasons for it being so would presumably apply to the equivalent loop.)

A more useful question is how can map, used properly, make things better, and I stand by my claim that the simple examples of the sort given in this article, and articles like it, fail to make that case.

I think map implies a data transformation without side effects. When I read 'map' in code, that's what I'm expecting. I expect I can replace 'map' with a parallel version that doesn't guarantee the order in which the elements will be processed and not change the semantics of the program. I expect that if I want to swap in an alternate function, I only need to be concerned with its ability to handle the inputs, produce the outputs and be a logically valid transformation.

I have no such expectations of a loop until I've read and understood its body. I hardly have any expectations of a loop at all until I've understood the whole thing.

And that's how map, used properly makes things better. It communicates what kinds of things the reader should expect it to do and not do. It reduces the cognitive load of reading the program, as if, for example, you're scanning for where a certain side effect happens, you can skip the function called by 'map' on your first pass through. It shouldn't happen there; maybe it could, but it should be pretty far down the list of places to check.

I don't think you are giving yourself enough credit here: it is you who has chosen to adopt good practices, not map that is obliging you to do so.

Mea culpa: I have to admit that I have sometimes used side-effects in map to modify the members of a list returned by a function reading data from a stream.

I think the semantics of "map" are quite a bit more specific than "for". For one thing, "map" functions have a return value which is usually the same length as its argument, which is usually a sequential data-structure. On the other hand, "for"-like functions or statements cannot or do not return values. True, if you ignore the return value from "map", it usually works the same as a "for" loop but the opposite is not true. The return value from 'map' is all the difference in the world and allows chaining of transformations amongst other things.

Putting aside languages in which loops may be expressions, you (and also village-idiot) are talking about how a program does things, not what it does, and what I am saying is that the case for map, etc. improving readability, reliability or productivity over the equivalent loop-based code is not made through simple examples. I am inclined to believe that functional programming is a better paradigm, but I did not arrive at that opinion from simple examples.

> For the sake of eliminating an if, you are lifting everything else into what is effectively a separate language with the original code embedded in that language. Overall, that doesn't look like a win to me.

Whenever I write Java (or Java-like OO), I always have this exact "separate language within a language" feeling.

I'm supposedly working in a high-level, object-oriented, loosely-coupled, message-passing/dynamically-dispatching world of classes and instances; yet an awful lot of code is actually written in a separate language of "primitive values" with opaque control structures like if/then/else, for/while, etc.

Compare this to e.g. Smalltalk, where "ifTrue" is a method on boolean objects, "timesRepeat" is a method on integer objects, etc.

Opaque control structures like if/then/else? They seem to be pretty clear when used in human languages.

"Opaque" as in completely uninspectable, unknown and unknowable to the language itself; as opposed to methods, which can be discovered, inspected and manipulated via reflection.

What real difference does it make whether repeat is a method on integer objects or whether it takes an integer argument?

Because a method on an integer object would mean it works in the same way as all library and application code; and therefore can be used as-is, or avoided by those who don't want it, or replaced if something better comes along.

Having "for" not be a method on an object means it's something completely different: a magical keyword control structure, a gift from the irreproachable language designers to the lowly language users; since mere users cannot be trusted to make such decisions for themselves.

Funny, I approach it from the other side.

Is the potential of failure part of my domain? Or is it something I have to handle in order to avoid errors?

Should .businessLogic() have to handle the case if .validate() failed? Or should it only be called if validate succeeded?

Using a common API (Either is not new or novel, it's all over the place) to separate success from failure lets me write code that is only concerned with its side of the success/fail tree without polluting it with null checks and error handling that belongs elsewhere.

Map, filter, fold, etc. are pretty much standard constructs and any decent programmer should be familiar with them.

There are subtle variations in naming like Java's Map is Select in C# because Microsoft modeled its functional API on SQL, but you have the same differences for iteration and selection (the things you are referring to for and if).

For example you have 'for (int i = 0; i < n; i++)' in C, but you don't have the exact same way of doing it in Python, where you have to use a range: 'for num in range(0, n):' or you can write a 'while' loop in Java but not in Go where it's a 'for' with a single expression.

I would choose a functional version over a three level nested for loop monster any day.

Let me be the one:

I'm a fairly decent programmer and I never worked with map or flatmaps outside of examples.

I have however significantly simplified two significant code bases, made and maintained for years a webapp that users loved etc.

Very often I feel it is just people wanting to sell something and it wouldn't surprise me if many of the people who sell functional are the same who sold object oriented back in the day.

This article was actually a bit refreshing IMO.

As programmers, we often confuse the means with the ends. We tend to over-emphasise the way in which code is written and we tend to forget about whether the code is doing the right thing.

That being said, I love map/filter/flatMap. Look at them as higher level loops.

For example: A while loop is very general, you can implement any kind of loop with it. This makes it harder to understand the meaning of the loop. Therefore, when you want to iterate a constant number of times, you'll likely use a for loop. It expresses your intent better. Map, filter, and flatMap are just extending this principle to more specialised use cases. The advantage for a reader is that the one word "map" already tells them a lot about what the loop body is good for.

This is the way I see it. A for loop is good if I'm counting, if I'm applying a function to every element in a list, I should express that.

An added bonus is that null elements are sorted and that you HAVE to separate data manipulation operations. You have to filter, map and apply sorting separately, not mix all three in a triple for loop with several branches.

Maybe it is a higher level of looping and certainly looks pleasing. However when it comes to using a debugger I have not found a way to avoid a higher level of effort.

What you say is true on the JVM. However, it is important to note that this does not need to be that way. One could imagine having the higher level loops and still debug them.

Of course, if you're writing actual code, now -- what I'm saying doesn't help you much. But if you're looking at a new language, it might be important to remember: not "filter" is broken that way, it's just the implementation on the JVM.

IntelliJ's debugger has decent support for working with functional code. Also the IDE can convert between map/filter/fold type functional pipelines and imperative loops automatically.

_Any_ debugger has _excellent_ support for working with imperative code. And not everyone uses an IDE, nor should it be a requirement.

I think this is replacing a formalism closer to natural language with one that is more abstract.

It's often justified to do that. For instance, we model stuff as matrices or graphs to gain access to their mathematical properties or because we have specialized hardware for matrix operations.

The big debate right now is whether the mathematical properties of functional programming are useful enough to move further away from natural language for general purpose programming.

We probably think something like this:

  Parse and validate the request
  If that fails then
    return 400, "invalid request: " + err

  Run business logic to get a result
  If that fails then
    return 500, "logic failed: " + err

  return 200, result as JSON
So should we use a formal language that looks similar to that or are there good reasons to use a chain of map and flatMap calls to hide the branching logic?

Reading an API's documentation doesn't mean that the mental overhead when using it is gone. You still have to think about how the pieces fit together, how to get from generic examples to your specific case etc.

I can dream up APIs which will keep confusing you, no matter how long you use them (hello Android SDK!). Or which have difficult to memorize syntax (hello Bash!).

I think this has to do with implicit behaviour vs. explicit statements. Using Optionals introduces an implicit layer that is much "thicker" than even the most tricky for syntax out there. There is a strong case for keeping all involved parts as simple as possible. And this assumes there are no implicit caveats and exceptions in the overall behaviour of the type, or that any one decides to subtly move the goal posts behind the scenes five years down the road.

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact