Beyond Ruby with concepts from functional Scala

Beyond Ruby with concepts from functional Scala

After spending a couple of years working on various Ruby projects, I had the fantastic opportunity to join d.labs , a software development consulting company in Ljubljana. My main task was establishing the first and, to this day, the largest Ruby squad in Ljubljana. That was twelve years ago, and it's been quite a journey!

I also enjoyed being involved with the Ruby User Group in Slovenia. I loved organizing monthly events for passionate Ruby engineers and their friends. Together with Miha Rekar , we dedicated almost three years to creating a space filled with engaging talks, lively discussions, and the formation of many lasting friendships.

Since then, my career has taken me into the realm of Scala. I initially explored Spark, then Akka, and eventually delved into the Cats/Typelevel and ZIO stacks and ecosystems.

Given this transition, I felt a mix of emotions when Kri?tof ?rnivec , the new leader of the Slovenian Ruby community (RUG.si), invited me to share my journey of moving away from the Ruby world to embrace Scala at recent December's Meetup. The invitation posed an intriguing question: What makes Scala unique, especially considering its reputation for having one of the steeper learning curves?

Do you want to go faster? Don't limit yourself. Use the power that you have.

Going practical with BicikeLJ demo

The capital of Slovenia - Ljubljana, has a public bike-sharing platform called BicikeLJ. One can pick a bicycle from various locations across the city and then use it to drive around until leaving the bike at another station. Although the system is highly affordable and user-friendly, it has one flaw. When demand for these bikes spikes, getting or leaving a bicycle at these parking spots is challenging.

BicikeLJ bike station with a few three bikes. Foto: Uporabna Stran

To demonstrate the power of Scala compared to Ruby, I've designed this API where the user enters a location - as a string - and the system finds the nearest station with free - available - bicycles. To do that, it first resolves a string to coordinates (via Positionstack API), then gets information about stations (PromInfo API) and then, via the usage of a haversine formula, filters stations based on availability, sorted based on distance from a given location.

Simple, sequential representation of the "algorithm".

The implementation in Ruby is relatively straightforward and very readable... Call two endpoints, get JSON, parse, filter, sort, and serve the results.

Classical Ruby implementation. [Ruby]

And because the fetching of locations and the fetching of coordinates for a given location string are independent operations, they can be parallelised trivially. Even with Ruby - one can spawn two threads for each operation.

Look, my friend. Threads in Ruby! [Ruby]

How does walking distance sound to you?

To spice the challenge a bit. We can also change the requirements of the demo a bit, and we can query for available bike stations, have free bikes, AND have the shortest walking distance. To fulfil the latest requirement. One has to invoke another API then also to get the information. In our case Openrouteservice API. That means at least seven requests must be made to return the data for a single query - 5 closest stations. Again, a problem can be parallelised.

Diagram of operations that need to be executed.

Attempt with Ruby is still very straightforward and readable. However, cracks in the implementation started to emerge,...

Solution with Ruby. Getting stations based on availability and walking distance. [Ruby]

Bashing the Ruby solution

Weaknesses in the solution from the perspective of Scala / FP engineer. [Ruby]

When dissecting the problems with my Ruby approach and solution, I focused on a few issues that transcend this example but can also be nicely observed.

  1. Although both Python (typing) and Ruby (in version 3) have made significant progress towards supporting the types in various ways, shapes, and forms, it is still widespread to not use types in the codebase. With this example, the return type of the bike_stations method is still being determined. Is it an array? What are the "world" dependencies of methods used within? What are possible exceptions to the method? All that information can be conveyed to the user. It can also help the compiler/interpreter understand if the code that consumes this method can adequately handle or integrate with it. Imagine how many issues could be prevented if the computer could infer if things can work together or not...
  2. nil. Ruby's version is a billion-dollar mistake (Tony Hoare: The Billion Dollar Mistake). Methods everywhere can return nil, and that alone can represent many things. Not to mention that it is also pretty challenging to pinpoint the exact origin of nils.
  3. Support for concurrency and parallelism. As my first picture in this article and presentation jokingly conveys. We have so much computing power and support for solving problems concurrently - if your tools need to be smartly leveraging and harvesting that.. you are losing out. The Global VM Lock (GVL), also known as Global Interpreter Lock (GIL), is an implementation detail of the Ruby VM. At a high level, it prevents Ruby code across multiple threads from running in parallel (while still allowing concurrency!)

Two new concepts from the FP (Scala) world

Monads. One of the scariest words you can say out loud to a room full of software engineers has to be "monad". It can potentially alienate 3/4 of people instantly and put them in a deep coma.

My approach to introducing monads - common in Scala - especially if done the FP way - was to explain usage in familiar terms. Arrays are monads (and applicatives). You can wrap a function that returns a "nil" into an array and then use it with a combination of maps, flat-maps, filters, etc. So, you won't have to encounter problems with nils.

OMG, arrays are monads! [Ruby]

