Fast forward to today: programming is still hard, and actually it probably got a lot harder. OOP did not sort it out. Most of the code in my field is object-oriented, and bad at that. A lot of people are saying that FP will sort it out. I am skeptical, not because I'm resistant to change, but because it is obvious to me that doing FP right is (and will be) very hard. Not that object oriented programming is easy.
Am I alone in thinking that fast forward a few years, once there is enough rotten FP code written, we will be reading people ditching FP because it's the root of all evil?
The facts are that programming is hard. Working with legacy code is hard. Learning a paradigm well enough so the code you write in it is not total crap is very hard, and requires years of practical experience if you are proficient at another paradigm, let alone if you simply skimmed a paradigm and moved away because it was too hard...
It's great that people want to move on from single-platform, single-paradigm monocultures, with one caveat: breadth without depth is shallowness.
I'd like to read people treating languages and platforms as tools and not as cargo cults. You don't read carpenters writing they'll ditch hammers for screwdrivers because the old cupboard they are fixing uses nails. You read carpenters debating the pros and cons of using hammers versus screwdrivers. And you read better carpenters that debate how cupboards are designed, because it is ultimately more important than whether they are glued, nailed or screwed.
The problem with this carpenter analogy is that it simply doesn't scale - a carpenter is not going to build a skyscraper. To build skyscrapers we need engineering - the practical application of science - something which is completely missing from our field. We call ourselves software engineers, but we're really software masons - we can do good work in small quantities, but we're terrible and building big structures - which is where many of our software problems lie.
You can't build a house on some land, and later turn it into a skyscraper either - because the foundations are perhaps the most vital part of the structure - they need to be designed with some knowledge of the size, shape and mass of the structure they intend to support. The approach taken in software development is the tacking on of new systems - building structures equivalent to say, the Kowloon walled city (unsightly and unstable).
To do engineering and science, we need math - and we have no sound way of modeling imperative languages/programs in ways that makes them useful as mathematical concepts. FP is math - so building up concepts in these languages is providing a richer set of math objects and abstractions which we can later use to build our large structures. Using FP doesn't mean we need to abandon imperative coding - it just means we should clearly mark the effects of such imperative chunks of code, so we can treat them mathematically.
Carpenters are still relevant in the construction of skyscrapers - but their responsibilities are only a small part of it - they're given clearly defined boundaries of when and where they should be working. This is really how we should be doing software - we need engineers and architects to build structures, using science and math, then assigning isolated environments for the "masons" (e.g, junior programmers) to work in - in such a way that a mistake by a junior programmer cannot bring the entire skyscraper crashing down. (Turns out this was understood in the 70s, because Unix pipes and processes are still the best approach we have to this day).
Of course, this doesn't mean it should be "my Coq is better than your Twelf" - we need to separate the math from the textual representations and even the execution models.
In-so-far as your claims are true and germane to FP, they merely re-enforce this critique. After all, doing math right is very hard.
> and we have no sound way of modeling imperative languages/programs in ways that makes them useful as mathematical concepts.
This simply isn't true. There exist sound logics of imperative programs, which can be used to explore mathematical concepts.
This also pre-supposes that imperative programs themselves (and machine models more generally) are not an interesting mathematical concept.
> FP is math
It's entirely unclear what this means, if anything. You probably mean to say that certain functional languages correspond to certain logics. But that's not terribly meaningful; you can establish similar correspondences with imperative languages.
And even then, logic is math does not imply that math is (just) logic. So even if you're granted this point, the rest of your argument doesn't follow.
From a more empirical perspective, plenty of mathematicians do great math without knowing anything about FP, and plenty of FP programmers write a ton of code without ever doing interesting math.
Finally, the vast majority of very mathematically informed programming is still done in languages like C and Python and Java. So I'm highly suspicious of the claim that we need programming languages which are close to foundations in order to do mathematically informed programming.
- automatic memory management by default
- testing by default
- distributed version control by default
All of these make you more productive in a typical project. (And should be turned off when not, that's why I say, by default.) I suggest that the following are also unalloyed good, and will make their way into more and more languages over time: - functions as first class values
- immutability by default / copy-on-write semantics by default
- side-effects only possible when declared (eg IO Monad)
- powerful static typing by default
(We already see in eg git and some file systems that even when the implementation is imperative, immutability makes concepts simpler. Immutability by default virtually requires automatic memory management.)On the other hand, with OO you are always just a few more lines of code away from shipping. It's popular because it provides easy abstractions over data (rather than operations), but as we all know it causes unnecessary coupling and broken abstractions if you aren't careful. Imperative programming (not just OO) presents a lot of problems for concurrency, which is perhaps the biggest problem in the coming decade.
OO can be done right, but when it is, it's just as "hard" as FP. In fact, a lot of good OO code is functional in nature.
What is tempting about FP is that it will make programming hard enough that bad programs won't ship. Is proponents of FP think that programming should be hard. That it should require a lot of thought up front, for every line of code.
In a good lang and framework, a complete program is a good program. This idea is likely not very welcome in an industry where deadlines are always more important than quality.
You read the story about how Linked In, Facebook, LMAX and you dream of applying that.
That won't work. Those companies decided to go with their current infrastructure after their previous one failed miserably and threatened their billion making core business.
Real life for most developer is a lot duller. Very often you will have barely enough to do what need to be done. Consistency, code gardening is difficult to justify a budget for until the house is on fire. No matter how good the pattern you use, the code will be shit if you don't have time to maintain it properly.
The patterns I know of, e.g Gamma/Beck/etc "Design Patterns" are pragmatic and useful solutions to common architectural problems.
And no, they are not "only for languages without first class support for some features", as some think. Or, actually, some of them are, others are useful regardless of language. Heck, a lot of them came from Smalltalk, a language which is expressivity wise miles ahead compared to "modern" languages like Go or Java).
That some people abuse them is not an inherent problem in them. Other people abuse macros, or gotos, or functions (the 100000 line function monstronsity) etc.
I really like how you put that, it basically summarizes my own opinions on programming languages and development methodologies, in more or less the most concise way I can imagine ;-)
I'm totally oblivious to FP languages, but from CS theory I remember they are not always fun and joy to work with at all, at least not for a sizable class of practical (real-world) problems, and often require you to build these crazy hard-to-follow mathematical abstractions/contortions to be able to do things that are downright trivial in other languages. Sometimes you actually want to have mutable state and the problem you are modelling does require you to allow side-effects or explicit synchronization.
From my years of experience with programming languages my conclusion is that whatever paradigm you can come up with, some problems will be hard, and some will be easy, but no matter what, you will still need to know what you are doing and tread carefully. IMO the best language is not one that is 'safe' or 'strictly [insert programming paradigm here]', but one that allows you to do whatever you like but at least provides you with the tools to 'do it right (tm)'. From there it's all up to the developer to actually use the available tools correctly.
This may be a little unsympathetic to unexperienced developers, but I don't believe in programming languages that are supposed to make programming 'easier' or 'more accessible'. Allowing you to write safer code is invaluable, but IMO it should be up to the developer to ensure he/she uses the tool correctly.
Doing FP right is hard, but not for everyone. Library writers have the hardest job because FP (especially pure, statically typed FP) forces you to plan ahead of time instead of cobbling things together. The reward of doing this is the ability to make very robust, stable, easy-to-use domain-specific languages that make it hard even for novice programmers to screw up.
Today a lot of IT is still take a set of inputs perform some operation on it and output it.
Uncontrolled side-effects are the real issue behind most of the accidental complexity that we are seeing. We all badly need to adopt more abstractions and techniques from functional programming.
Also, changing languages or idioms doesn't necessarily help with the exposed problems. We also need a change of mentality in how we are doing software development. Lets face it, when we need to do something right now, urgent, that should have been done yesterday - no matter the language, no matter the abstractions or idioms involved, we are bound to do stupid shit - because there's accidental complexity and then there's inherent complexity and nothing saves you from inherent complexity other than thinking really well about the problem at hand and splitting it into simpler, more manageable parts.
This is also why TDD is a failure and complete bullshit in how it is advertised. Tests don't save you from doing stupid shit. Tests don't tell you whether your architecture is any good, they only tell you if your architecture is testable. Tests don't prove the absence of bugs, they only prove their presence. Tests only tell if you reached a desired target, not what that target should be. And perhaps most importantly since this is touching the core of their purpose, when uncontrolled side-effects are happening in your system, tests are a poor safety net - anybody that had to deal with concurrency issues can attest to that.
Agile methodologies are also trying to paint a turd. Yes, we should deploy or publish as soon as we've got something to publish. We should pivot a lot. We should communicate more with the end-users or within the team. And so on and so forth. But it's an indisputable fact that some problems are hard enough that they can't be solved by puking code and tests in a matter of hours or days, or by adding more people to the team.
This is so painfully wrong. Static types are not in the way of modularity and type correct programs are not hard to adapt to another type correct program.
I would say it's the exact opposite. Types are perfectly modular and compose beautifully. Adapting your program's design in a type safe environment is a breeze, because your compiler/types more often than not tell you exactly what you can and cannot do.
And also, imagine trying to build a system that works like our body does, particularly fascinating is the process of wound healing: https://en.wikipedia.org/wiki/Wound_healing
You know, there's a reason for why Akka's actors, a library built for a fairly static and expressive language (Scala), are dynamically typed. Try finding out why that is.
Why do people think tests are for that? Tests are basically same as free climbers safety lines. They won't protect you from everything and sure they are tedious to place, but once they save your tush, you'll be glad they were there, and you weren't splattered across the floor.
I've seen lack of tests in practice and it's not pretty. Nope. Not pretty at all. Basically lots of entangled undocumented, untested systems that you can't refactor because your refactor just broke some code somewhere.
To demonstrate some of the bugs, if you accidentally type your username wrong, the whole server crashes and resets. It was not pretty.
I never claimed that they aren't useful.
> once they save your tush, you'll be glad they were there
Of course, I'm glad when I work on well tested codebases. But I was speaking about the advertisement that it received. And people really do think that because that's what TDD enthusiasts claimed - and note that I'm making a difference between testing and TDD.
Particularly funny is this story on Sudoku solvers: http://ravimohan.blogspot.ro/2007/04/learning-from-sudoku-so...
The introduction is genius and I quote: "Ron Jeffries attempts to create a sudoku solver - here, here, here, here and here. (You really ought to read these articles. They are ummm...{cough} ...err.... enlightening.) ... Peter Norvig creates a Sudoku Solver."
Absolutely. So many people seem to aggressively argue for 'test all the things!' or 'test nothing!', where I imagine most jobbing programmers are practical enough to test the key things first and then expand from there. If I'm greenfielding something, I'll do TDD-first, but on new features on an old code-base or legacy apps it's initially way more just a catch in case I come off the mountain unexpectedly. I don't want to spend a day writing a feature and then find out I've broken something fundamental, I want to find that out after a couple of hours.
Instead of writing six different very similar functions that implicitly do the same thing but on different types, we write a single function with generic type parameters. Instead of writing a bunch of nested for loops working on a bunch of mutable state in an imperative way, we use map, filter, reduce, fold, etc to explicitly describe the transformations we are trying to accomplish. Instead of allowing our code to crash when bad inputs cause an error, we use asserts to declare pre- and post-conditions. And so forth.
TDD style tests have their place, but they don't do all that much to make the implicit into the explicit. If you use tests as a source of truth or a form of documentation, you are essentially saying your specification doesn't actually exist except as some weird emergent phenomenon - and then you have to take the time to disentangle the assumptions inherent in the tests from the assumptions inherent in the choices of test inputs and outputs. Tests can, by their nature, only exercise an infinitesimally small fraction of the possible combinations of inputs and state transitions that your code can go through. So tests, which serve to validate the code, need themselves to be validated. How can we even arrive at the intuition that our tests are covering some useful sample of this gigantic state-space? Metrics like code coverage and the size of a test suite are only meaningful if you assume the tests themselves are sound in the first place.
On the other hand, people pay first of all for working solutions. If you keep building robust and simple solutions, your managers will see it eventually - even though many managers don't recognize design talent when they see it, for fear of having to increase your salary.
Or the incompetent ones aren't seeing it because their metric is in the lines of code written, whereas a good developer always avoids writing unneeded code - this can be extended to other areas as well - people really good at solving concurrency issues for example, avoid concurrency issues like the plague they are. If that's the case, then it's time to search for something better. I live in Romania, our situation is similar, but I discovered that I have no problems in finding interesting work remotely - I stopped doing that because it's boring for me to not have colleagues nearby and found a small local company that's pretty cool. But yeah, you don't have to stay within the local market if you can't find something you like.
> break functionality into lots of small objects > use immutabile objects as much as possible (e.g. using thin veneers over primitives or adamantium)
are the guidelines that I'm using in C#.
> separate business logic into collections of functions that act on said objects (service objects) minimize mutation and side effects to as few places as possible
How does separating out functions minimize mutation?
> thoroughly document expected type arguments for object instantiation and method invocation with unit tests (mimickry of a static type system)
Yet another argument for static typing...
The effect is still the same though, your code is just the same old Rube Goldberg machine.
Plenty of Object-Oriented Languages do not allow you to rewrite existing code in other modules like this. In fact, most of them don't. Encapsulation is a hallmark of OOP, and it's typically non-OOP languages that allow you to do things like this. Just because Ruby says to you "I'm OOP", and you don't like a feature of Ruby, does not mean you do not like OOP!
Rust has a nice solution to this problem: You can modify an existing class, such as integers, but that modification only applies within the scope of code where you apply it. It can't bleed out into all code that runs after the patching.
On a large scale project, I understand the need.
In some cases automated tests are helpful, but not everywhere.
"Sick of Ruby, dynamic typing, side effects, and basically object-orientied programming"
He's sick of OOP in Ruby, not of OOP in general. Ruby's dynamic typing means you have one type, "the dynamic type". The compiler leaves you entirely on your own as static analysis is impossible. It means many more tests, assertions, and runtime error conditions. Most OOP techniques have evolved out of, and rely on strong static typing, Ruby isn't that kind of language.
OOP in Ruby is the inevitable result of many Ruby projects growing in scope and size. When you reach that point, a better way is to move part of the project out of Ruby and into a language that can handle that scope and size.
P.S.: Any communication with the outside world is a side effect (ex. network, file, database I/O), so sick of it or not, you better get used to it. Haskell masking side effects as monads through a language loophole isn't making things significantly better.
Eh what? Even as a Haskell non-user, this just doesn't make any sense.
a) Haskell doesn't "mask" side effects b) The fact that monads are involved is completely irrelevant c) It's not a language loophole
b) see above - because monads as a type class exist, a function may be written with a type signature that does not indicate in any way that it will be eventually used over IO.
c) the fact that the Haskell denotational semantics are entirely silent over what happens at run-time with the IO monad is definitely a loophole. From a birds view, a Haskell program is a giant expression that reduces to an object of type IO, and what happens with that object is entirely outside of the language semantics.
Sounds like the 'other peoples code' problem to me.
Just like all non trivial abstractions are leaky (Joel) I guess all non-trivial applications are 'hairy'
Sure the objects or classes where probably not perfectly designed or the choices of the correct objects/classes to begin with, but that is the problem with OOP, you don't know if the main building blocks you are using are the correct ones.
Come on people, FreeBASIC has really great options for object-oriented programming and could use your skills.
"Since the only computer language Richard [Feynman] was really familiar with was Basic, he made up a parallel version of Basic in which he wrote the program and then simulated it by hand to estimate how fast it would run on the Connection Machine."
http://longnow.org/essays/richard-feynman-connection-machine...
What were the projects and discoveries for which the connection machines were clearly essential?
Are there connection machines that are still in operation?
What happened to the old ones?
Does Danny Hillis now keep a cluster of GPUs running somewhere instead of his old hardware?
Would connection machines be more useful now than in the past?
Is it the right time to boot up thinking Machines 2.0 ?
I use a mixture of OOP and FP day to day (mostly with the Java/Scala/Clojure family) and have to say that both have their place. In large projects I appreciate OO design patterns for clarity and flexibility (though maybe that is just because it is what I'm used to), and FP's mandate on immutability for the same.
Finally I have grown to whole heartedly share the author's dislike of dynamic typing. I find Scala, not its more "pure" FP cousins Clojure and Haskell, to provide the most productive balance of the above.
Anyone else like me: tried both and ended up walking the middle road?
The other point of the article is that dynamic typing makes code rot. I am not convinced that static languages do any better. Code rot is not a problem of the typing system, it's a problem with the programmer writing the code, or the environment he's in. Let's see what happens when he inherits a 5-year-old Haskell codebase.
That said, I think the OP will do good in learning other languages. That always helps.
It doesn't until you encode the business rules in the type system. But in a well-designed language that's actually fairly easy.
> The other point of the article is that dynamic typing makes code rot. I am not convinced that static languages do any better. Code rot is not a problem of the typing system, it's a problem with the programmer writing the code, or the environment he's in. Let's see what happens when he inherits a 5-year-old Haskell codebase.
Having done both, it's far easier to port an old statically-typed codebase to newer versions of its dependencies than to do the same for an old dynamically-typed codebase. Compare a Wicket upgrade (seamlessly smooth, and the compiler will catch anything you've missed) with a Rails/Django upgrade (batten down the hatches, hope you've got high test coverage, and even then you'll likely have something break).
The siren song of static typing is loud today. Robert Smallshire has given convincing -- and even evidence based -- arguments that it increases development time while catching only a very small percentage of bugs:
So rewriting now... In F# (and C# where handy). I like Ruby but for projects like this I have seen it fall over too often; most Rails dedicated companies I know deliver great projects but they don't have to do long term support. I would like to see how that would work out as I see companies in the wild struggling to find devs willing to support their codebases. This is not a gripe with Ruby/Rails per se, but (in my experience!) Python programmers who do large Django (or other frameworks, but I encounter mostly Django) projects are more disciplined so less goes haywire, there are too many people who can do JS, so you'll always find people willing to work on your crap. Ruby is in a niche but popular spot; hard to find coders, some coders are not so good and yet enormous projects are created in it.
However occasionally amongst the muck, a beautiful and elegant thing pops out and makes it all OK. This event is getting rarer for me as our product evolves though.
We created OO, at least the C++ version, in part as a way to create boxes of code -- a reusable module system. But what happened was that we created a huge stinking pile of mutability and hidden dependencies. If I'm looking at a method in an object that takes one parameter, I literally have no freaking idea what the current state of the object is, what the current state of the parameter is or might be (and let's assume the parameter is itself an object or graph of objects). In fact, it's impossible for me to reason about what the hell I'm looking at. That's why we are forced to use the debugger so much.
Pure FP takes that all away. I have data going in, I do a transform, I have data going out. If my data is clean and my transforms are broken down enough to be understandable? It just works.
We keep trying to bolt on solutions like TDD to a fundamentally flawed model of development. Damn, I hate to say that, because I love OOA/D/P. I'm not giving it up, but my current programming practices consist of using OCAML/F# and pure functions to begin with, then "Scaling up" to objects as systems get more mature. If I've got a big closure, I'm probably looking at an object. So far I've found that scaling up is not necessary. I get more mileage from composing my functions into command-line executables, a la unix, than I do sticking everything together. But that could change.
It's right to be discouraged. There's something deeply wrong here. A big change is coming to software development.
I have a Rails 3.2/Ruby 1.9.3 application that will be hard to port to Rails 4/Ruby 2.1 because of that. The point of the original post could be that writing that application in Haskell would require less work on tests so we could have less technical debt by now. Maybe. But when integration tests break (originally run with Selenium) because the UI changed so much and there are many new functionalities to test, I don't think the language can help. It's back to having enough budget.
My favourite example is the 'Utility' module that almost every project ends up with at some point. Why would that ever be an object? In C++ you'd open up a namespace and trow a bunch of free standing functions at it. Doesn't have to be more complicated than that. Classes are supposed to be one method of abstraction (among many others) that programming languages offer to us to structure our code.
The real problem with OOP aren't objects though. It's inheritance. Complex inheritance graphs are probably the best way to couple supposedly independent parts of your code as tightly as possible. And they're notoriously hard to wrap your head around. I guess a good example are component based scene graphs (again, in C++). Whenever you're implementing some sort of graph, chances are you start by writing a class called 'Node'. That's fantastic as long as you stop the chain there. Each individual object in your scene should be a subclass of Node that has any number of independent components (mesh component, audio component, AI component, what have you) attached to it. Favoring composition over inheritance is always a good idea as far as I'm concerned and I'm happy to see languages like Rust adopting it.
I don't want to get into the whole TDD thing right now. All I'm gonna say is that assuming your code didn't break anything because all tests passed is a risky business and testing JavaScript UIs might not be as useful as you think. Having said that, Cinder (http://libcinder.org) had a bug in it's matrix multiplication code not so long ago that could have easily been detected with unit tests.
It was the WordPerfect 6 something language, replacing the WP5.1 macro language.
I had built a perfectly reasonable autocorrection engine, it handled typos, misplaced commas and periods, adding accents, and a few things more.
The Object Oriented macro language in WP6 removed any possibility to make my macros work, in exchange for adding silly graphic buttons.
Now I like C++ and D, but they are not 'pure OO'. Therefore I hate Java.
I find that an equally reasonable way is to be able to add new language constructs to make this reasoning far more (humanly) tractable. You can do this with functions, and objects and so on, many a times, but without proper macros, you may lose far too much in terms of performance (in a dynamic language).