Sunday, March 31, 2013

Passing functions in Ruby: harder than it looks

People say, "Oh, Ruby has functional programming! We pass blocks around all the time!"

I'm sorry to inform you: Ruby blocks are not first-class functions.

Functional programming is so called because we find it useful to pass functions as values. When we do that, we expect that the function can be called and:
  • it'll get its own level on the stack
  • when it returns, that level of the stack will go away
  • this stack is the same one used by the caller.
When a function returns, whoever calls it gets the return value and goes on its merry way. Sounds easy, right?

Yeah, not in Ruby. See, the common case in Ruby is to pass code in as a block, something like this:
format_all_the_things { |a| "format #{a} into a string" }
Then inside the higher-order function, the block of code is called using yield:
def format_all_the_things
   one = yield "thing 1"
   two = yield "thing 2"
   [one, two].join " & "
end

Here, the block of code is an invisible parameter. It's a little like a first-class function, but not really. For one, blocks don't get their own level on the stack. For another, they don't execute in the same stack as the caller. Best I can tell, this "yield" keyword fires up a coroutine, which gets its own stack. Two violations of my expectations of a first-class function

Danger: return

Use the return keyword in a block, and you invite all kinds of trouble. See, return means it's time to knock a level off the stack. Since the block didn't get its own level in the stack, it has to return from the caller. But in this case, since it was started in a coroutine, there's nothing there to go back to -- LocalJumpError.
 > format_all_the_things { |a| return "yay #{a}" }
LocalJumpError: unexpected return
So, return is your enemy in blocks. Also in Procs.
this is even stranger
wrap the whole thing in a lambda and the block doesn't local jump error; it happily returns from the lambda when it hits the first return keyword inside a block. There's some magic going on here. This works for blocks, but not Procs. Can anyone explain this to me?

> lam = -> { format_all_the_things { |a| return "yay #{a}" } }
> lam.call
 => "yay thing 1"
Ruby has a third option: lambdas. These behave much more like the functions that I know and love.
lam = lambda { |a| return "yay #{a}" }
Lambdas do (seem to) get their own level on the stack, and when they return they return from themselves, not their caller.
Unfortunately this still doesn't work with yield.
> format_all_the_things &lam
LocalJumpError: unexpected return
Dangit! I thought I could trust lambdas, but I was wrong.
Turns out even lambdas behave sanely with return only if accessed using .call() instead of yield. This is the friendly way:
def format_all_the_things(&formatter)
   one = formatter.call("thing 1")
   two = formatter.call("thing 2")
   [one, two].join " & "
 end

format_all_the_things &lam
 => "yay thing 1 & yay thing 2" 
This is the right way to pass and use functions as values in Ruby: lambdas and .call().

Next

"Just don't use return!" you may say. Ruby devs are wonderfully disciplined at avoiding the language's less respectable features.

Sometimes the code is clearer if you can exit the function as soon as you know what the return value is. Especially with one of those handy post-statement ifs:
return "REDACTED" if (a.contains_sensitive_information)
Oddly, there is a control statement that appears to do exactly what I want return to do, and that's next. No longer a loop-control statement, it appears in Ruby 2.0 to be everything we could wish return to be.
> format_all_the_things { |a| next "REDACTED" if (a.include?("1")); "yay #{a}" }
 => "REDACTED & yay thing 2" 

More danger: Break

There are some circumstances where you can use break within a block to end Ruby's internal iteration early.
[4,5,-1,7].each { |n| break :invalid if n < 0 }
This works only with blocks, not with Procs (LocalJumpError) or lambdas (ignored). I think it's evil. Code that exercises flow control on its caller is not a function value.

Conclusion

If you're going to use a functional programming style in Ruby, use lambdas and invoke them with .call().

------
This is all in Ruby 2.0.

Here are my experiments in blocks, Procs, and lambdas with return, break, and next.

Thursday, March 21, 2013

Welcome All the People

On our team, in the user group, and at conferences, we try to build community. In each conversation, we do this by emphasizing ways we are alike.

  •  Who doesn't like beer?
  •  Monty Python is funny.
  •  Everyone has done Object Oriented programming.
  •  We all went to college.
  •  Each of us can grow a beard.
  •  Video games are awesome.
  •  Everyone can see and hear.
  •  We all grew up watching Saturday morning cartoons.

Each of these shared references draws together most of the developer audience and promotes a feeling of belonging. And each of them further isolates people who don't fit these assumptions.

I've been guilty of this. Writing presentations or posts, I look for domains familiar to my audience, and choose role playing. This emphasizes the programmer == geek stereotype, and the people who don't know what it means to "level up" feel stupid and out of place.

When a reference appeals to the majority then people who don't get it feel isolated. No one asks, "What does that mean?" when 80% of the audience reacts with knowing laughter.

Let's build community on what every one of us shares.

  • We want to learn from each other. 
  • Each of us wants to help build better software.

If you don't want to learn or build good software then don't read this blog. From now on, I aim to make only these assumptions about the people on my team, in my user groups, and at each conference.

Dick jokes? Funny, yes. Offensive, no. It isn't about offensive/not offensive anymore. It's about welcoming/isolating. When a particular joke applies to 80% of the audience differently than to the other 20%, that joke is isolating. That builds homogeneity. I want everyone to feel welcome, I want diverse ideas and contributions. I'm aiming for inclusion.

These are my action items:

  • Consider my target audience as the attendee mix I hope to see someday. Men and women and transgendered, Asian and African and European, 20s and 40s and 60s. People who like sports, and cooking, and quilting.
  • Give context to my references. The next time I mention the Prime Directive of Programming, I'll talk about how Captain Picard in Star Trek: The Next Generation follows the Prime Directive of "do no harm."
  • Have hallway and table conversations about a diverse range of topics, that any random person might be able to jump in on. I'll keep the stuff that freaks some people out for parties and private.
Let's make each other think. Development communities should be about learning, not warm fuzzies of all being alike, when "all" means "people who resemble me." I want to be all in, and let all in.