Talks

Ruby versus the Titans of FP

Ruby versus the Titans of FP

by Cassandra Cruz

The keynote titled "Ruby versus the Titans of FP" presented by Cassandra Cruz at RubyConf 2016 explores the functional programming capabilities of Ruby in comparison with major functional programming languages such as Clojure, Haskell, and JavaScript. Cruz, a hobbyist programmer with a decade of experience, shares insights from her recent experiences with Ruby while applying functional programming concepts.

Key Points Discussed:
- Introduction to Functional Programming (FP):
- FP focuses on functions as transformations rather than objects, promoting separation of logic and internal state.
- Cruz highlights common misunderstandings about functional programming definitions, emphasizing a pragmatic view where functions operate on generic objects.

  • Core Functional Programming Features:

    • Five key features of functional programming are identified: higher-order functions, currying, composition, functional purity, and immutability. Only three are discussed in depth during the talk.
    • Higher-order functions are highlighted for their utility in creating reusable and composable logic.
  • Comparative Analysis with Clojure:

    • Clojure, a Lisp language, is examined for its support of higher-order functions and mutable data structures.
    • The use of the map function in Clojure is explained as a means to transform collections, showcasing its simplicity and elegance.
    • Ruby's ability to implement a similar map functionality is demonstrated using Procs.
  • Explore Haskell's Currying:

    • Haskell's pure functional approach and its currying are introduced. Cruz explains that functions can be applied with fewer arguments, returning a new function waiting for the remaining arguments.
    • An example of implementing currying in Ruby illustrates how it can ease function composition, generating concise and effective logic.
  • JavaScript's Role:

    • Cruz discusses JavaScript's first-class functions but notes its limitations regarding functional programming features within its core API.
    • She mentions libraries like Underscore, Lodash, and Ramda, the latter being notable for its curried functions and functional programming paradigms.
    • The importance of point-free style and composition is highlighted, demonstrating how these concepts can enhance code reusability and clarity.
  • Conclusion and Call for Community Engagement:

    • By analyzing Ruby in conjunction with these languages, Cruz concludes that Ruby can effectively incorporate functional programming principles, albeit less traditionally than its counterparts.
    • She encourages community involvement, particularly in open-source functional programming projects, and emphasizes the importance of mentorship and collaboration.

Overall, the presentation illustrates that while Ruby is primarily object-oriented, it possesses capabilities to adopt functional programming paradigms, opening new avenues for development.

00:00:16.000 All right, good morning everybody! Morning! That was more enthusiastic than I thought. I thought everybody but me went out to drink last night. All right, and welcome to my keynote: Ruby versus the Titans of FP.
00:00:22.880 I'm going to talk a little bit about myself so that you know where I'm coming from with this, because this is going to be more of a story. I've been a hobbyist programmer for about a decade. The very first thing that I ever programmed was when I was 12 years old. Who here remembers QBasic on MS-DOS? Who here remembers "Gorillas"? My very first line of code was to cheat in that game. I made my brother always play player two, and I made his wind values display something other than what they were - just a little bit to the left or right - so that he could never hit me. That began a lifelong love of programming.
00:00:33.680 I'm also fascinated with functional programming; it's a very mathematical discipline, and it's easy for me to reason about what I'm doing. However, I'm relatively new to the industry. I'm at my first job at Mavenlink, a little company you've probably heard of from the three other speakers that I've been wandering around with here. I've been there for eight months, and it's my first real experience with Ruby. While I've experienced Haskell, Clojure, and JavaScript, it's only really this year that I got into Ruby. Yet somehow, I snuck onto the stage of RubyConf as a keynote speaker, and I'm still trying to figure that one out!
00:01:18.040 So when I first started learning Ruby, my motivation was, "Oh hey, I need to do this for my job," but I also wanted to bring all my functional knowledge with me. Not many people use Ruby in a functional manner. If you want to create something, you have an object for it, methods, and everything is object-oriented. I was just thinking, "Okay, I’ll roll with it," until a few months ago when somebody encouraged me to submit a CFP. I finally decided to explore this and thought, "Okay, I'm going to make functional programming work in Ruby." This talk is a documentation of my past three months distilled so that you don't have to see me yelling at a REPL trying to figure out what's broken.
00:02:02.040 All right, so welcome to Ruby versus the Titans of FP! Before we get started, we want to talk about what functional programming is, because not everybody is familiar with it. If you ask 20 people, they'll give you 20 different definitions. Some people will bring up math, while others might refer to category theory and sit you down for a three-hour lecture. I prefer to be a little more pragmatic.
00:02:49.720 To me, I like to describe it as a programming paradigm that focuses on functions as transformations over generic objects and data structures, as opposed to objects that we model. A good example of this is a login object. I think everyone here has written a login object if they've worked on a web app. When you approach it, you're like, "Okay, I'm going to give it some attributes, and it's going to know its internal state, but then it's also going to know how to validate itself, delete itself, and save itself to the database." Functional programming separates those two things.
00:03:25.560 So you can still have a login object that can end up being a generic data structure, just like, "Okay, I know the user I'm associated with; I know if I've been deleted." But then you'll have a separate set of functions that can operate on that object, for example, to invalidate it. Why is that a good thing? Well, one, it makes it easier to reuse logic, and I think this is one of the biggest benefits. I'm pretty sure everybody has been in a situation where they're like, "Oh hey, this class has an amazing method that I really want to grab, but I can’t subclass it for whatever reason because inheritance scares me sometimes." It’s helpful to have that logic already separated so you can figure out how to make it generic for an entire class of objects.
00:04:07.239 It allows you to compose smaller units of logic together, which will be the focus for ten minutes in this presentation. It can, keyword can, in giant air quotes, result in cleaner and more performant code if it's done the right way. If it's done the wrong way, it can get kind of messy.
00:04:40.320 All right, so these are my five key features of a functional programming language, and this is probably contentious. If you go to the functional programming lexicon, you'll find like 300 definitions inside of it. But these five things are essential to me: higher-order functions, currying, composition, functional purity, and immutability. We are only going to talk about three of those today. Functional purity and immutability are things we can achieve through the data structures we use and how we treat the flow of our programs.
00:05:13.520 However, higher-order functions, composition, and currying tend to be more primitive in our languages. If we don't have support for them, we end up losing a lot of functionality. So today, I'm going to compare Ruby versus Clojure, Haskell, and JavaScript to see if we have those features that make other functional programming languages appealing.
00:05:53.640 Let's start with a little thing called Clojure. Clojure was born in 2007 and it is of the Lisp family. That makes some people very happy and others very sad. What makes it a functional language? It has higher-order functions, first-class functions - which means we can take a function and store it into a variable and use it anywhere we want - and its standard library has mutable data structures, which makes me so happy. I wish I had more chances to use it.
00:06:33.639 This is an example of what a function call in Clojure looks like. It's simply a list, where the first item in the list is a function and everything else are arguments. Lisp is actually short for list processing. This seems elegant and simple, but then you may end up with programs that look quite confusing. However, we are not going to dive into anything complex with Clojure; we’re going to talk specifically about the "map" function.
00:07:24.360 Map is a function that takes arguments. What are those arguments? The first argument is a function that we want to use to transform some collection. The example we're going to use—and that you're going to be tired of by the end of the 35 minutes—is going to be incrementing numbers. The second argument we pass in is a collection, which will be a standard array for our purpose.
00:08:08.880 So this is what it ends up looking like: We map our increment function over the numbers 1 through 5, and it returns 2 through 6. With this, we have already arrived at the first capability, which is higher-order functions. A higher-order function is one that either takes in other functions as part of its arguments or returns a function as its result. In this case, our map function takes in a function for use in transformation.
00:08:55.360 So, can we use this in Ruby? Obviously! I’m pretty sure everybody here is thinking, "Why? Everybody knows we can do this!" The standard way of doing a map in Ruby is to have some collection call map on it as a method and then pass in the arguments. But we're going to create something a bit more generic for reasons that will become clear as we proceed. We're going to start with a proc and give it our two arguments: the function as our first argument—making this map a higher-order function—and then our collection.
00:09:40.640 We're then going to cheat and just call the collection's map method. If it quacks like a duck, it must work for us. If you've ever read the Clojure documentation, 'f' and 'c' are used all over the place; they refer to function and collection respectively. Congratulations, you can now read 90% of the Clojure documentation because that's often all it is - signatures.
00:10:10.560 How do we use this? Well, here we’re going to create an increment function that simply takes a number and returns one. We are going to give ourselves a list of numbers from 2 through 4, and then we will call map to invoke it with our arguments. This method can drive some people insane, but fortunately, Ruby is nice to us.
00:10:55.680 We can hide the 'call' method, which was a mind blow moment for me two months ago while learning. I thought, "Okay, I’m tired of using 'call' everywhere!" It turns out there's syntactic sugar for it, which makes things cleaner. If you define an object that has a call method on it, it also has this alias. You can hide all your calls - it looks quite beautiful.
00:11:43.680 A more complex example, revisiting logins, let's create a proc that takes a login and sets it to deleted at the current time. We can check, "Is deleted at not nil?" Cool, that confirms we’ve deleted something. Then we're going to create a list of logins - or a vector of logins. I’ve yet to find a use for vectors in production code - and we will map over that with our 'invalidate login' helper.
00:12:37.440 So we’ve met our first requirement: we can use higher-order functions! The syntax here is a little different from what I’m used to, but it’s there. Next, we’re going to cover currying and lean into our old friend/enemy Haskell. Haskell was created in 1990, making it one year younger than me, which officially makes me feel old! It's from a family of languages called ML, which I always thought stood for machine language until corrected two days ago.
00:13:32.080 What makes it a functional language? Everything is curried, everything is pure, and it has the most beautiful type system I have ever seen in a programming language. Although, it also drives some people insane. What does a function call look like in Haskell? Here, we’re going to create our increment function by using a plus sign followed by '1'. We then take that increment and pass it into map with 2, 3, and 4, and we receive what we expect, 3, 4, and 5.
00:14:20.960 However, what’s interesting is that 'add' takes two numbers, but we only gave it one, and it didn’t throw an argument error! Let's investigate this, but in the worst possible way. Two important concepts we need to know are that type systems express function signatures and help in type annotations, checking against the types that you annotate.
00:14:55.760 Add, or at least our version of add, takes two arguments, grouping them to signify we are taking them at the same time with a function arrow telling us we are going from something to something resulting in a value. Congratulations! You can now read basic type signatures in Haskell.
00:15:35.720 So how do we typically handle 'add'? We call 'add' with two numbers, which gives us one number, but if we give it one number, we get a stack trace. Haskell does things differently. Haskell has the same 'add' function; it takes in two numbers and returns a number, but we can only give it one argument at a time. When we provide it with one argument, it returns a function waiting for the second argument.
00:16:38.520 Now, let's go back to our 'add' example and see what happens. We provide it one number, and we get a function waiting for the second number. This gives us our definition of a curried function. A curried function is a function that, upon being applied to fewer than the total set of arguments it takes, returns another function waiting for the rest of its arguments. If we had a function that took three arguments, and we only give it two, it would return a function waiting for the last one.
00:17:48.080 Let’s implement this in Ruby using closures. Here we’re going to create 'add', which is a proc that takes in 'x.' When we call this with a number, it will return another proc waiting for 'y.' When we call this second proc, it will give us our result. We’re now going to create our increment function by simply calling our 'add' method with one and expecting it to bind that one to 'x.', returning a function waiting for 'y.' So we can call increment with two and expect a result of three.
00:18:21.360 Now, there's a better way—nobody wants to write 20 nested closures! Luckily, we have native currying in Ruby. This feature made me extremely jealous because I came from JavaScript, which doesn’t have native currying. I felt delighted because we can write our functions in the usual way—XYZ does a thing—and it will automatically handle currying for us. In addition to everything we can do with currying, we can call it with all its arguments at any time and get the result immediately.
00:19:29.440 Moving on to the order of arguments, you’ll notice that Haskell, Clojure, and Ruby all had the same order of arguments. If we wanted to create a function called 'ink_map' that increments a list of numbers, we can build it this way, assuming that our 'add' and 'map' are curried. What about decrementing everything instead? It’s simple enough: we just use our 'add' function and provide it with negative one instead of one. This allows us to build entire families of functions from our primitives.
00:20:09.960 I can build every step function from my 'add' function. I can create every iterative transform function from my 'map' function. This abstraction allows us to simplify our code and avoid worrying about implementation details.
00:21:02.560 Now, let's talk about our best friend, JavaScript! JavaScript is relatively new, emerging in 1995, and doesn’t belong to any specific family. It's somewhat like Scheme but with C syntax, so it stands on its own. What makes it a functional language? Well, it has first-class functions, which lets us carry out higher-order functions and closures, but it doesn’t have much built into the language.
00:21:56.920 Many functional programming libraries exist for JavaScript, but the core API doesn’t support a fully functional style. For instance, the array object poses certain challenges. We define an array with three numbers; map is a method on the array, and if we want to access map as a standalone, that’s how we do it. Pop gets something off the end of the array, and it mutates the array beneath it, violating immutability.
00:22:46.560 Now, wait a second, doesn’t Ruby also do the same things? Yes! It does exactly that! In fact, 'map' is a method on arrays, and 'pop' mutates the same way. Hence, a lot of the standard library functionality creates friction for a functional programming flow in both languages and gives us insight on how functional programmers in JavaScript solve their problems, which can also be applied to Ruby.
00:23:44.520 The answer? Functional programming libraries! In the beginning, there was Underscore, and it was okay—kind of. It got the order of arguments wrong, and then Lodash came around, where unless you used Lodash FP, nothing was curried. Lastly, Ramda popped up, and everything is curried, all arguments are in the right order, and everything has Haskell names for functions. It allows you to pretend you aren't using a browser.
00:24:50.640 Ramda also provides nice composition functions, so I'm going to define ‘addTwo’ here. I'm going to create an incrementer 'add one' and glue those two functions together. The argument I’m passing in gets passed into one of the increments, and the output from that becomes an input for the next increment. So our 'add two' function, when given the input of two, returns four.
00:25:48.640 This gets a bit more complex: if you want to implement a 'map reduce', typically you have a function that takes in something, calls map, saves off the result, calls reduce, and then passes it back. But with Ramda's libraries, both map and reduce are curried. Map takes an increment or a transformer and returns a function waiting for a collection. Then reduce expects a transformer, an initial value, and it returns a final value. Because the types match, we can glue them together.
00:26:51.680 This also allows for point-free style, where we don’t have to explicitly mention our data. If we never mention our data, we don't risk mistyping it. This leads us to actual easy logic reuse; if we craft an increment function and a decrement function, we can combine them in multiple ways. If we have 20 functions, we can piece them together like Legos any way that we want, allowing for true logic reuse.
00:27:47.680 And we can compose our compositions, which simplifies refactoring. A solid example is the request/response cycle: I have a composition that takes a request into some internal state, a composition that does something with the state, and another composition that takes the state and transforms it into a response object. I can isolate those three, compose them together, and make my whole request/response cycle a single function. This keeps everything clean even with 300 routes.
00:28:53.720 So to use this in Ruby, we're going to create a binary composition, which concerns itself with just two functions. We’ll make an outer proc that takes in two functions, returning a proc waiting for the arguments. Once we receive the arguments, we’ll call 'y' with the arguments, and the output of that will be passed into 'x.' This order aligns with mathematical composition, and we can create our increment function. If we add two to four, we get six back!
00:29:51.960 If we want to do this for more than two functions, we can take the composition we just created and build a variadic compose that accepts multiple functions and reduces over that with the binary composition operator. So here we can create an 'add three' from three increments. While I don't know why we wouldn't just use 'add three' to build up our total, it ultimately works for this demonstration.
00:30:58.960 So what if I just want a gem? Well, I’ve written one! If you go to RubyGems and search for 'reductio,' it only has three functions: add, compose, and map, as those are the three functions I need for this demonstration. You can see that the 'add' function is curried, while 'compose' is variadic. You can glue together up to 20 functions. Is that a good idea? Probably not. But can you do it? Yes.
00:31:54.960 I have done it, and it’s not pretty. Please don’t look at my GitHub; it’s a scary place! This is something I’m very happy about because I got comfortable enough in Ruby to create a functional library. So, what have we established? Ruby can do well with higher-order functions, composition, and currying when comparing it to these three languages. We haven’t looked into functional purity or mutability, but the tools we have are sufficient.
00:32:46.520 While people like to compete Ruby against other functional programming languages, nothing stops Ruby from being a part of the circle too. That's what I have for you today.
00:33:11.680 Oh! I went through that way faster than I expected, which means I have question time. This could be interesting!
00:33:24.320 If you would like to learn more about functional programming, Dr. Frisbee’s "Mostly Adequate Guide to Functional Programming" is available on GitHub and written by an adorable Badger named Brian Lorf, a functional programmer at Netflix. Although all examples are in JavaScript, it should be easy enough to follow. Also available is my repository for Reductio.
00:34:07.700 I want to stimulate more discussion, as I feel I have trapped myself in a bubble trying to divine things the hard way. If you’re on Twitter, please tweet your questions at me, or just ask them in the next 10 minutes. Use the hashtag #FunctionalRuby, so I can keep track of everything and others could contribute too. Also, I would very much appreciate help: I’ve never written an open-source gem before, and writing documentation has been confusing for me. So if you’re a functional programmer and want to help with that, or if you’re not a functional programmer but wish to learn, please feel free to assist.
00:35:07.150 Before I finish, I want to give a shoutout to Transcord. I also want to commend RubyConf because it has been a challenging week for me as a trans woman of color, but I felt welcome here. I’m glad I could be a part of RubyConf, especially my very first time!
00:36:02.220 If you know any trans individuals or allies seeking a sense of community, please check out the link to Transcord, which is a global support group that would love to have more members. If people haven't sold you on the Kool-Aid known as Mavenlink yet, we have this nice engineering blog that talks about our company's culture.
00:36:28.240 So, I think we’re ready for questions. I was not prepared for this.
00:37:13.560 Yes? Okay, so the question was that they have trouble seeing the benefits of point-free style, as it can seem ambiguous and indirect due to the fact that you never actually mention your data.
00:37:20.560 One of the things that really appeals to me about point-free style is that we get to walk away from the constraints of our data. With the map-reduce I demonstrated earlier, it doesn’t matter if it's an array, a linked list, or if it’s a maybe, either, IO, or some other functor; I don't worry about it.
00:37:30.560 I just know there’s an interface that whatever comes into this function needs to comply with, and that allows me to use it generically, which is one of the main benefits for me. I can define my type annotations like, "This takes in a monad and returns it, etc." and then I don’t really have to worry about my data after that.
00:38:35.000 Does that answer your question? Is it kind of like duck typing? In JavaScript, it’s treated that way. So if you use Ramda and pass something with a map method as the mappable entity, it will trigger that method, similarly yielding the desired outcome.
00:39:20.780 So the question asked was about handling I/O, as all programs have side effects, and how you manage that when you have object-oriented definitions that need to operate alongside functional logic. My first instinct is to say, use a monad, but that doesn’t exist in Ruby yet—I'm working on that!
00:40:27.800 Currently in our company, we have a team that uses React, and the way I handle this is by pushing side effects to the very end of our chain. Compositions function themselves and can be composed with other things, allowing me to create a pure composition transitioning from Point A to B while in my app, combining that with components that accept data and generate side effects.
00:41:39.000 This creates a clear divide, maintaining pure code within libraries and pushing side effects toward the edge of the app.
00:42:03.560 Now, regarding Active Record scopes, I enjoy that scopes compress together to build a SQL query. However, I'm a bit uneasy that someone can induce side effects by interacting with data at any stage.
00:43:10.000 Active Record queries can execute database calls on certain actions when I’d prefer to minimize those interactions to the furthest down in the chain.
00:44:15.000 Has anyone here conducted any benchmarking?
00:44:22.000 No? Okay, the question was about why we don’t want to combine functions excessively!
00:44:35.000 Looking at the type signature for this can be complicated! This operation conducts iteration over a list and applies functions repetitively. If we were to add further operations, it becomes easy to miss what we’re doing, especially when revisiting code months later.
00:45:05.000 To keep things clear, I prefer to encapsulate multiple operations into their compositions, improving maintainability and readability. It’s helpful as you can create brief, concise functions while obtaining clarity and function.
00:46:05.000 I have a few minutes left—any last call?
00:46:34.560 No? All right!