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?
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.
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.
The implementation in Ruby is relatively straightforward and very readable... Call two endpoints, get JSON, parse, filter, sort, and serve the results.
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.
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.
Attempt with Ruby is still very straightforward and readable. However, cracks in the implementation started to emerge,...
Bashing the Ruby solution
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.
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.
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.
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.
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.
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?
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 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.
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. ??
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 ??