Euruko 2018

Ducks and monads: wonders of Ruby types

Ducks and monads: wonders of Ruby types

by Igor Morozov

In the talk 'Ducks and monads: wonders of Ruby types' by Igor Morozov, presented at EuRuKo 2018, the speaker explores the importance and implications of types in Ruby, a dynamically typed language. Morozov begins by expressing his self-doubt as a software engineer, emphasizing how mistakes and errors in code can lead to significant issues. He outlines four primary reasons for his lack of trust in his coding abilities:

  • Undefined behavior: Encountering errors like 'undefined is not a function' due to expectations of variable assignments that aren't met.
  • N plus one errors: Realizing this error type after working extensively with Rails, where missing data leads to inefficiencies.
  • Complex domains: Mistakes arising from intricate application designs.
  • Syntactic errors: Simple typos like missed brackets or quotation marks that can derail coding efforts.

Morozov emphasizes that while Ruby’s dynamic nature offers flexibility, it necessitates rigorous attention to data integrity. He cites a Rollbar report that highlights that a significant percentage of common errors are type-related, underscoring the importance of maintaining accurate type handling.

He discusses the misleading practices introduced by some features in Active Support which can treat different data types as equivalent, potentially leading to errors. To combat these pitfalls, Morozov advocates for leveraging libraries like dry-types to enforce type safety, allowing developers to define data structures more clearly.

A significant portion of the talk delves into monads, specifically the Result monad (also known as Either), which is a structured way of handling success and failure in applications. He explains how monads can simplify error handling and promote cleaner code by allowing methods to chain together without cluttering the application with error-checking boilerplate.

Morozov concludes by emphasizing that using types effectively can lead to better application design and reduce errors, suggesting that every Ruby developer should explore the concepts of type safety and monads in their coding practices. The key takeaways from his presentation include:
- Utilize appropriate libraries for type safety.
- Reconsider reliance on features that can lead to ambiguity in data handling.
- Employ monads to manage application errors effectively while maintaining readability in the code.

The talk asserts that understanding and applying type principles is not just beneficial but essential for the development of reliable and maintainable applications.

