10 ms·
I think this most effectively demonstrates why I like a lot of OOP: it can be verbose. This is example function is relatively illegible: alignCenter :: [Stri
by biznickman 7y ago
I think this most effectively demonstrates why I like a lot of OOP: it can be verbose. This is example function is relatively illegible:
alignCenter :: [String] -> [String]
alignCenter xs = map (\x -> replicate (div (n - length x) 2) ' ' ++ x) xs
where n = maximum (map length xs)
One of the most verbose languages I've used, Objective C, has made this a best practice. Despite the brackets (which scare people off), it can be some of the easiest to read code.
- nnq 7y agoWhat is illegible about that?! It just says, in code: "replace each line of text with itself prefixed by a number of spaces equal with half of the difference between its length and the length of the longest line in the text". It's like the most obvious way to think about this particular problem! Now, how well this scales to different problems, and especially to problems where mutation is a natural way to think about things... that's a different problem and part of the reason I'm not that much in love with extremist functional programming. Mutation has its place and the monadic abstraction is something I dislike. But for simple examples like this pure FP rocks and is very readable and intuitive!
- marcosdumay 7y agoAs usual in FP code, the order of your words are completely different from the order things appear in the code. Also, most developers aren't used to concise function declarations (yeah, even with JS pushing them), what further adds to the problem. This is not something people with no experience on FP can read easily. Now, calling it illegible strongly implies that people with no FP experience are the metric one should code for, what is perfectly valid if you are at a company aiming to hire cheap coders, but not something to brag about.
- nnq 7y agoOrder of words is not that relevant, in English I can say instead of the above: "take each line of code [map ... xs] and replace it with [\x -> ...] a number of spaces equal to half the maximum line width [replicate (div (n - length x) 2) ' '] prepended to it [... + x]" All languages have passive voice and other tools so you can make the order of words be whatever you'd want for customizable emphasis while keeping meaning the same. Sure the order for arguments for `map` may not be the most intuitive, but that doesn't make things harder. See ReasonML for an example of functional programming with regular C-like syntax. Now, if you want to see hard to read functional code, look into code abusing point free style (I'm not even sure there is a fine way to use it at all...), plus mind bending types (usually to satisfy some monadic style abstractions), plus over-currying stuff all over the place. But the snippet above is only unreadable for people who stubbornly refuse to invest a couple hours of their time into learning a new notation for things! Heck, you can even translate it almost 1-to-1 to modern Javascript, or even add an extra function name and some more sensible variable names to make things more readable while keeping it functional: function alignCenter(lines) { let width = Math.max(...lines.map(ln => ln.length)); let getPadding = len => ' '.repeat(Math.max(0, (width - len) / 2)); return lines.map(ln => getPadding(ln.length) + ln); } ...the above is more like something I'd actually let pass code review in real life :)
- thibran 7y agoHaskell is very terse, but it doesn't mean that this is true for every FP language. Code below is in Gerbil Scheme and should be readable even for OOP programmers. (def (alignCenter strings) (def n (apply max (map string-length strings))) (def (align s) (let* ((count (round (/ (- n (string-length s)) 2))) (whitespace (make-string count #\space))) (string-append whitespace s))) (map align strings)) Here also the same code in JS, which is almost identical. function alignCenter (strings) { const n = Math.max(...strings.map(s => s.length)) const align = s => { const count = (n - s.length) / 2 const whitespace = ' '.repeat(count) return whitespace + s } return strings.map(align) }
- munk-a 7y agoI think this is definitely a problem with a lot of functional code styles so when working functionally myself I try to be quite verbose and explicit within functions, spacing things out in recognizable patterns and using lets to capture intermediate computation steps. I love side-effectless code, but I hate the assumption that side-effectless code is maximally perfect when on a single line - don't use variables for variable usages, use labeled pre-computations to clarify what you're actually doing to try and maintain readable self-documenting code. (all that said, the example above is... sort of a weirdly terrible one like most string manipulation tasks are, string manipulation usually has a high ratio of computation to significant design decision making)
- anaphor 7y agoI don't think you should judge it based on the syntax. With UFCS (Uniform Function Call Syntax), or a pipeline operator, it could easily be rewritten to something like: xs.map(\x -> (div (n - length x) 2).replicate(' ') ++ x) This is just pseudocode obviously, but you get the point I hope. There is nothing fundamental to FP about this particular syntax. Also to be honest, I don't find the original unreadable at all. But that may be because I have more exposure to this style of programming? FP doesn't somehow remove the inherent complexity in tasks like string manipulation.
- PhantomBKB 7y agoOnce you become used to reading it, it doesn't seem like a big deal. But Haskell has a steep learning curve if you have never used functional programming languages before so it may take a while to pick up a few things.
- non-entity 7y agoWhat are some of the better ones to start with? I've tried Erlang and F#, but the hardest part is figuring out whether I'm writing code that's too procedural.
- BoiledCabbage 7y agoIf you're looking for statically typed FP, honestly, start with Elm. You can do it in a weekend it's so conceptually contained. Then F# and OCaml are highly regarded. Avoid Haskell - worst bang for your buck on seeing returns on learning. It has some interesting concepts, but is a terrible language for learning statically typed functional programming. If you're looking for dynamically typed FP (ex. LISP), go with Clojure, or lesser rec possibly Racket flavor of Scheme.
- BalinKing 7y agoI honestly think a good amount of legibility is a matter of familiarity. I've used Haskell for a couple of years, and I didn't find this code particularly illegible (if I had to guess, this is probably more easily-understandable to me than the equivalent OOP formulation). That's not to dismiss your criticism, not at all; rather, I think that the challenge would decrease with experience.
- WorldMaker 7y agoTo underscore the point that legibility is as much a matter of familiarity as anything else: I think a big difference with an "OOP language" like the above poster seems to prefer versus Haskell has a lot more to do with the English language keyword operators that most contemporary OOP languages favor versus Haskell favoring more mathematical notation. There exist "OOP Languages" like OG Smalltalk, Self, Io that might make for better comparisons to Haskell by virtue of being closer to the simpler syntax and general focus on more "mathematical" operators. Just as there exist more functional languages that use much more of an English keyword approach than Haskell. (Some forms of Lisp/Scheme are so English keyword-forward with micro-DSLs that they may be a better example in a lot of posts like these for comparing "contempory OOP languages" and "functional languages". Plus there's all the bits of hybridized "functional languages" embedded directly inside contemporary "OOP" languages such as LINQ syntax in C#. It's interesting to me how many of these sorts of articles jump straight to Haskell.)
- allenu 7y agoI agree it can be hard to read at first, but after learning the syntax, it's not hard to read. It's also about what the language allows you to do (or prevents you from doing) that determines, to me anyway, the utility. I love Objective-C but it allows you to do a lot of things that can lead to bad code (mostly related to state and hidden side-effects). FP overall is a paradigm that helps prevent you from doing that and helps you rethink and describe problems declaratively.
- jlg23 7y agoAs a lisp programmer who never wrote in Haskell the snippet you quoted is crystal clear to me, while with explicit loops I always have to double check, because I am pattern matching the code "ah, a map or reduce was meant" but I still have to double check assignments and the abort condition (off by one, incrementing the wrong index etc).
- ThinkBeat 7y agoI write Lisp but I have no idea what /x means in the example above for instance.
- dzsekijo 7y agoThe "\" (which is "\" rather than "/", as already pointed out) is just an ascii approximation of λ (just like "u" is used to approximate μ).
- kian 7y agoIt means a 1-argument lambda, where that one argument is named x
- Jare 7y agoThat's a syntax knowledge problem, not a readability problem. \x -> introduces a lambda function with a parameter x, like (x) => in javascript etc.
- yoz-y 7y agoTo me this looks like perl golfing, but for some reason perl golfing is bad but writing extremely terse functional programs is not. Personally I like to combine both approaches. In swift I would write something like this: let l = ["abc", "ab", "abcdef", "abcdefgh"] width = l.reduce(0, { max($0, $1.count) }) let centered = l.map({ (line: String) -> String in var padding = (width - line.count) / 2 return String(repeating:" ", count: padding) + line })
- jimbokun 7y ago"To me this looks like perl golfing" I think that's preposterous and totally unfounded. The identifiers have names like "alignCenter", "replicate", "maximum" and "length". Those are all actual full English words, nothing remotely obfuscated about them. "map", "++" for concatenation, and "x/xs" for an arbitrary list of things are all idiomatic so can be kept short because they are used so often, like pronouns in English. Lastly, your program looks very similar (almost isomorphic) to the Haskell version, so I think it's just dumb you use it as an argument to criticize the Haskell version as "extremely terse".
- yoz-y 7y agoOk. My problem with functional programming written like this (I admit that this example is too short) is that once there are too many functions combining with other functions it is very easy to lose thread of what is happening. My comparison with perl is more to the point that these programs are way easier to write than read. Again, this example is too simplistic to illustrate my point, but what I like with combining imperative and functional programming is that it is easier to keep trace of what is happening by using intermediate variables that hold the state and thus break the chain of functions.
- nyberg 7y agoThat is why the importance of being terse but following the rule of not going too far with nesting and pointfree is so valuable. I don't share your difficulty to follow the flow of the program as usually the name of the function, type, and documentation say all you need to know about it. That said, I find it difficult to follow what is happening when using several variables as you're then likely doing something wrong in the complexity of the function. A function should say much with very little and control flow should be clear instantly. The only thing outside of this would be a procedure which do-notation allows one to express. Imperative programming is already within Haskell with do-notation an it's composition is that of imperative. Mixing functional with something else makes it much harder to make use of combinators and other forms which ends in more messy code (in my experience). If one could go pure functional without do-notation emulating the "C monoid" it would be rather nice.
- traderjane 7y agoBut the example you posted has more to do with whether a language or author has the aesthetics of preferring succinctness and symbols (Perl-like) to words (Ruby-like).
- uxp100 7y agoI don't really like 'where n' afterwards instead of say 'let n' at the beginning, but I'm not very familiar with haskell (really just enough to know the first line is a type signature) and I found this ok. Maybe objective C is better though.
- jlg23 7y agoMy first thought, too. OTOH, with indentation like in the example I could live with it.
- marcosdumay 7y agoIndentation is always like in the example. Haskell has either semantic whitespace of braces everywhere, and nobody uses the braces.
- nnq 7y agoYou can write code without ever using `where ...` :) It's just how mathematicians are used to express their version of "top down problem solving". It's also similar to what you do when you define a function/method before the smaller ones it calls and before the even smaller ones they call (using what's called "function hoisting" in Javascript for example). Now, if you really dislike "top down problem solving", you might have an allergic reaction to `where ...` which applies the same pattern but does it down even to the micro level of small functions of few lines...
- andolanra 7y agoI think this is a question that's orthogonal to OOP-versus-functional-programming, but is rather about programming style. Haskell programmers like being terse, but there's also nothing stopping you from writing Haskell like this: alignCenter :: [String] -> [String] alignCenter lines = let maxLineLength = maximum (map length lines) in [ leftPadding ++ line | line <- lines , let leftoverSpace = maxLineLength - length line leftPadding = replicate (leftoverSpace `div` 2) ' ' ] You could have verbose and easy-to-read functional code—you don't see this often in Haskell by convention, but you might in OCaml or in Scheme—and you could also have terse OOP code filled with single-letter variable names (less so in Objective C by virtue of the method call syntax, but I've definitely seen code like this in Java and JavaScript.)
- roywiggins 7y agoFunctional programming seems to let people lapse into "point-free style" fairly easily, which (imho) has a much greater chance of becoming rapidly unreadable. Not to say FP can't be readable as you've demonstrated very well. https://en.wikipedia.org/wiki/Tacit_programming https://en.wikipedia.org/wiki/Tacit_programming
- klodolph 7y agoThis may be more common with Haskell specifically, because Haskell makes currying so easy and has tools like http://pointfree.io/ http://pointfree.io/ (which I’ve definitely abused before).
- tel 7y agoPoint free can totally be abused. It's also, in my opinion, often very useful for clarity. It's my experience that, especially with FP code, often the important things to write down are the stages/steps in a transformation and that the intermediate outputs do not, by themselves, hold much semantic meaning. Pointfree lets you write just those steps. Moving to "point-full" coding forces you to give names to these semantically meaningless intermediates. This can sometimes just be noise. Terseness is also just shocking sometimes. It can take time to get used to it. I feel like when I read more verbose languages I skim each block of code numerous times and collect the meaning iteratively. When I read Haskell, I depend upon each name having distinct and important semantic meaning, I leverage type information heavily, and when I do read something I read each word carefully. Totally different styles, but I've found strong preference for terseness.
- hinkley 7y agoI'm finding that in Javascript, I really enjoy map() and filter(), but map() caused some friction on code reviews at one place, from someone I don't think had any FP background. The are best when they are one liners, of the form .map((val) => someFunction(val)) or .filter((val) => val.length > 1) reduce() on the other hand is always illegible to me. I get pretty grumpy when I am 'forced' to use a reduce because the code calls for it. It just looks like I'm trying to write obfuscated code. Often I'll just unroll it to a forEach. This code structure in your example reminds me of some Python code I was reading recently (Norvig's Sudoku solver). Putting the conditional at the end of the line fights the way humans process data. Quite often we are trying to filter lines of code that could be involved in a problem and saying things like "do an action (if this condition is true)" increases the amount of short term memory I have to use to process the function. That's bad ergonomics.
- roywiggins 7y agoI've been noodling with Julia recently and the dot-syntax is really surprisingly ergonomic for transforming hunks of data. It has the benefit of making vectorization easier for the compiler, but I enjoy the syntax. https://docs.julialang.org/en/v1/manual/functions/#man-vectorized-1 https://docs.julialang.org/en/v1/manual/functions/#man-vecto...
- hinkley 7y agoThat's a cool trick but I'd like an operator with more pixels for that behavior. When I used to track new languages, I ran into one where the . was implicit. You could change a member of an object from a single value to an array and it would iterate over all of the values. So you could do an information architecture that was 1:1 and change to 1:many later and things would still work.
- jimbokun 7y ago"but map() caused some friction on code reviews at one place" Someone not familiar or comfortable with "map" should not be working as a professional programmer today. Most languages now support "map" or something very similar. Try to move to a different team or different company with actual professional developers.
- kazinator 7y agoTXR Lisp: (defun align-center (strings) (let* ((maxl (find-max [mapcar len strings]))) (mapcar (op fmt "~^*a" maxl) strings)))
- AtlasBarfed 7y agoThis opinion is flameworthy and stereotypes heavily: what seems to happen in FP is that the overall community is substantially math-IQ smarter than the imperative languages. Alas, that ALSO means the overall community loses social-IQ in the process. This leads to: 1) higher barrier to entry for the general programmers, in language semantics, documentation, examples... 2) a tendency to overabstract, underdocument, and write clever code 3) write code that is obvious to the person who wrote it (at least for a few months), but being CODE is tougher for people to unpack. 4) people don't collaborate in packages, so they tend to be single-hero projects that are abandoned, and since FP code is a bit harder to parse for typical programmers, don't get adopted/unorphaned. Thus the library code beyond the standard library gradually degrades. 5) I won't say that FP is the only domain of religious zealotry in language minutae (syntax, etc), but it does seem to have a higher proportion or much louder zealots. I've been in the industry for 30 years, and while it could just be mental ossification of age, nothing seems to have changed since the usenet days with Scheme/LISP, despite the fact the toolchains are now free and downloadable, and despite some of the smartest people in the world preferring them. The fact that Rust is gaining some momentum with its fairly alien memory management is yet another example to me that the IQ barrier of purer FP (no infix, lots of recursion, etc) is just too high to surmount.
- ummonk 7y agoA list comprehension in python would be more comprehensible than either AND less verbose.
- DonaldPShimoda 7y ago> I think this most effectively demonstrates why I like a lot of OOP: it can be verbose. Verbosity is not inherently good. In fact, I think verbosity is inherently bad. Have you read much first-year programmer code? It's absurdly verbose at the cost of legibility. The real issue is clarity. Your code should be sufficiently verbose that its purpose is self-evident, but it should not be overly verbose such that your screen is cluttered with meaningless junk (see: Java). --- To me, there is a fundamental distinction between the intents of functional programming and imperative programming that a lot of these articles either gloss over or miss completely. Imperative programming is about describing to the computer a procedure by which to accomplish a goal. Functional programming is about manipulating the relationships between data directly such that you transform your given input into the desired output. If you want to understand what a program does, then imperative code is going to be better to read. But if you want to understand what a program means, then (well-written) functional code is going to be better. This is also why so many functional languages have strong static type systems: to better enable the programmer to express programs' meanings outside of the implementation. However, as others have mentioned, string manipulation is always kind of hairy anyway, so reading this function will result in understanding what it does instead of what it means (unless you just read the signature, which is actually what I do a lot of the time). My thought process of reading this particular function without prior context would be something like: - The function is named `alignCenter` and takes a list of strings and gives back a list of strings... so the strings are being centered amongst themselves. I know they aren't being centered relative to anything else because there are no other inputs to the function — which I would not know in an imperative language, where there could be hidden state somewhere. - It maps some function over the list of strings. I assume that function will do the centering. - This inner function takes a string and does something to it. Let's investigate. - We replicate a space character some number of times and prepend the resulting string to the input string. (I think the author's reliance on operator precedence is disappointing here, as it detracts from the clarity IMO. I would rather put the `replicate` call in parens before the `++`.) - The number of spaces is determined by dividing by two some other number less the length of the string. - That number is the length of the longest string. So: to center a list of strings, we indent each string by half the difference between its length and the maximum length among all the strings. This seems like a reasonable way to center a group of strings. (Notice that I did not use words like "first", "then", etc. which indicate a procedure. Instead, I have described the relationships of the strings.) (Of course writing it all out makes it seem like a longer process than it is, but in reality it probably took me 10-15 seconds to go through everything and figure out what was going on.) --- I think this isn't a great example for the author to have chosen. My go-to example of a beautiful functional solution is generating the Fibonacci sequence. A "good" imperative solution uses iteration and might look like: def fib(n: int) -> int: a = 0 b = 1 while n > 1: t = a a = b b = t + b n -= 1 return b If you were to just glance at this code without being told what it does (and if you'd never seen this particular implementation of the algorithm before), you would probably need to write out a couple test cases. Now, the Haskell solution: fib :: Int -> Int fib n = fibs !! n where fibs = 0 : 1 : (zipWith (+) fibs (tail fibs)) We can easily see that the `fib n` function simply retrieves the nth element of some `fibs` list. And that list? Well it's `[0, 1, something]`, and that something is the result of zipping\* this list with its own tail\* using addition — which very literally means that each element is the result of adding the two elements before it. The Fibonacci numbers are naturally defined recursively, so it makes sense that our solution uses a recursive reference in its implementation. \*Of course, I'm assuming the reader knows what it means to "zip" two lists together (creating a list by performing some operation over the other two lists pairwise) and what a "tail" of a list is (the sub-list that simply omits the first element). To me, this data-relationship thing often makes more sense than a well-implemented imperative solution. I don't care to explain to a computer how to do its job; I care to think about how to move data around, and that's what functional programming is all about (to me). Of course, this is just my subjective opinion, but I think there's some merit to it. I'd like to hear your thoughts!