Hacker News new | past | comments | ask | show | jobs | submit login
Functional calisthenics (codurance.com)
69 points by pedromsantos on Oct 19, 2017 | hide | past | favorite | 36 comments

Isn't this going a little overboard?

I remember when OOP came up, some people made functions illegal. So instead of

s = sinx(x)

the OOP way was

m = new Math() s = m.sin(x)

And instead of switch statements you had to derive a new class for each case call a virtual function. Very "OOP" but it led to the OOP equivalent of spaghetti code.

Are the functional guys also starting to leave the real world for the sake of being "pure"?

Author here. A few have mentioned already, but this is to practice, to force you to understand what FP brings to the table, and lead you in path of discovery. When it comes to production code, you apply what you have learned, and make the decisions that you think more convenient. As an example, if you want certain performance gains on F# you do go to mutable state (Phil Trelford did a talk about it). Doesn't matter how pretty/OO/pure is the code if your application doesn't do what is supposed to ;-)

Thanks for the article. There's a class of problems that I wouldn't know how to solve using the constrains you propose.


    { a: { b: { c: 4 } }, d: 2 }

    { 'a.b.c': 4, d: 2 }
My go to solution uses mutable state, self-recursion, and intermediate variables:

    function flatten(source) {
      var result = {}
      _flatten(source, null, result)
      return result
where `_flatten(source, prefix, result)` is self-recursive (`prefix` accumulates the keys) and populates `result[prefix]=value` when value is a string. `result` is conceptually an IO monad that's passed along.

I guess the general problem would be traverse a tree of arbitrary depth and accumulate structured data on the go. Prominent examples would be compilers.

Any hints on where to start?

Cool. I commend you for having a team that is willing to practice new things for fun. That's pretty rare.

The article states:

    These rules are constraints on how to create your code
    and are only intended to be applied when doing katas or
These rules are an exercise to kick you out of an OOP mindset, not best practices for writing real code.

By that same token, I think it is a good rule when practicing OOP to not allow bare functions. It helps train yourself to think "Is there a logical class that this method belongs to?" In real code, of course, the answer is often "no". But when first learning OOP, it's good to teach yourself to ask that question in the first place.

Your code isn't a good example of "the OOP way", though. A fluent OOP solution would be:

    s = x.sin()
Another good rule when practicing OOP would be "No classes without state." That helps you avoid the habit of writing pointless "namespace" classes like the "Math" class you define here. Of course, again, sometimes it is useful to wrap a bunch of related behavior in a stateless class, but that should be the exception, not the default.

I don't think that no classes without state is a good hard and fast rule. In some languages classes are the easiest way to get a new namespace, and thus in combination with static methods they are useful in producing fluent APIs (which I believe are a good design choice). You could of course achieve the same result with modules but I'd argue it's cleaner to have an inline class than a separate file when you're only adding a couple of functions.

You know, you're talking about a very specific kind of OOP here. Elixir, for example, is a language I'd call OOP—but it doesn't have classes. It just has "bare functions" in modules, where a module's functions will usually expect values of the type the module is named after.

Also, to reverse that perspective, a "type" in Erlang/Elixir-lang is just any data formatted in a way that a given set of functions will work with it, rather than requiring an explicit tag. {1, 2} can be an instance of a type†, if you have a module Foo that spits that shape of data in response to Foo.new/0 and takes it for Foo.change/1.

† An example of such a type is Erlang's http://erlang.org/doc/man/queue.html . queue:new/0 just returns {[], []}.

The actor model that Elixir/Erlang is OOP in its "purest" form, but that's not what most people mean when they discuss OOP languages.

In Java static methods are a particularly simple way making clear to the compiler and runtime that a given functions does not have side effects (to instances of the owning class). I can imagine that, as a pattern, they would assist escape analysis.

A static method can of course mutate an object of the same class (give the object as an argument (direct or indirect) or use a static variable of the class)

I can not see how they could be used to help escape analysis more than that you have one less reference (this).

Yes, of course that's possible. I was trying to point out their usefulness to the programmer as well as potentially to the runtime/JIT. Next time I'll list the obvious exceptions!

In C# you can have static classes. I often use these to group simple functions together.

I think it's more of an exercise than a stringent philosophy. Looking at these rules, they're designed to be helpful for cultivating functional habits and patterns of thought if you're not used to thinking and coding that way.

I wouldn't expect anyone to follow them religiously in production code.

>And instead of switch statements you had to derive a new class for each case call a virtual function. Very "OOP" but it led to the OOP equivalent of spaghetti code.

I disagree. Switch statements are fine to react to incoming data, but terrible for everything else. If you add a value to an enum you have to change every place where the enum is used in a switch. And the code inside the switch is not reusable (Unless you make it a method, at which point you're close to the "OOP" way anyway). I agree that religiously avoiding switches lead to spaghetti code, but mindlessly using them does so too.

Nitpick, but why

  m = new Math() s = m.sin(x)
would be "the OOP way" instead of


Besides this nitpick, i concur with Veen. The post seems to be about exercises/challenges, not FP best practices.

" x.sin "

Good point. I guess in C++ numbers aren't objects so you can't do this.

I hope they understand that this is just an exercise. I have seen too often these things being elevated into a religion.

> I have seen too often these things being elevated into a religion.

Yeap. I have no hard preference between sin(x) or x.sin, in an OOP context or not, as long as the language is somewhat consistent (e.g., it bothers me that Ruby has some of these functions on the numbers themselves, like 1.23.floor, but others live on Math, like Math.sin(42)).

> it bothers me that Ruby has some of these functions on the numbers themselves, like 1.23.floor, but others live on Math, like Math.sin(42)).

Every time I type `len(some_list)` in Python, I die a little inside.

While I agree to an extent, the reason for this is not entirely bad. The len function is basically a contract that the result you get will be an integer. An object could easily have a `.length` property that is non-integer, like a line/interval for instance. Think of this as a clunky response to a lack of type information in the language proper.

> Nitpick, but why "m = new Math() s = m.sin(x)" would be "the OOP way" instead of "x.sin" ?

Because you can't do the latter post-hoc. You can't add hyperbolic functions by yourself if your standard library didn't anticipate them. Well, in Ruby you can and it's a standard practice to pollute this way classes at random, and in Python you can as well, though it's frowned upon, but certainly you cannot in C++ or Java.

it's probably doable as is in Smalltalk, the issue is OOP trying to ontologically categorize everything is impossible; one day we may need operations on numbers we don't know now and we'll have to "import CategoricalHomotopy as ch; ch.tropos(42)"

sinx(x) can be still object-oriented. Actually, really neat object oriented systems which implement multiple dispatch, like CLOS, tend to use this notation.

No. Almost every functional language forces you to follow these "calisthenics." These look more like guidelines to follow if you want to execute the functional style in an imperative language.

I would say that only Haskell (and derivatives) force you to follow most of these rules. Neither Clojure, nor F#, nor Elixir force you into most.

I don't understand "no explicit recursion". How do you do any kind of binary tree search without recursion?

This is in contrast with structural recursion. When you define a binary tree datatype you should get for free a generic fold operation on these trees, analogous to the map and reduce functions on lists. I don't know if Clojure gives you that.

The people that talked about the rules at Codurance was none of the people that defined the original rules. We had to "reverse engineer" the thinking, find reasons and then discard, change or let as they were (one of the objectives of the write up was to put down those reasons). Explicit recursion did take a while. The reasoning we came with is to learn to use map/reduce functions (do take into account that our group has an ecletic knowdledge of FP and hybrid languages: Clojure, Haskel, Elixir, Scala, F#, ...). Can that binary search tree be converted into a linear search if you rearrange the tree into a list? Yes. Now you can use map /reduce. Is it performant? Most probably not. Are there other ways? Time to discover. Interestingly, most of the katas/exercises on which we are practicing these rules are not heavily algorithmic in nature. Maybe we do need to add some to understand better and modify the "rules" with the new knowledge.

If you want an example, consider writing a simple recursive sudoku solver, where at each location you fill in one empty square with the values 1-9, and reject early if you ever have a repeated value in a row, column or box.

This is easy to write in C, and I always imagined would be something functional programming would excel at, but it doesn't seem to fit with your rules.

SOLD!!!!! I will have to add this to my kata collection. That also reminds me of the 8-queen problem. The last solution that I created (C#) years ago did use recursion.

Here is an answer in the CS Theory Stack Exchange that explains how: https://cstheory.stackexchange.com/a/18399/13730 (I'm not endorsing it as a practical way of doing it, mind you.)

The idea is that the only explicit recursion is contained in generic combinators "fold_result" and "unfold_result" that are "boring" in the sense that they can be derived mechanically from the "resultF" data type and do not actually know about the binary search; they just do recursion.

In practice, folds and unfolds tend to be more useful for other things.

In fact, a colleague that is reading SICP at the moment is the one that basically put the idea forward, as he was going through the whole idea of fold/map/unfold on the book exercises

I wouldn't even call these things calisthenics. These are requirements for the programming style to be called "functional."

Really if you aren't following <Edit> Most <Edit> of these rules you are not doing functional programming.

The best way to learn about functional is not to follow these rules but to use a functional language that basically enforces these rules. Javascript is not a good language to learn about functional programming.

I'm not really sure why you need the requirement of infinite sequences to do functional programming, or the restriction of no intermediate variables. Let bindings seem like a nice thing to have.

No you're right. I missed the infinite sequences thing. I'm going to reword the post from "all" to "most"

You are right. If you don't follow most of those rules, you are not doing pure FP. My interest on them was because I was writing production code that felt procedural. The rules are there so you can practice with purpose. Once you go into production code, you can discard them, but with knowledge that it will not be because your OOP skills take over, but conscious decisions.

I came here expecting an article on CrossFit!

Applications are open for YC Winter 2024

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