Hacker News new | past | comments | ask | show | jobs | submit login
Refactoring Ruby with Monads (codon.com)
136 points by wasd on Oct 3, 2014 | hide | past | favorite | 39 comments



This should be "Refactoring Ruby with Applicative Functors". The common interface for the so-called "monads" here is a subset of that of an applicative functor [1]; the fact that the types discussed could also support monad definitions (and that in some other languages, the applicative functor definition would piggy back on the monad definition) isn't really relevant.

[1] Comparing the listed Ruby to Haskell, .from_value is "pure", #within is "fmap", and there's no real equivalent of <*>.


`#and_then` is the monadic bind operation `>>=`; applicatives don't have this operation in general.


Recently I benchmarked a small network server written in Haskell. During the load testing the GHC runtime would allocate at rates of 1.6GB/s which is highly unusual from my experience but is apparently normal for pure functional languages. The impressive bit is that during the operations the heap was never bigger than 50MB and GC pauses longer than 100ms.

If we where to re-write all our ruby code to be purely functional I don't think that CRuby GC would keep up very long with that kind of memory pressure.


The reason GHC allocates at such an aggressive rate is the fact that memory allocation is really cheap in GHC. Allocating memory on the GHC heap is literally just bumping the "top of heap ptr", so writing one word.

The idea is that 90% of allocations are very short-lived, so allocating a ton doesn't matter as long as you can throw it away cheaply. Suppose we have an inner loop over an array, summing values. We may allocate at 1.6 GB/s doing the computation, but everything except the last value is garbage.

When we GC we simply copy this last value from the current heap and reset the "top of heap pointer". Yet another cheap operation.

There's a lot more going on (generational GC, nursery's and what not), but GHC's GC heavily abuse some facts that, for example, CRuby cannot, such as the fact that older generations can never reference data in newer generations, so GCing the current generation never has to perform "major GC" (i.e. checking the older data whether it still needs the current generation).


Small network server? Was it a custom one or one you got from somewhere? I know for a fact that wai, warp, and snap-server are way faster than that.


A custom non-HTTP TCP server with STM and forkIO


Because, you failed to properly set up GHC, you don't have the right to say anything about Haskell or functional programming.


It's the Internet, why wouldn't I write about my experience ?

And I'm sure it's possible to tune GHC. The argument is that if you're going to write immutable data structures in ruby then there is going to be more GC pressure. I don't think it's possible to optimise the ruby GC anywhere near the GHC one because of the mutable nature of the language.


>but is apparently normal for pure functional languages

Where did you hear that?

>and GC pauses longer than 100ms.

Yikes! That is not normal at all. You wouldn't be able to write a decent webserver in haskell if that were normal.


    > but is apparently normal for pure functional languages
    Where did you hear that?
It's certainly true for Clojure as well. My guess is that it applies to any language that uses immutable data structures. I remember Rich Hickey talking about issues with the JVM due to clojure's unusual allocation model in one of his talks.


It may well be true for clojure, but it is not for haskell. I know both clojure and scala have issues due to assumptions in the JVM execution model that don't play well with functional programming, but ghc was written explicitly just for haskell.


I think he/she meant the GC pauses were never longer than 100ms.


Yes, and getting anywhere close to 100ms is not normal at all. Warp benchmarks show total request latency never goes above 50ms or so, a 100ms gc pause would be very apparent.


It's from memory, it's possible that the major GC where around 50ms. Minor GC where around 5ms for sure.


I don't plan on rewriting my Rails apps, but the talk is certainly food for thought. At the very least this is a nice explanation of Ruby metaprogramming techniques; pair it with Paolo Perrotta's "Metaprogramming Ruby" and you'll have a good handle on the subject.


For the author's toy example, I don't see how this is better than using begin... rescue.

  def weather_for(project)
    begin
      project.creator.address.
        country.capital.weather
    rescue NoMethodError
      nil
    end
  end


If that does what it looks like, then Optional is better than that in one respect: begin/rescue will catch all NoMethodErrors, including unrelated ones. But you probably do want an error to be thrown if you typo and write "project.creator.adress" by mistake.


Because you may want to chain weather_for with some other methods and have this same "if nil don't break!" strategy. We would end up putting `rescue NoMethodError` everywhere and end up right where we started.

Instead, he later changes weather_for to accept and return this new `Optional` construct, making it chainable, and dealing with this "if nil don't break!" in one spot and one spot only.


Alternatively one could use NullObjects for the Optional use case: http://devblog.avdi.org/2011/05/30/null-objects-and-falsines...


how badly does this destroy your stack traces?


This is an excellent article but one of its main achievements is to make Ruby work like PHP (swallowing nil errors). The difference between try and Eventually isn't just that try is on every Object, but that the calling code gets to decide when a nil is unexpected. Personally, I'd rather get errors from nils which I can fix, than swallowed nil values that I'll probably never know about.


Please don't actually do this. Ruby is not a functional language, if your project really needs to be functional all the way through just use Haskell or whatever. Otherwise just pull out the parts of your code would benefit from being expressed in functional style into a module and work on implementing a proper DSL.


I think the point is that most projects don't need/can't be functional "all the way through". Like any sufficiently complex application many of us work on systems (or at least parts of systems) that could benefit from a functional approach. If the application happens to be written in Ruby, learning how to use and manipulate abstract data types can be rather useful.

The only valid complaint I see against taking a functional approach to Ruby is Ruby's shitty, memory intensive object system. Fortunately it's improving with every release and hardware just keeps on getting cheaper, so at the end of the day it's not a very strong complaint.


