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!