00:00:00 It depends on our next speaker, really. No pressure, but I want you all back here at 3:00 p.m.
00:00:05 I need to do some switching on the computer. This might or might not work; I'm already lost.
00:00:11 Igor Morozov is an engineer from Moscow and he works here at Clean. He was one of the first people to work with very cutting-edge technology like ROM.
00:00:24 He’s here to tell you about it because he cannot stop talking about it. So, there you go.
00:00:46 Hello everyone, my name is Igor Morozov and I'm a software engineer from Moscow. I work at Clean, and my primary job is related to Ruby. I also have some commercial experience with Python, JavaScript, and Reason.
00:01:01 I've come here basically to confess that I don't trust myself. It's not some kind of insecurity issue, as you might think; I just don't trust myself because I know that I'm a software engineer who makes mistakes.
00:01:15 There are four reasonable explanations for why I don't trust myself. The first one is 'undefined is not a function.' That's practically the reason I can’t trust myself.
00:01:34 You see, sometimes, especially in dynamically typed languages, I expect a variable to contain something, and it just doesn’t.
00:01:59 The next reason for not trusting myself is 'N plus one errors.' Until I switched jobs and began working with Rails, I had used ROM for over two years, which accounts for nearly all my prior Ruby experience. It was then that I realized that N plus one errors are real, and I must be extra careful about them.
00:02:11 But I really can’t concentrate to ensure that I will never forget to include something, and well, sometimes I just make those mistakes.
00:02:25 Then we have complex domains with numerous conditions. In enterprise application design, it is easy to mess things up. For instance, I can send a cleaner from Moscow to clean an apartment in St. Petersburg, and those kinds of things happen.
00:02:55 Thankfully, we catch such mistakes early. So the first reason I don’t trust myself is that I sometimes make silly syntactic errors; I forget to close brackets, add an extra one, or forget about quotation marks.
00:03:12 The last reason is that I forget some details. There’s always something small that I can't handle, and I make mistakes. To actually deploy working software, I have to double-check myself.
00:03:40 One of the solutions I’ve discovered is to use the right tools. I already use code reviews, linters, manual testing, and automated tests, but I feel that it is not enough.
00:04:02 What I really love about programming languages is they serve as tools to build the right code. I use Ruby's type system to ensure my code works correctly.
00:04:21 Before we start discussing types, we need to answer one question: do we really need to care about types in Ruby? Ruby is a dynamically typed language; it doesn't have type annotations and thankfully it won't.
00:04:46 But do we really have to care? The answer is yes, we actually have to care. The dynamic typing is not an excuse to be reckless with our customers' data. It’s practically our job to keep that data safe, transform it, and store it.
00:05:04 We don’t want anything to go wrong with it. Rollbar recently published a report analyzing over a thousand Rails applications and collected a list of the most common errors. I noticed that two out of ten most popular errors are related to types.
00:05:36 For example, trying to iterate over a nil and trying to get an ID on a nil. These errors may not seem significant; you get an exception, fix it, and move on. However, what we must truly avoid is silent data corruption.
00:05:54 One significant case from my experience is related to Rails' JSON field. A while ago, you could just serialize your JSON into a string and store it in a model, and the model would try to parse it into a hash. But now, that’s not how it works, and apparently some people didn’t notice this during the upgrade. Neither did we.
00:06:21 Some data might have been corrupted, but thankfully we caught it early and fixed it. I want to prevent such errors, and to do that, I need the right libraries.
00:06:37 When I talk about the right libraries, I don't mean code reviews or specs; I mean libraries and frameworks that help to build our code.
00:06:46 Writing good code is significantly easier than fixing it, especially when we talk about types—specifically in Ruby.
00:06:59 Every object has a type, and when we talk about data types, we must ask ourselves: what is actually a type?
00:07:08 Fortunately, in 1976, professors Parnas, Shore, and Weiss published a paper analyzing different usages of the term 'datatype'. They provided five definitions regarding syntax, value, space, behavior, and representation of a type.
00:07:37 I believe the most useful definition is that a type is a set of values that a variable can possess and the set of functions that one can apply to them. This perspective is probably the most intuitive way to understand types.
00:08:05 If we consider an integer variable, we know it only contains specific values like -1, 0, 10, 11, and volumes of possibilities. We also understand that we have a limited set of functions we can apply to those numbers, such as subtraction, addition, and multiplication.
00:08:26 The beneficial aspect of this definition is that it encourages us to consider all possible values. We must evaluate every possible value of a variable to avoid mistakes.
00:08:54 From the Rollbar report, it is evident that mistakes often happen because developers do not accurately evaluate the data they pass around.
00:09:13 Errors occur when we try to call a function that doesn’t work with nil. Moreover, it’s essential to recognize that nil does not belong to every type.
00:09:33 For instance, if you have an integer, you shouldn’t encounter a nil value.
00:09:45 In languages like Java, we are taught that every type has a special value that we need to treat carefully. You can’t use nil the same way as objects or arrays.
00:10:05 Many of our commonly used tools can lead to recklessness. For instance, Active Support introduces methods like blank and present that mislead us into treating nil, empty strings, and empty arrays as equivalent.
00:10:40 It’s simply not true that an empty string and nil are the same in your application’s semantics. While they may not contain valuable information, they are treated and processed differently in code.
00:11:09 I urge you to remove those methods from your code, not just by replacing them with similar checks but by fundamentally reconsidering the design of your data. Ask yourself why a variable can be nil and whether it’s necessary to call a method if it can be nil.
00:11:47 The great thing about these reflections is that removing those common pitfalls leads to better application design. However, I cannot trust myself not to make silly type mistakes again.
00:12:14 Whenever I have to handle types, I need a tool that will indicate when I'm wrong. It would be advantageous to have type inference; however, Ruby does not yet provide this.
00:12:36 Therefore, I have to rely on libraries like dry-types, which offer a simple, extendable type system written in Ruby. The great benefit of this library is that it works effectively.
00:13:08 If I define a string variable, I make sure to use the correct constructor, and this immediate checking helps catch mistakes before they become problems.
00:13:23 The best aspect of dry types is that the library warns me if I pass anything other than a string; it ensures that I design my application around this structure.
00:13:49 When I have more complex definitions, such as enums or schemas with hashes, the library supports it, leaving the decision of how to implement it firmly in your hands.
00:14:02 Additionally, by using dry types, you can maintain your application architecture without losing its structural integrity. The journey to a type-safe Ruby application begins with properly describing your data.
00:14:25 The first step involves utilizing dry types to craft type-safe applications by defining your data structures, ensuring that typing remains consistent throughout your code.
00:14:47 Static typing in our programs helps us describe our data accurately. If you have mutable data, implementing these definitions in your constructors is crucial. Furthermore, if you are bold, consider rewriting your applications using dry structs.
00:15:14 There are three primary approaches to achieving type safety in Ruby: first, cease using certain features of Active Support as they may lead to inefficiencies in your code; second, ensure that your application effectively considers all possible values.
00:15:47 Lastly, we should leverage strict constructors to prevent common mistakes, which effectively mitigate errors; languages with robust type systems allow us to express these problems.
00:16:01 In Ruby, we can express errors in various ways using types.
00:16:05 Result objects, for instance, have gained more attention as they express errors in the language. They teach us that errors are not exceptional; in reality, they occur frequently due to complexity.
00:16:31 Result objects push us to consider processes that could go wrong. Good types facilitate error handling, making our applications more robust.
00:17:01 However, one concern about result objects is the lack of a universal result object to unify application errors. Often, each project requires you to build your own result object, cumbersome in practice.
00:17:23 Fortunately, we have a mathematically sound abstraction for result objects known as monads. Monads come from category theory and functional languages, working exceptionally well in Ruby.
00:17:46 Moreover, a monad is merely a result object that abides by certain rules. We have a gem called Chime which contains various monads, including useful implementations like Either, Maybe, and Try.
00:18:04 Today, I'll concentrate on just one of them: the Result monad, also known as Either. A result is a composite data type consisting of two subtypes: success or failure.
00:18:34 Practically, these cases provide containers for different data types. To use the result in your applications, all you need to do is install the gem, require it, and utilize constructors for success or failure.
00:18:58 Monads are hot topics in functional languages and category theory, so they provide significantly more than just data containers. A critical aspect of monads is how they help build pipelines for data transformations.
00:19:23 One method that aids in this is the bind method, which applies a given block to a successful value, allowing for complex chaining without sacrificing readability.
00:19:44 However, bind only executes smoothly for successes. If a failure arises, it halts the execution, preventing any further processing.
00:20:03 To address failures, we also have an or method, which functions similarly to bind but skips processing when it meets success, allowing for flexible error handling.
00:20:25 The problem with these methods is they must return a monad. If your application is not built with this design, integrating them can lead to cumbersome boilerplate code.
00:20:59 Fortunately, we can utilize a method called fmap, which is akin to bind but wraps the outcome back into a success.
00:21:13 This flexibility enables us to employ Ruby's built-in methods without fully adhering to monadic principles, allowing for greater versatility in writing code.
00:21:28 However, in Ruby, we need to escape the context of monads since it does not provide features available in functional languages like Haskell.
00:21:49 Thus, we utilize a method called 'value,' akin to fetching a value from a hash. This function retrieves an unwrapped successful result.
00:22:06 If it's a failure, we can return a default object, ensuring that applications remain predictable and efficient.
00:22:36 Sometimes, we may not care about the success or failure; we simply want an error message. We could provide an identity function in those scenarios.
00:22:58 Ruby supports an identity function, named 'itself,' which returns the object itself. Hence, if we disregard type safety or the outcomes of operations, we can use it to streamline the process.
00:23:30 However, chaining computations can get convoluted with multiple nested values, conditions, and various pathways that can complicate our code.
00:23:55 We can utilize do notation to simplify these processes. This concept borrowed from Haskell enables us to write concise Ruby code that interacts efficiently with monadic values.
00:24:22 By applying this concept, we execute methods that return a result while streamlining the code involved, ultimately retaining ease of use without losing Ruby's characteristics.
00:25:14 To recap, types assist in crafting applications, allowing for well-maintained structures using the right design principles.
00:25:40 Active Support may include unnecessary methods that introduce confusion, such as blank and present which are rarely justifiable.
00:25:56 Finally, utilizing dry types leads to the design of applications that are inherently type-safe and manage errors gracefully with the help of monads.
00:26:15 I have compiled references which will be published later. If you’re interested in exploring Hanami or similar resources, I encourage you to check those out.
00:26:45 If you're curious about the intersection of programming and category theory, feel free to reach out. Thank you.
00:27:04 I have a clicker now; we're more dangerous with each talk. This is a break, and I want to see all of you back here at 3:00 p.m. Thank you.