RailsConf 2023

Functional Patterns in Ruby

Functional Patterns in Ruby

by John Crepezzi

Introduction

The video "Functional Patterns in Ruby" presented by John Crepezzi at RailsConf 2023 explores how concepts from functional programming, particularly from the statically-typed language OCaml, can be implemented in Ruby to enhance code quality and structure.

Key Points

  • Background and Transition: John details his journey from working with Ruby on Rails to joining Jane Street, a company that uses OCaml. He emphasizes the importance of keeping an open mind and seeking new learning opportunities.

  • Differences Between Ruby and OCaml:

    • Type Systems: Ruby employs dynamic typing, allowing variables to change types at runtime, whereas OCaml uses static typing, enforcing type constraints before code execution.
    • Programming Paradigms: Both languages support multiple paradigms, but Ruby is primarily object-oriented while OCaml has a strong functional focus.
  • Language Features from OCaml Applied in Ruby: John discusses specific OCaml features that can enhance Ruby programming:

    • Functors: These allow for the definition of modules with parameters, enabling more flexible and reusable code. John illustrates how to create a similar pattern in Ruby using dynamic module generation.
    • Variants: These represent a value that can exist in one of several states. John explains how to implement a variant type in Ruby, emphasizing the need for a cleaner way to manage multiple data types while maintaining user input.
    • Monads: Specifically the Option Monad, which provides a way to encapsulate values that may or may not exist. This prevents common errors associated with null values and emphasizes handling potential absence explicitly, improving code safety.
  • Technical Implementation: John outlines how to replicate OCaml's functors, variants, and monads in Ruby. He shows examples of handling cases within logical structures that prevent common pitfalls, highlighting the importance of robust design patterns.

Conclusion

The main takeaway from John Crepezzi's talk is the value of integrating functional programming concepts into Ruby to create more robust and maintainable code. He encourages developers to explore different programming languages to enhance their skills and bring new ideas back to the Ruby community, demonstrating that many functional features can be effectively modeled in Ruby.

00:00:20.600 I've been working with Rails for a long time at various companies.
00:00:25.920 Similar to last year, I started writing this talk, but I ended up with way too much content.
00:00:31.859 As a result, I won't have much time to share about myself or my background, but you should know that I'm pretty cool.
00:00:38.640 If you're interested in learning more about me, you can follow me as CJohnRun on GitHub and Twitter.
00:00:44.460 I'm also on Mastodon.social, but I only have three followers, so I don’t often toot in public.
00:00:51.600 Like many of you, I hope I love Ruby on Rails. I love it even more than I did last year.
00:00:57.300 Who here loves Rails more than they did last year? It's such a great framework.
00:01:02.940 It's a web framework that lets me easily and safely express my thoughts without unnecessary fluff on top of a programming language that I still find extremely beautiful after 15 years.
00:01:08.820 Last year, around the time of RailsConf in Portland, I left my job at GitHub. After a brief misstep indicated by the crashing car, I now work at Jane Street, which is a tech-forward finance and trading company based in New York.
00:01:21.240 At Jane Street, I continue to work on developer tooling, just as I did at GitHub. It's pretty exciting because they have some of the best developer tooling I've ever seen, complete with robust systems for code review and deep editor integrations.
00:01:39.420 However, what they don’t have, sadly, is Ruby, and it follows that they also don’t have Rails. Jane Street writes nearly all their code in a fairly obscure programming language called OCaml.
00:02:03.840 I had never worked in OCaml, nor did I know anyone who had. Not many other companies or projects use OCaml, and to be honest, I stressed quite a bit before joining that I might be investing my time in a skill that wouldn't end up being transferable.
00:02:22.200 When you first join the company, they put you through a two-week onboarding for the language. They call it a 'boot camp,' and it's something you must attend. However, it's also something you will absolutely need.
00:02:39.060 OCaml has some features and patterns that just don't make sense if you haven’t been exposed to them before. Someone will walk over to you and probably reference Lambda calculus, and you'll probably make a confused face.
00:02:57.180 Then they will hand you a book called 'Types and Programming Languages.' It's pretty dense; they refer to it as 'TAPL' and usually say something like, 'Oh, it's a classic!' or 'It's not as complicated as it looks.'
00:03:14.940 This is clearly a company that has spent a lot of time thinking about what programming language they use and why they’re using it. The impressions left on me were that I need to keep an open mind and not just spend all my time talking about Ruby.
00:03:31.860 I also realized I needed to figure out what I didn’t know and how I could learn it. Lastly, given that Ruby and OCaml exist in such totally separate worlds, I began to consider what new tools I could gain that might apply elsewhere in my programming life.
00:03:44.580 In this talk, I'm going to go over some neat features from OCaml and show how they can be applied in Ruby. What I’m not going to do – and this is the thing I've seen other talks like this do – is try to convince you that OCaml is somehow better than Ruby.
00:03:56.220 As a spoiler, I don’t think it is better. In fact, I convinced my wife to name one of our kids Ruby, yet as of this talk, I have no kids named OCaml.
00:04:14.360 Rather, I hope to demonstrate that many of the things we see as features in OCaml are actually design patterns. With a little bit of work, we can adopt these patterns in Ruby as well.
00:04:29.600 To set a good foundation, let’s first go over some quick similarities and differences between these languages. The most obvious difference lies in the type system: Ruby has a dynamic type system while OCaml uses static typing.
00:04:42.720 For those of you who haven't programmed in a statically typed language, this means that the type of each variable is known before the code runs. Consider this example in Ruby: first, we define a variable called 'var' that holds the integer five.
00:04:54.840 Then, we redefine 'var' to now hold five but as a string, and we redefine it again to hold 'var' as a symbol. Finally, on the last line, we assign something from params to 'var', and now we don’t even know what type it is anymore.
00:05:12.120 It could be a string, an array, or a hash. We’ve all written code like this; it doesn’t feel weird to me but, what is the type of 'var'? The answer is it kind of depends on where you are in the execution.
00:05:29.940 Each line makes 'var' a different type. Thus, the type of the variable is only determinable at runtime. It’s relatively easy for me to write some code like this in Ruby.
00:05:55.500 Here I am calling the absolute function to get the absolute value of a string. We can all look at this code and know that it is probably wrong; in most codebases, 'abs' is not defined on strings.
00:06:06.900 If you do have 'abs' defined on strings in your codebase, please come find me because I want to hear about it. But that doesn't stop us from writing this code in Ruby and running it.
00:06:29.880 We won’t receive a no-method error until this part of the code is executed. By contrast, in a statically typed language like OCaml, types are always known. We tell the compiler the types of things through type annotations.
00:06:49.020 They are essentially code that declares the type of all the variables. For example, here’s a function called 'abs' that takes an integer called 'n' and returns another integer.
00:07:09.240 You can't change the types of variables in OCaml; if you attempt to do so, the program will simply refuse to compile. So here we're trying to define a variable that holds the integer five and redefine it to hold the integer five as a string, and we receive a compilation error.
00:07:30.000 Additionally, you can’t even write code that mixes up types. In this example, I'm trying to pass our 'abs' function a string instead of an integer, and the OCaml compiler catches that.
00:07:55.380 The language has a powerful type inference engine, which means you often don't have to explicitly write down these types. However, it’s crucial to say that every variable type is known before the code runs, as long as it compiles.
00:08:15.060 Another major difference between these languages is the programming style or paradigm. For some of you who might be relatively new to programming, the idea of programming styles might seem odd.
00:08:44.760 These programming styles are significant and were revolutionary in changing how code is written and reasoned about. Generally, the most common paradigms worth discussing are imperative, procedural, object-oriented, and functional.
00:09:09.420 There are certainly more, and there will be debates over the lines between these, but these are the main ones when talking about programming languages you might choose for an everyday project.
00:09:28.620 Imperative languages, like assembly and the original BASIC, execute statements in order with no functions, where any sharing just involves jumping from one part of the code to another, typically done with commands like 'jump' or 'go to.'
00:09:53.820 We can even implement this in Ruby using a library from Benjamin Bach written around 15 years ago that lets you define labels and navigate to them. In the example, we’re defining a loop that prints a string seven times.
00:10:09.419 Here we define a label called 'loop' and then jump to that label seven times. Languages like C organize imperative code into subroutines or functions, allowing better organization, isolating variables within the functions they were created in.
00:10:29.760 This also avoids the problem of jumping to a part of the code that is outside the scope of your program. Object-oriented languages like Ruby or Java organize behavior into objects and allow sending messages to those objects.
00:10:46.440 Instead of functions that take parameters, there’s an implicit parameter called 'self,' making it so we have to refer to these as methods instead of functions, but the same idea applies.
00:11:06.600 Functional languages like Haskell focus on functions as their primary organizational construct. Functions can be combined, passed to each other, can contain nested scopes, and can call themselves.
00:11:26.720 This is an example of functional-style patterns in Ruby, reflecting the different styles I've described before.
00:11:37.220 You might be thinking, "Hey, John, you said that Ruby is object-oriented, yet everything shown – all the code examples – are in Ruby." That's true, and it's because Ruby, as many of you know, is multi-paradigm.
00:12:03.744 It supports all these programming styles, though it excels at being object-oriented. In fact, most languages today are multi-paradigm; when a new feature comes out in one language, the others tend to adapt it.
00:12:18.780 OCaml is also multi-paradigm; the 'O' in OCaml actually stands for 'object,' referencing the object-oriented nature of the language. Still, it is decidedly focused on being functional.
00:12:32.700 In the same way that Rubyists rarely write recursive code, OCaml developers tend to completely ignore the object-oriented features.
00:12:51.480 Despite their differences, these languages share something crucial: they are both expressive and prioritize making developers happy.
00:13:16.740 Ruby makes developers happy by providing the flexibility to express their ideas as they see fit. For example, you could come up with various ways of writing the various code snippets shared earlier.
00:13:34.080 On the other hand, OCaml ensures that certain error-prone programming practices become impossible, reducing the uncertainties for developers when writing or modifying code.
00:13:53.160 Having worked in Ruby for so long, I tend to approach OCaml with Ruby 'glasses' on, aiming for module interfaces that sound like sentences. I'm meticulous about the separation of concerns for different parts of my app.
00:14:20.700 I spend a lot of time ensuring my code is testable; however, when I return to Ruby and work on Rails, I’ve noticed the absence of some features from OCaml that enhance my productivity and simplify refactoring.
00:14:44.760 So, I think about the lessons learned at work and how they can translate back to Ruby and Rails. Today, I’ll be covering three features from OCaml.
00:15:01.440 For each feature, I will explain what it is, how it works in OCaml, a similar approach we can adopt in Ruby, and how it might be applied in a standard Rails application.
00:15:18.600 I want to issue a slight word of warning: if you're sitting in your chair right now thinking, 'I already knew that there are static types and dynamic types; this talk is boring,' you should know that it starts light but ramps up quickly.
00:15:30.720 The pace gets quite fast, and if something doesn't quite make sense, consider revisiting the topic or experimenting with it yourself. Some concepts didn't click for me until the twentieth attempt.
00:15:48.720 With that warning behind us, let’s dive into our first topic, which is probably OCaml's most fun-sounding feature: functors.
00:16:06.960 In OCaml, there are basic value types—think strings, integers, maps, and arrays—and functions that take in some number of parameters and return something.
00:16:20.700 For example, we have a string called 's', an integer called 'i', and a function called 'add' which takes two integers and returns a third integer.
00:16:37.620 There are also modules, which are groupings of functions that hold similar values together. For instance, here’s a module encapsulating a car wash, containing functions like 'wash,' which takes a car as input, returns a message, and outputs the same car.
00:16:54.960 A crucial note: these are not classes—there are no instances of 'carwash.' It lacks state, meaning the values cannot change. The car wash is just a bucket for loosely collecting similar items.
00:17:10.800 Modules can be included within other modules in OCaml, just as they can be in Ruby. One of the primary ways we avoid duplicating code for similar features is to include modules within other modules.
00:17:29.640 For instance, I may have a module for blog posts and a separate module for wikis, and if both require short URLs, I could introduce a third module that contains these methods, sharing the relevant parts of their implementation.
00:17:52.680 Many times, when we summarize this type of module sharing, there are significant details about the behavior of individual classes—like blog posts and wiki entries—that need to be integrated into how short codings work.
00:18:06.060 For this, we might require a method named 'has_shortcode', which necessitates the prefix. We provide the blog post's prefix as 'post-' and the wiki entry's prefix as 'wiki-.' We do it this way because 'include' doesn’t accept arguments.
00:18:20.520 In Ruby, there are two typical ways to handle this. The first would be to define methods with commonly known names; you define them on the including class, and the module assumes those methods exist.
00:18:42.360 For instance, 'has_shortcode' could make an assumption that the including module has a method called 'shortcode_prefix' and, when using the module, we would have to define that requisite method.
00:19:01.680 The second way that you'll often see in Rails, but might not have noticed or considered is defining a method like 'has_shortcode.' This presents a nicer interface by allowing you to write one line, passing in the prefix directly.
00:19:30.480 In fact, some library authors will register a hook and include the module directly into 'ActiveRecord::Base', allowing one-line usage of the module you created.
00:19:57.300 The implementation normally looks similar to this: we modify the outer class to store the necessary attributes. In this case, 'has_shortcode' serves as an ActiveSupport concern, defining two class methods.
00:20:12.840 One method is 'shortcode_prefix' via an attr_reader and the other is 'has_shortcode,' which is used to introduce behavior into the model. The 'shortcode' implementation can then utilize the 'shortcode_prefix' method.
00:20:41.460 With this approach, the methods we define actually live on every model, even the ones that don’t utilize shortcodes. There are indeed ways to circumvent this issue, but most libraries do not pursue them.
00:21:09.360 For instance, if you look at ActiveRecord::Base, you might be surprised to see many methods present that you wouldn’t expect. None of this is the end of the world, but functors, as I’m about to describe, provide a potential third approach.
00:21:32.880 In OCaml, functors can be thought of as functions from one module to another. They allow you to pass specific details to a module, modifying its behavior at the moment of creation.
00:21:54.840 For example, here’s a definition for a functor that takes in a module called 'M' and returns a new module, where the property 'x' from the original module is incremented by 1.
00:22:12.360 Now, could we create a similar concept in Ruby? This talk would suck if I said no, so that’s what we’re going to try to implement.
00:22:28.440 What we aim for is a class called 'BlogPost,' which is where we want to include the methods. It should have methods resulting in a short URL.
00:22:42.960 We need to introduce something in the middle to hold the prefix that allows us to hook into the inclusion point. We can define a module method on 'has_shortcode' for this purpose.
00:22:59.640 We'll call that method 'make.' This way, it has access to the prefix since the prefix gets passed into it, and because of the way we'll invoke 'make,' we have the opportunity to return a new module instead of just the one being included.
00:23:15.840 Creating the new module is trivial with 'module.new,' inside of which, we can simply include the original module, thus creating a proxy module to define the prefix method between ourselves and 'has_shortcode'.
00:23:31.680 Next, we define methods on the proxy module for each of the passed-in arguments. By far, the simplest way to handle this is to use 'define_method.' In this case, we're defining a method called 'prefix'.
00:24:01.560 The 'prefix' method will return the prefix that is within scope from 'make,' and voilà—we've built something functional using this pattern!
00:24:21.960 This implementation also ensures that no instance variables are created on 'BlogPost,' and consequently, all classes that inherit from 'ActiveRecord::Base' that do not require this behavior avoid unnecessary extra methods.
00:24:37.920 While it's unfortunate that this code is highly specific to 'has_shortcode,' it doesn't have to remain that way. We can encapsulate the entire concept into a separate module called 'functor.'
00:24:56.520 This function will feature a 'make' method that works like the previous implementation, but will also define methods for all possible arguments, allowing a more general tool.
00:25:18.960 This will provide the same behavior to any module we define and you'll see that inside of the short URL method, we will have access to 'prefix,' which comes from the proxy module.
00:25:35.880 Additionally, we will still have access to 'id,' which comes from the base model. It’s also possible to use this implementation with a more traditional API—you could wrap it all to conceal the internals.
00:25:51.120 By doing this, we create a simple Rails API while maintaining the benefits provided by the functor.
00:26:06.600 At this stage, let’s transition to the second feature, which focuses on a language feature of OCaml known as variance. With this feature, you can define a value that can be in one of several different states.
00:26:27.240 A common example is color values: colors can typically be expressed in many different formats such as RGBA, RGB, hex, or by name.
00:26:43.920 However, how would we handle this in Ruby? Personally, I would create a Color class with factory methods for each individual color representation.
00:27:01.320 In this example, we have an initializer that only accepts RGBA colors, and we define factory methods that essentially map other color spaces onto RGBA, normalizing all incoming color data.
00:27:20.760 While this might be a reasonable solution, it diverges from the OCaml implementation, as we ultimately discard the user's input. If we need to maintain a user interface that reflects how users input color values, we must somehow store that information.
00:27:42.960 To solve this, we want to represent and store a color in any of these formats while also minimizing database complexity, ideally keeping all color information in a single database column.
00:28:06.720 We can modify the Color class to store both the details of the original input while determining the normalized color only when we actually need to color something.
00:28:25.560 For instance, when initializing, we would take the label that indicates how the information was stored and retain all input values.
00:28:43.800 Thus, we would implement two methods: one called 'to_rgba' to actually use the color in the interface, and another called 'to_hash' to serialize the data into the database.
00:29:07.680 In essence, this establishes how we can replicate the concept of variance in Ruby. A variant is essentially a label and the data associated with that label.
00:29:23.520 While it could be problematic that any value could theoretically be passed to the label, we could enforce stricter checking on this by making the constructor private.
00:29:36.960 Furthermore, we could encapsulate this entire pattern into a library that simplifies creating and using these variant types, allowing us to discard factory methods completely.
00:29:55.680 For example, we might want 'variant.create,' where we pass all valid labels for that variant type, and in return, we receive a new class.
00:30:14.760 Then, with this new class, we can call .new and pass it a label; if we attempt to create an instance with an invalid label, it should raise an exception.
00:30:37.740 The implementation consists of dynamically generating the new class within the create method, specifying what labels are valid.
00:30:51.600 Creating a new variant with this API will return a class that descends from a parent class for shared behavior purposes.
00:31:07.440 It’s important to note that because OCaml is statically typed, it can go further with these variance types; it supports a feature called pattern matching.
00:31:21.600 This allows for clear handling using switch statements; you can succinctly define various states that the variable can hold.
00:31:39.960 Moreover, the OCaml compiler checks that all possible cases are accounted for, so if the code doesn’t handle every potential status, it won’t compile.
00:31:55.560 In OCaml, the variable must have a type, so if we skip, for instance, the RGBA case, any output to that variable becomes undefined.
00:32:05.760 In Ruby, we could simply return nil, but nil is of a different type, thus breaking the invariant of having a known type.
00:32:20.520 Thus, we may not compile an OCaml program with a missing case without handling it explicitly.
00:32:38.160 In Ruby, we can't prevent code from compiling if a case is missing, but can we implement exhaustiveness checks to avoid such gaps?
00:32:58.920 The interface I’m envisioning is a method on all variant types within the 'P' class. This method will accept a hash where the keys represent the label to match and the values denote executed procs for those matching labels.
00:33:24.060 The implementation of 'match' is straightforward; it checks if any cases defined are not properly handled. If any are left unhandled, we'll raise a 'UnhandledMatchCase' error.
00:33:50.520 We’ll also handle the cases where a default is provided, bypassing the raise. We’ll need to raise an exception for unnecessary cases. With this interface, we can identify dead code.
00:34:02.520 Through this use of match statements, we cover our bases on variants by identifying points where specific values are called but not handled.
00:34:18.720 Now we’re going to discuss the last feature for today: monads. In the rapid pace of conversation, I must clarify something I previously misspoke on.
00:34:30.600 When I mentioned the wildcard case of a pattern match, I indicated that it couldn't return nil because nil differs from string types. However, in several statically typed languages, null isn't a type but a value.
00:34:51.240 In Java, for example, you see that null can manifest for any type; it's often synonymous with absence of value.
00:35:06.780 In OCaml, however, null simply doesn’t exist. The absence of null helps to avoid the perils of unintended consequences often associated with null checks.
00:35:20.280 The option monad solves the problem of needing a variable that can hold either a value or no value using the OCaml work with variants.
00:35:41.520 The shape of the option monad strictly comprises two states: either None or Some containing a value, where Some is the method for holding a defined value.
00:36:10.920 The most common way to unwrap these options is by using a match statement keying off whether the value is Some or None.
00:36:30.960 These options can be composed easily by chaining calls, ensuring that code executes smoothly until a None value is reached.
00:36:45.480 While Ruby provides similar functionality through if statements and the safe navigation operator, such constructs can lead to mistakes if the developer isn’t careful with null checks.
00:37:05.520 The option monad pattern emphasizes explicit handling of possibly null cases through 'bind' methods, propelling developers to account for values that could be absent.
00:37:21.840 If there’s a situation where ignoring null could result in serious errors, this is a beneficial pattern to adopt.
00:37:39.840 It’s also worthwhile to explore other common monads, as each embodies a design pattern that efficiently conveys information about dealing with API responses.
00:37:53.760 As we close, let's take a moment to reflect. It’s understandable if you feel overwhelmed; there's been a lot of content presented today.
00:38:14.760 We've moved from possibly not knowing OCaml existed to understanding its contrast with Ruby, to dissecting how to transfer these functionalities back into our Ruby community.
00:38:32.880 I hope to have impressed upon you that there is immense knowledge to gain. Many concepts that appear as language features are fundamentally design patterns.
00:38:46.680 In a language as syntactically flexible as Ruby, we can integrate these patterns seamlessly. So, I encourage you to explore functional languages, declarative languages, or any language that intrigues you.
00:39:08.760 Do so not because you're abandoning Ruby, but because diversifying your language skills can enrich your overall development proficiency.
00:39:23.520 By exploring these various programming paradigms, we can enhance the Ruby community and ensure that Ruby and Rails thrive for many more years to come.
00:39:40.560 Thank you for your attention; I hope you enjoyed the closing keynote!