Effect system. The second concept is absent - for various good reasons - in the Ruby ecosystem. An effect system is a system that describes the computation effect (we can also call em' IO monads) into "programs"; and these programs are then executed within so-called runtimes.

Effects can interact with each other in the external world and can encapsulate pure or input computations. Because of the separation, we have a clear and neat way where clear definition and execution are separated concepts. I've demonstrated this concept in Ruby using Proc and simple Runtime that sequentially executes effect... The Effect / IO - monad in the example is named KIO, as the standard library already uses the class IO in Ruby.

And remember. The example uses sequential execution. With some thinking, one could also introduce parallel execution to definition and runtime... Thus, it provides an environment where total control over execution is provided and mixing different "modes" is supported.

The definition of effects / IO monads and the program on the left. Runtime on the right. [Ruby]

What about errors and retries, Oto?

We neatly separated the definition from execution via effects. We can quite trivially also write an example where runtime can use the retry definition and execute its logic accordingly.

Adding "retry" support to our runtime. [Ruby]

The Scala side of things

I've offered a neat and hopefully "universally readable" solution that uses Scala 3 and Cats Effect - pure asynchronous runtime.

Implementation of the "algorithm" with Scala 3 and Cats Effect [Scala]

Although most of the Ruby engineers who saw this example almost instantly understood the requirements and logic behind this solution... there are a few immediate questions. Why is there a "for" underneath the "near" function definition? A prevalent question is if you need to remember that you are looking at monads and that a lot of mapping and flat mapping is required to work with them. Scala addresses this problem with for-comprehensions or, in a few examples - such as with ZIO - using the so-called direct style syntax that eliminates it.

OK. How about the second case - where we also want to incorporate the walking distance requirement?

Max parallelism, walking distance included. Without for-comprehension... [Scala]

The second example still needs to be received, as well as the first one. Most of the confusion from Ruby engineers came from an "overload" of maps/flat maps. And the question what do functions parTraverse and parFlatMapN do? I do not know that these functions define or combine parallel execution of IOs.

What is the deal with types?

  • The Query and Size are two custom types. Both are defined via the Refined library that puts additional type constraints to the code. Size containers are to be positive and not greater than five, and the Query is constrained by being a non-empty string.
  • IO[List[Destination]] - telling us that this function defines IO that has only two possible ways where it can fail. The positive value is - a list of destinations, and the other is - the negative exception of sub-class Exception.

The Scala wrap

To complete the demonstration, I also presented a working Scala HTTP server and an API that serves near-walking distance stations for a given query. However, the list of bike stations is refreshed internally - every 10 seconds. This means that in the request-response interaction, only decoding of location string to coordinates is needed, and the requests to get the walking distances.

With this example, I also wanted to show the power of expressiveness and composability that can be achieved using a functional approach.

Full Scala demo with Ref, loops and server. [Scala]

Careful Ruby engineers have also observed the usage of Ref, which ships with Cats Effect and can be used as a thread-safe data structure to move the data around your application. There were also questions about the meaning of Resource. A typical pattern is to acquire a resource (e.g. a file or a socket), perform some action on it and then run a finalizer (e.g. closing the file handle), regardless of the outcome of the action.

Summary

There is magic and beauty in both languages. I like them both; both languages are exciting and appealing from the perspective of practical reasons. Both share a collection of quirks and imperfections and have found their audiences and users in various industries. Ruby's emphasis on simplicity, readability and developer happiness is something that Scala would greatly benefit from. And on the opposite side - Scala could sometimes be a bit less sophisticated, academic and perhaps just a bit more comfy.

I hope I've managed to convey the elegance of both languages and inspire curious Ruby engineers to explore these concepts further.

Source: GitHub - otobrglez/beyond-ruby

Slides: beyond-ruby-scala.pinkstack.com

As for performance && speed - Scala is faster. ??


Teo Winkler

Front-end developer ??

1 年

I never worked with Ruby or Scala, but this was a fun read and Scala looks intriguing! By the way, there is a small typo '... room fool of ...'. ?? thx for sharing ??

要查看或添加评论,请登录

Oto Brglez的更多文章

  • Engineering Managers, what is something you wish someone had told you about the role before you switched from engineering?

    Engineering Managers, what is something you wish someone had told you about the role before you switched from engineering?

    I stumbled upon this lovely question on X, posted by Emma Bostian, Engineering Manager @ Spotify. I felt the need and…

    5 条评论
  • Becoming S/W Engineer

    Becoming S/W Engineer

    I was invited to speak to an auditorium of young high school students not so long ago. A few hundred teens, full of…

    2 条评论
  • The compression puzzle challenge

    The compression puzzle challenge

    Life is short, exciting and full of small events. Small events can change your daily life and sometimes inspire you…

    1 条评论
  • Cracking the WORDLE game

    Cracking the WORDLE game

    If you’ve recently opened any social media app, be it Facebook, Twitter, or if you are a vivid reader of Forbes or…

    3 条评论
  • My hacks, experiments and products of 2021

    My hacks, experiments and products of 2021

    As the year 2021 has finally ended and we open the new chapter of 2022 - I've taken a couple of minutes and composed my…

    1 条评论
  • The four dusty pillars of software engineering

    The four dusty pillars of software engineering

    As organisations are finishing the last quarter of 2021 and as we put all our heads together and try to compose the…

    1 条评论

社区洞察

其他会员也浏览了