13

After learning functional programming in Haskell and F#, the OOP paradigm seems ass-backwards with classes, interfaces, objects. Which aspects of FP can I bring to work that my co-workers can understand? Are any FP styles worth talking to my boss about retraining my team so that we can use them?

Possible aspects of FP:

  • Immutability
  • Partial Application and Currying
  • First Class Functions (function pointers / Functional Objects / Strategy Pattern)
  • Lazy Evaluation (and Monads)
  • Pure Functions (no side effects)
  • Expressions (vs. Statements - each line of code produces a value instead of, or in addition to causing side effects)
  • Recursion
  • Pattern Matching

Is it a free-for-all where we can do whatever the programming language supports to the limit that language supports it? Or is there a better guideline?

GlenPeterson
  • 14,890
  • 6
  • 47
  • 75
Trident D'Gao
  • 441
  • 3
  • 13
  • 6
    I had a similar experience. After about 2 months of pain I started to find quite a nice balance of "stuff that maps to objects" and "stuff that maps to functions". It helps to do some serious hacking in a language that supports both. At the end of it, both my FP and OOP skills are vastly improved – daniel gratzer Nov 13 '13 at 04:05
  • 3
    FWIW, Linq is both functional *and* lazy, and you can simulate functional programming in C# by using static methods and avoiding state persistence. – Robert Harvey Nov 13 '13 at 04:28
  • 1
    At this point, you should read [sicp](http://deptinfo.unice.fr/~roy/sicp.pdf). It is free and well written. It offers a nice comparison between the two paradigms. – Simon Bergot Nov 13 '13 at 08:44
  • I don't mind loosing all my OOP skills. Functional programming is awesome! – Chiron Nov 13 '13 at 09:01
  • Both C# and JavaScript are pretty functional. No point in using their OOP features excessively, beyond mere modules. – SK-logic Nov 13 '13 at 10:06
  • 4
    FP and OOP are at the same time in some sense orthogonal and in some sense dual. OOP is about data abstraction, FP is about (the absence of) side-effects. Whether or not you have side-effects is orthogonal to how you abstract your data. Lambda Calculus is both functional and object-oriented for example. Yes, FP typically uses Abstract Data Types, not objects, but you could just as well use objects instead without being any less FP. OTOH, there is also a deep relation: a function is isomorphic to an object with only one method (that's how they are "faked" in Java, and implemented in Java8, … – Jörg W Mittag Nov 13 '13 at 10:52
  • … C#, Ruby, Scala, Smalltalk etc., for example). A closure is isomorphic to an object with only one method and some instance variables (that's how they are implemented in C# and Scala, for example). An object is isomorphic to a record of closures (that's how you implement objects in ECMAScript, for example) or to a closure of closures with a selector function. Scala, for example, doesn't even *have* functions in the kernel language. They are just syntactic sugar for an object with a method named `apply`. So, a) they are not that different, and b) you can combine them. No need to choose! – Jörg W Mittag Nov 13 '13 at 10:57
  • 1
    The paper [*On Understanding Data Abstraction, Revisited*](http://CS.UTexas.Edu/~wcook/Drafts/2009/essay.pdf) by [William R. Cook](http://WCook.BlogSpot.Com/) compares data abstraction using ADTs (the preferred data abstraction in FP) and data abstraction using objects, and it gives examples of both, in a purely functional setting, in Java(!). – Jörg W Mittag Nov 13 '13 at 11:02
  • 1
    You might want to look into the general concept of "Abstraction", because Object Oriented Programming and Functional Programming are mainly techniques of abstraction. While there are fundamental differences, astonishingly they have common principles (Polymorphism, Interfaces) while other things are not necessarily common (Mutable state, Inheritance). So in fact you will be able identify cargo-cult OOP and real-beef OOP when learning more about both paradigms. – wirrbel Nov 13 '13 at 12:38
  • 3
    I think the strongest aspect of your question has to do with readability. "How much Functional Programming style is appropriate to bring to work at an Object Oriented shop?" Or what Functional features are worth a little OOP confusion for the benefits they bring. – GlenPeterson Nov 13 '13 at 15:09
  • 1
    Why are "Lazy Evaluation" and "Monads" grouped together? They're not really related. – KChaloux Nov 13 '13 at 15:30
  • 1
    Having done the same thing here's what I've come up with as easy to use in my .NET day job: side effect free functions translate nicely, one of the biggest boons is the habit of making functions return things creating fluent interfaces, these make sense to my oop colleagues just fine, and they behave like expressions. Laziness is still a mess in most OOP because it's something you can't do halfway. Monads confuse, but monad-like interfaces tend to just seem good to OOP folks though they're not sure why/how you designed it as such. Higher order functions are well accepted these days as well. – Jimmy Hoffa Nov 13 '13 at 16:04
  • @bonomo I made massive edits to save this question because I thought the part of it which is appropriate to this site was very worth saving. Feel free to edit it in a different direction, but know that it may be closed again if it doesn't fit with the rules. – GlenPeterson Nov 13 '13 at 16:15
  • @KChaloux Monads are easier to use in the face of lazy eval, Eg `>>` can't be implemented properly in a strict language since the left side may short circuit the right. The gist is, since in lazy languages eta conversion holds (modulo seq) you can reason equationally and intuitively which isn't the case in a strict language – daniel gratzer Nov 13 '13 at 16:32

6 Answers6

13

Functional programming is a different paradigm from Object-Oriented programming (a different mindset, and a different way of thinking about programs). You have begun to realize that here is more than one way (object-oriented) to think about problems and their solutions. There are others (procedural and generic programming come to mind). How you react to this new knowledge, whether you accept and integrate these new tools and approaches into your skill set, will determine whether you grow and become a more complete, skilled developer.

We are all trained to handle and are comfortable with a certain level of complexity. I like to call this a person's hrair limit (from Watership Down, how high can you count). It is a great thing to expand your mind, your ability to consider more options, and have more tools to approach and solve problems. But it is a change, and it pulls you out of your comfort zone.

One problem you may encounter is that you will become less content to follow the "everything is an object" crowd. You may have to develop patience as you work with people who may not understand (or want to understand) why a functional approach to software development works well for certain problems. Just as a generic programming approach works well for certain problems.

Good Luck!

ChuckCottrill
  • 525
  • 3
  • 8
  • 3
    I would like to add, that one can gain a better understanding of some traditional OOP concepts when working with functional languages such as Haskell or Clojure. Personally I realized how polymorphism is really an important concept (Interfaces in Java or typeclasses in Haskell), while inheritance (what I thought to be a defining concept) is kind of a weird abstraction. – wirrbel Nov 13 '13 at 12:34
6

Functional programming yields very practical, down-to-earth, productivity in everyday's code writing: some features favour terseness, which is great because the less code you write, the less failures you do, and the less maintenance is required.

Being a mathematician, I find fancy functional stuff very appealing, but it is usually useful when designing an application: these structures can encode in the program structure a lot of invariants of the program, without representing these invariants by variables.

My favourite combination may look pretty trivial, I however believe that it has a very high productivity impact. This combination is Partial Application and Currying and First Class Functions which I would relabel never write a for-loop again: instead pass the body of the loop to an iterating or mapping function. I was recently hired for a C++ job and I amusingly noticed, I totally lost the habit of writing for-loops!

The combination of Recursion and Pattern Matching annihilates the need of that Visitor design pattern. Just compare the code you need to program an evaluator of boolean expressions: in any functional programming language this should be about 15 lines of code, in OOP the right thing to do is to use that Visitor design pattern, which turns that toy-example in an extensive essay. Advantages are obvious and I am not aware of any inconvenient.

user40989
  • 2,860
  • 18
  • 35
  • 2
    I agree completely but I have had push-back from folks who throughout the industry tend to agree: They know they visitor pattern, they've seen and used it many times so the code in it is something they understand and are familiar with, the other approach though ridiculously simpler and easier is foreign and therefore more difficult to them. This is an unfortunate fact of the industry having had 15+ years of OOP banged into every programmers head that 100+ lines of code is easier for them to understand than 10 simply because they've memorized that 100+ lines after repeating them over a decade – Jimmy Hoffa Nov 13 '13 at 17:22
  • 1
    -1 - More terse code does not mean you're writing "less" code. You're writing the same code using less characters. If anything, you make _more_ errors because the code is (often) harder to read. – Telastyn Nov 13 '13 at 18:11
  • 8
    @Telastyn: Terse is not the same as unreadable. Also, huge masses of bloated boilerplate have their own way of being unreadable. – Michael Shaw Nov 13 '13 at 21:15
  • @MichaelShaw - and "harder to read" is not the same as unreadable. There is very little positive correlation in my experience between the number/severity of errors and the ratio between the operations you're doing and the length of code to make those operations happen. (even if you're right that there's a downslope once code gets too verbose) – Telastyn Nov 13 '13 at 21:27
  • I have never seen a good example of currying. All the books seem to use it to add 1 to a number instead of X. Can you please provide a good example or link to one? Thanks! – user949300 Nov 14 '13 at 04:32
  • @Telatsyn Compare the functional (OCaml)-style `List.iter print_string l` and the C-style variant `for(int i = 0; i < list_length(l); ++i) { puts(list_get(l,i)); }`. Now tell me which one has less code, which one is easier to read, which one is easier to maintain. – user40989 Nov 14 '13 at 07:36
  • @user949300 Why do you think the example you quote is bad? Is it because it does not obfuscate the idea it illustrates by artificially associating it with buzz-words and late-hot-things? – user40989 Nov 14 '13 at 07:39
  • 1
    @Telastyn I think you just touched on the real crux here, yes terse can be bad and unreadable, bloated can be bad and unreadable too, but the key isn't the variable length and puzzlically written code. The key is as you mention above number of operations, I disagree that number of operations doesn't correlate to maintainability, I think doing less things (with clearly written code) *does* benefit readability and maintainability. Obviously doing the same number of things with single letter function and variable names won't help, good FP needs *considerably* less operations still clearly written – Jimmy Hoffa Nov 14 '13 at 15:59
  • @jimmyHoffa - In my experience, programs written in a functional stype tend to have very uninformative names. There's certainly nothing inherent to FP to cause that, but that's the culture. I certainly agree that for some problems, using FP to solve them results in less operations - just as OO will better handle others (and other paradigms yet other problems). Making a blanket statement that FP is more concise is misleading at best. Focusing on that conciseness because it lets you write less code is missing the point. – Telastyn Nov 14 '13 at 16:13
  • @user40989 re: currying. Coming from an imperative background, I have seldom needed a `function add(a,b)`, and can't remember ever needing a `function add1To(b)`. That's what a variable named `delta` is for. Without more context, the example is, IMO, a very poor sales pitch for the value of currying. – user949300 Nov 14 '13 at 18:51
  • @Telastyn: The reason we want conciseness is because it can be used to write clear code. If we have the option of terseness, we can get straight to the point. – Michael Shaw Nov 14 '13 at 19:51
  • @user949300: See http://programmers.stackexchange.com/questions/185585/what-is-the-advantage-of-currying A variable named `delta` wouldn't actually do the same thing, since it doesn't carry with it the addition operation. In Haskell, it wouldn't be `function add1To(b)` it would be `(1+)`, which is short and clear and can be dropped in the middle of a more complicated expression. – Michael Shaw Nov 14 '13 at 20:29
  • 2
    @user949300: if you want a completely different example, how about this Java 8 example?: `list.forEach(System.out::println);` From an FP point of view, `println` is a function taking two arguments, a target `PrintStream` and a value `Object` but the `Collection`’s `forEach` method expects a function with one argument only which can be applied to every element. So the first argument is bound to the instance found in `System.out` yielding to a new function with one argument. It’s simpler than `BiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));` – Holger Nov 15 '13 at 09:30
  • @Telastyn No, terseness is about writing less code, not about writing obfuscated code using esoteric operators. For instance in `List.map print_endline [ "apple"; "peach" ]` (OCaml), there is less code than in the classical for-loop used in C++ to do the same job. The FP code removes all allusion to an iterator, this is clearer, as it removes, initialisation, boundary checks, increments, and does not introduce an intermediary variable. – user40989 Aug 06 '16 at 17:37
5

You may have to constrain what parts of your knowledge you use at work, the way Superman has to pretend to be Clark Kent in order to enjoy the perks of a normal life. But knowing more will never hurt you. That said, some aspects of Functional Programming are appropriate for an Object Oriented shop, and other aspects may be worth talking to your boss about so that you can raise the average knowledge level of your shop and get to write better code as a result.

FP and OOP are not mutually exclusive. Look at Scala. Some think it's the worst because it is impure FP, but some think it is the best for that same reason.

One by one, here are some aspects that work great with OOP:

  • Pure Functions (no side effects) - Every programming language I know of supports this. They make your code much, much easier to reason about and should be used whenever practical. You don't have to call it FP. Just call it good coding practices.

  • Immutability: String is arguably the most commonly used Java object and it is immutable. I cover Immutable Java Objects and Immutable Java Collections on my blog. Some of that may be applicable to you.

  • First Class Functions (function pointers / Functional Objects / Strategy Pattern) - Java has had a hobbled, mutant version of this since version 1.1 with most of the API classes (and there are hundreds) that implement the Listener interface. Runnable is probably the most commonly used functional object. First Class Functions are more work to code in a language that doesn't support them natively, but sometimes worth the extra effort when they simplify other aspects of your code.

  • Recursion is useful for processing trees. In an OOP shop, that is probably the primary appropriate use of recursion. Using recursion for fun in OOP should probably be frowned upon if for no other reason than most OOP languages don't have the stack space by default to make this a good idea.

  • Expressions (vs. Statements - each line of code produces a value instead of, or in addition to causing side effects) - The only evaluative operator in C, C++, and Java is the Ternary Operator. I discuss appropriate usage on my blog. You may find you write some simple functions that are highly reusable and evaluative.

  • Lazy Evaluation (and Monads) - mostly restricted to lazy Initialization in OOP. Without language features to support it, you may find some APIs that are useful, but writing your own is difficult. Maximize your use of streams instead - see the Writer and Reader interfaces for examples.

  • Partial Application and Currying - Not practical without first class functions.

  • Pattern Matching - generally discouraged in OOP.

In summary, I do not think work should be a free-for-all where you can do whatever the programming language supports to the limit that language supports it. I think readability by your coworkers should be your litmus test for code made for hire. Where that chafes you the most I would look into starting some education at work to broaden the horizons of your co-workers.

GlenPeterson
  • 14,890
  • 6
  • 47
  • 75
  • Since learning FP I've habitted to design things to have fluent interfaces which results in what is akin to expressions, a function that has one statement which does a bunch of things. It's the closest you'll really get but it's an approach that flows naturally from purity when you find you no longer have any void methods, using static extension methods in C# aids this greatly. In that way your expression point is the only point I disagree with, everything else is spot on with my own experiences learning FP and working a .NET day job – Jimmy Hoffa Nov 13 '13 at 16:13
  • What really bothers me in C# now is that I cannot use delegates instead of one-method interfaces for 2 simple reasons: 1. you cannot make recursive lambas without a hack (assigning to null first and then to a lambda second) or a Y-combinator (which is as ugly as hell in C#). 2. there are no type aliases that you can use in a project scope, so the signatures of your delegates pretty quickly become unmanageable. So just for these 2 stupid reasons I cannot enjoy C# anymore, because the only thing I can make it work is by using one-method interfaces which is just unnecessary extra work. – Trident D'Gao Nov 13 '13 at 16:52
  • @bonomo Java 8 has a generic java.util.function.BiConsumer which might be useful in C#: `public interface BiConsumer { public void accept(T t, U u); }` There are other useful functional interfaces in java.util.function. – GlenPeterson Nov 13 '13 at 17:15
  • @bonomo Hey I get it, this is the pain of Haskell. Whenever you read someone say "Learning FP made me better at OOP" means they learned Ruby or something that's not pure and declarative like Haskell. Haskell makes it clear OOP is uselessly low level. The biggest pain you're running into is that constraint-based type inference is undecidable when you're not in the HM type system, so cosntraint-based type inference is completely not done: http://blogs.msdn.com/b/ericlippert/archive/2012/03/09/why-not-automatically-infer-constraints.aspx – Jimmy Hoffa Nov 13 '13 at 17:16
  • 1
    `Most OOP languages don't have the stack space for it` Really? All you would need is 30 levels of recursion to manage billions of nodes in a balanced binary tree. I'm pretty sure my stack space is suitable for many more levels than this. – Robert Harvey Nov 13 '13 at 17:25
  • @RobertHarvey in typical FP fashion you would define LINQs `Select` on IEnumerables as a recursive function, balanced binary tree is one thing but in FP recursion is typically used for a great deal more than just that, I wrote a recursive descent parser for URLs at one point that grabbed schema, then recursed to match for the next portion getting domain, then recursed to break down folders and files and query string variable names and values etc. Anytime there's something with sequential repetition, recursion is your tool in FP. Many FP recursion uses wouldn't fit in .NET's stack space. – Jimmy Hoffa Nov 13 '13 at 17:46
  • @JimmyHoffa: And the number of recursion levels you used for your recursive descent parser was...? See http://stackoverflow.com/q/2556938 – Robert Harvey Nov 13 '13 at 17:48
  • @RobertHarvey I was just making a point that for nearly anything you will use recursion, so your one example may fit but many of the innumerable other times you use recursion in FP would not fit stack space. URLs can get pretty harry but I don't suspect it would risk stack space, though everything you do to arrays in FP will be recursive with at least as many stacks as their are members of the array/list/whatever. In these cases it's clearly too risky to use recursion in .NET – Jimmy Hoffa Nov 13 '13 at 17:52
  • @RobertHarvey In a data mining course I took, almost everyone who used Java had issues with running out of memory (not blowing the stack) when doing one of the recursion-heavy projects. No one else did in the other languages. I suspect something like that is what GlenPeterson meant? (My best guess, having not looked at their code, is that in Java-style OOP, methods end up "heavier" because of the objects involved) – Izkata Nov 13 '13 at 18:04
  • @RobertHarvey I think you may have misread my answer. Recursion is the best tool for processing trees in nearly any programming style. But writing a recursive function every time you need a loop is a pain in the neck! Stack space, performance, and decades of habit are other reasons to use loops when writing for your OOP buddies. Surely you are not suggesting that if I need to print out 10,000+ records from a database that my first choice in Java should be a recursive function instead of a `for` loop? – GlenPeterson Nov 13 '13 at 19:03
  • @GlenPeterson: No, I'm suggesting that Java should be capable of such recursion, that's all. – Robert Harvey Nov 13 '13 at 21:01
  • @RobertHarvey You can pass a command line switch to the JVM designating your stack size and that will work for an arbitrary number of recursive calls, assuming you have enough memory and time. But without tail call optimization, you will always be able to loop more times than you can recur. Languages like Clojure and Scala compile to loops if they can when you recur, so it's not so much of an issue. – GlenPeterson Nov 13 '13 at 21:13
3

In addition to functional programming and object oriented programming, there's also declarative programming (SQL, XQuery). Learning each style helps you gain new insights, and you'll learn to pick the right tool for the job.

But yeah, it can be very frustrating to write code in a language, and know that if you were using something else, you could be way more productive for a particular problem domain. However, even if you're using a language like Java, it is possible to apply concepts from FP to your Java code, albeit in roundabout ways. The Guava framework for instance does some of this.

sgwizdak
  • 31
  • 1
2

As a programmer I think you should never stop learning. That said, it's very interesting that learning FP is tainting your OOP skills. I tend to think of learning OOP as learning how to ride a bike; you never forget how to do it.

As I learned the ins and outs of FP, I found myself thinking more mathematically and gained a better perspective of the means in which I write software. That's my personal experience.

As you gain more experience, core programming concepts will be much harder to lose. So I suggest you take it easy on the FP until OOP concepts are totally solidified in your mind. FP is a definite paradigm shift. Good luck!

  • 4
    Learning OOP is like learning to crawl. But once you get on your feet steadily, you'll only resort to crawling when you're too drunk. Of course you cannot forget how to do it, but you would not normally want to. And it will be a painful experience to walk with the crawlers when you know you can run. – SK-logic Nov 13 '13 at 10:15
  • @SK-logic, I like your methaphor – Trident D'Gao Nov 13 '13 at 15:11
  • @SK-Logic: What is learning imperative programming like? Dragging yourself along on your stomach? – Robert Harvey Nov 13 '13 at 17:22
  • @RobertHarvey trying to burrow underground with a rusty spoon and a deck of punchcards. – Jimmy Hoffa Nov 13 '13 at 20:59
0

There are many good answers already, so mine will address a subset of your question; namely, I take umbrage to the premise of your question, as OOP and functional features aren't mutually exclusive.

If you use C++11, there are a lot of these sorts of functional-programming features built into the language/standard library that synergize (pretty) well with OOP. Of course, I'm not sure how well TMP will be received by your boss or coworkers, but the point is you can get many of these features in some form or another in non-functional/OOP languages, like C++.

Using templates with compile time recursion relies on your first 3 points,

  • Immutability
  • Recursion
  • Pattern Matching

In that template-values are immutable (compile-time constants), any iteration is done using recursion, and branching is done using (more or less) pattern matching, in the form of overload resolution.

As for the other points, using std::bind and std::function gives you partial function application, and function pointers are built-in to the language. Callable objects are functional objects (as well as partial function application). Note that by callable objects, I mean ones that define their operator ().

Lazy evaluation and pure functions would be a bit harder; for pure functions, you can use lambda functions which only capture by value, but this is non-ideal.

Lastly, here's an example of using compile-time recursion with partial function application. It's a somewhat contrived example, but it demonstrates most of the points above. It'll recursively bind the values in a given tuple to a given function and generate a (callable) function object

#include <iostream>
#include <functional>

//holds a compile-time index sequence
template<std::size_t ... >
struct index_seq
{};

//builds the index_seq<...> struct with the indices (boils down to compile-time indexing)
template<std::size_t N, std::size_t ... Seq>
struct gen_indices
  : gen_indices<N-1, N-1, Seq ... >
{};

template<std::size_t ... Seq>
struct gen_indices<0, Seq ... >
{
    typedef index_seq<Seq ... > type;
};


template <typename RType>
struct bind_to_fcn
{
    template <class Fcn, class ... Args>
    std::function<RType()> fcn_bind(Fcn fcn, std::tuple<Args...> params)
    {
        return bindFunc(typename gen_indices<sizeof...(Args)>::type(), fcn, params);
    }

    template<std::size_t ... Seq, class Fcn, class ... Args>
    std::function<RType()> bindFunc(index_seq<Seq...>, Fcn fcn, std::tuple<Args...> params)
    {
        return std::bind(fcn, std::get<Seq>(params) ...);
    }
};

//some arbitrary testing function to use
double foo(int x, float y, double z)
{
    return x + y + z;
}

int main(void)
{
    //some tuple of parameters to use in the function call
    std::tuple<int, float, double> t = std::make_tuple(1, 2.04, 0.1);                                                                                                                                                                                                      
    typedef double(*SumFcn)(int,float,double);

    bind_to_fcn<double> binder;
    auto other_fcn_obj = binder.fcn_bind<SumFcn>(foo, t);
    std::cout << other_fcn_obj() << std::endl;
}
alrikai
  • 101
  • 1