It's a style and design problem. Ruby is designed to make it easier on the developer when he's trying to do stuff. Part of that design is being able to read the code. If I went into a codebase and saw classes being used for functional constructs instead of repositories for holding state, I'd quietly close the project down and find another gem.

Ruby has a way to do immutability. It has methods to do common stuff like nil-checking if you pull in ActiveSupport, something I now do on all my projects. You just don't need to inflict such a heavy-handed approach for the meagre gains it will bring. If you find yourself implementing another language in the one you're using, then it's either the entire point, as in something like Opal, or there's no point at all and you should just go use that other language you're implementing.

It's not a question of performance, it's a question of interfacing and code style. The biggest bottleneck is programmer attention, and you're doing a real disservice to the next guy down the line if you mutate Ruby's conventions this way, especially if you don't have clean interfaces written.


I'm sorry, but I really don't follow at all. You call this approach heavy handed when in fact it's relatively small amount of source code that makes for much more readable developer code down the line. To me, Maybe seems like a much lighter touch than monkey patching Object.

And I don't understand your insistence that you're "writing another language" in Ruby by using a few functional constructs. For a language that uses .each as its base iterator and encourages you to pass blocks around it seems quite fitting, in fact.

Anyways, probably going to have to agree to disagree here. I come from the "every good language is a shitty lisp" school of thought so my personal lens might just not line up with yours.


I went through HtDP and loved it, it helped me gain a data-centric view on programming, and the lessons carried very well into my later Ruby career. I looked at Arc and liked the idea.

I like Ruby better than Lisp. It's hard to explain why. One of the reasons why I like to argue about this sort of thing on the Internet is because it helps me isolate the subtler qualities of different styles of programming.

I like Ruby because you can build a community around it, in a way that I don't think you can build one around Lisp, at least not like the one you find in Ruby, with the massive quantities of libraries and such.

Lisp is too free-form. You almost never need the power it gives you, and it encourages people to build their own language features where the problem being solved could probably do with a more conventional approach.

Ruby hits a sweet spot, enough dynamism to where you don't feel constrained, not so much that you can't easily reason about code. Code as data is a neat idea, but code needs to be read by humans and that's the harder task.


From the conclusion:

> Now, to be clear, I’m not saying you should immediately refactor all your Rails applications to use monads; I’m just using Ruby as a tool to explain an interesting idea about programming, not giving you a best practice for all your Ruby code. However, it’s good to know about techniques like this! And in some cases, by applying the power of monads wisely, we can untangle nested callbacks, make parts of our code more reusable, and generally make our programs better.


Yes, the author agrees with me that it's an intellectual exercise. I just wanted to underline that.

Rubyists are a clever bunch, if you find yourself writing the same code over and over again, chances are somebody's already figured out how to refactor it appropriately, tucking away the details behind an intention-revealing module.

A better way to refactor code is in using Ruby's built-in metaprogramming abilities. Metaprogramming Ruby is the best book to show you how.

http://www.amazon.com/Metaprogramming-Ruby-Program-Like-Face...


Monads are useful even in imperative languages. They're just a context with a way of specifying how actions get chained together. Scala, Swift, and a number of other imperative languages have adopted monads (like Maybe) to great effect.

Conversely, the state monad (for example) has seen less adoption. Probably because it's a little difficult to grasp, and imperative languages just rely on mutable state for a similar effect. The state monad provides some nifty extras above and beyond variables as they appear in imperative languages, but you can't argue with the low learning curve of throwing a value into a variable and mutating it.

As for the article, I really like the API presented here. I'd worry about the performance penalty of sprinkling method_missing throughout my code, but the code clarity is phenomenal.


> Scala, Swift, and a number of other imperative languages have adopted monads (like Maybe) to great effect.

Much of the use of "monads" in many other languages -- and particularly all the examples here -- really rely only on that subset of monad functionality that defines applicative functors.

> Conversely, the state monad (for example) has seen less adoption.

The use of the state monad really relies on it being a monad and not a mere applicative functor; I'm not sure if that's related, but I think that applicative functors are an easier thing to wrap your head around than monads.


No. #and_then here is monadic bind (>>=), and is necessary for all three use cases he described - nested nil checks, nested iteration, nested IO actions/callbacks.


No. Scala is a very functional language that you can write imperative code with. Swift is not functional and doesn't have real monads. Ruby monads are just a way of obfuscating your code. Monads are more than the monadic bind >>=. You cannot just use monads and ignore the rest of category theory, it's like using a stick to fix the engine of a car. Useless. Just use a real functional language so you don't have to deal with this tomfoolery.

Example of a real monad: http://www.haskell.org/haskellwiki/Monad


only in languages with first class functions.


Still, perhaps a good idea to help learn Monads if Ruby is your native programming language. Its helped me, as Haskell is still weird to my eyes


Just because a language isn't functional doesn't mean it can't benefit from some goodies of the FP paradigm.

C# is very successful with adapting bits and pieces from "other worlds" (such as FP, or dynamic typing etc.)


Why? What do you think is so wrong with ruby that using techniques from FP there is terrible? Virtually every other language handles it fine, and I can't seem to find anything about ruby that would make it a problem.


for those who haven't seen the talk, he says during it that he's not actually advocating that people implement this in their codebase :p


As someone currently maintaining a Ruby codebase originally written by the author, I winced at that ;)


I apologise for that codebase! Let’s just say I’ve learned a lot about building Rails apps since 2007. ;)




Consider applying for YC's W25 batch! Applications are open till Nov 12.

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

Search: