Talks

Ruby: Write Once, Run Anywhere

This video was recorded on http://wrocloverb.com. You should follow us at https://twitter.com/wrocloverb. See you next year!

Michał Taszycki with RUBY: WRITE ONCE, RUN ANYWHERE

Some people say that Ruby is dying. Bullshit! I'd say it's more alive than ever.
Ruby used to be a language associated scripting and web backends. Now, thanks to RubyMotion we can write desktop and mobile apps. While Opal allows us to execute Ruby in a browser. With these tools we can finally write cross-platform Ruby applications.
After this talk you'll know how and you'll be eager to try it yourself.

wroc_love.rb 2014

00:00:12.320 Are you ready? We go rock and roll! Yes.
00:00:23.119 I’m ready, but this is hard, really. I have some bad news for you about Ruby.
00:00:37.250 Some people say that Ruby is dying. Well, I don't actually agree with that. Yes, some people have said Ruby is no more, but I actually have quite the opposite opinion. Ruby right now is more alive than ever. Remember JavaScript? A couple of years ago, we reached a point in time when our computers got better, browsers became usable, and some people began extracting JavaScript engines outside of the browsers. They started using them in different ways. Some of those approaches were quite crazy, like Backbone, which opened the way for client-side applications, or even more ambitious like Node.js, which allowed JavaScript to run on servers. I certainly wouldn't allow JavaScript on my server, but many others embraced it. It blew our minds, creating projects like Meteor. There were valid cases for it, too, and people did some fascinating stuff like the milestone of a Commodore 64 emulator in JavaScript.
00:01:12.680 That’s something! Linux running on JavaScript? Wow! Quake III? That’s impressive. What could we do about it? We could only envy those guys. Sure, they have to program in JavaScript, which some might consider unfair. However, JavaScript was becoming ubiquitous, usable on both server and browser sides. This led to ideas of using the same code base across different applications. Now, we have the technology. We have Opal, which is a Ruby to JavaScript compiler. How many of you have used it in some serious ways? Okay, cool, just one person. Well, if you don’t know, it’s a Ruby to JavaScript compiler. It does something similar to CoffeeScript but arguably does it better. With Opal, we can compile Ruby into JavaScript, which is quite performant, albeit a bit worse than CoffeeScript. Of course, it’s not at version 1.0 yet, so I'd refrain from recommending it to everyone, but people have started creating some interesting projects with it, like Backbone-style frameworks or Meteor-like frameworks.
00:02:28.310 I think this is something we could really utilize, and it's actually quite simple to work with. In Rails, you just create a new Rails application and specify Opal in the configuration. It will work! You will be able to write your JavaScript in Ruby, which is pretty awesome. There's also RubyMotion, which is a better alternative for MacRuby. Unfortunately, it's a paid tool, but it allows for a lot of interesting functionality. Essentially, RubyMotion is a static Ruby compiler that compiles to Ruby 2.5 runtime. How many of you have used RubyMotion? Cool! You should definitely give it a try—if you have $300 to spare and want to develop applications for iOS and OS X, it’s a strong option. RubyMotion is currently stable and actively developed, so I think it’s something we should explore more. Additionally, the 1.9 version of RubyMotion was released just a couple of weeks ago.
00:04:09.329 I won't delve too deeply into Ruby since I am not an expert on it, but I know you can embed a virtual Ruby machine inside any C program, which is promising. Can you imagine someone writing a Commodore 64 compiler in Ruby? I would love that! That all sounds convenient, but are all these solutions truly Ruby? There are some drawbacks. For example, in RubyMotion, there is no 'require' or 'eval' at runtime. You can still do metaprogramming using methods like 'define_method' with blocks, but parsing Ruby source code is virtually impossible because Apple doesn't allow it.
00:06:02.909 There are also potential issues with multiple arguments due to Objective-C's different syntax for those methods, leading to different method names. For instance, you might see a method taking one argument and a hash, but it won’t actually be that method. Instead, it'll be a method with named arguments. You may encounter issues sending method names to objects running on RubyMotion. However, it’s not all bad. Another concern is that threads work concurrently in RubyMotion, so if you’re using threads in MRI, you might have some challenges. Memory leaks are also possible in RubyMotion, which is interesting since there’s no garbage collector; it relies on reference counting to clean up. Cycles can create dependencies that may lead to memory leaks.
00:07:29.669 On the Opal side, you’ll find that only static requires work because requiring files in a browser doesn’t really make sense. All requires in Opal translate into sprockets directives. Because performance is vital, there are a few changes we need to make. For instance, strings are immutable, symbols equal strings, and numbers are floats in JavaScript. However, Opal isn't complete yet; most functionality is present, but for example, you can't implement a 'for' loop yet. You can use an 'each' method instead, but nobody has implemented a parser for the 'for' loop. So, is that a big issue? It differs; they aren’t entirely Ruby, but they are sufficient.
00:09:14.430 What excites me the most about this JavaScript revolution that occurred is the potential for reusing parts of the code across applications running on different platforms. I believe that right now, using a subset of Ruby, we can achieve this, and let’s refer to it as cross-platform Ruby. If you see that image, it should remind you of something. Anyone have a guess? Yes, hexagonal architecture! So, let’s consider that—as in hexagonal architecture or ports and adapters—your whole application could essentially become an adapter, with the domain of the application encapsulated in this cross-platform Ruby. This can be repurposed across various platforms.
00:10:01.059 However, that’s not actually feasible; I can't imagine someone wrapping the entire application in an adapter. That idea seems quite ludicrous! The representation would likely be more complicated, but bear with me. The aim is to encapsulate components of the domain logic in our applications, either to expel something from it or drive the rest of the application through it. Let’s discuss the interesting aspects concerning interactions. First up, we have internal interactions. For instance, what happens when we have one subsystem in cross-platform Ruby talking to another? Implementing this can be exciting because we can use methodologies such as clean architecture or hexagonal architecture, DCI, command-query separation, or micro-libraries. To me, all philosophies can fit well here: domain-driven development, functional programming, even cowboy coding—this is your dream world!
00:12:10.550 So, what about simpler interactions? For example, let's consider an access case between an iOS application and a Rails application that needs some resources or needs to relay information to the core domain. An example here might be a translator subsystem that provides the same labels or resources for both the iOS and Rails applications. Creating a class and a getter will fulfill that need. Access from the outside can also give orders to the core domain. Let’s say we want to track user actions in our application. The user flow varies across platforms, but the mechanism for storing events can remain consistent, allowing us to provide the same interface for both platforms.
00:13:34.930 Next, consider interactions originating from inside the application and reaching outside. This typically relates to domain-driven development. The core of an application can instruct the adapter on what to do. For instance, if we have an image editor application aiming for uniform behavior in the browser and on desktop, we need to create abstractions across both platforms, perhaps encapsulating animations in a way that keeps functionality the same across both. That leads us to a somewhat complex scenario where the outside world sends information to the heart of our application and, at some point, perhaps asynchronously, the core of the application may need to engage with the outside world.
00:15:56.230 Let’s discuss a basic case: imagine a to-do application. We have a to-do item that can be marked as done. This is a setter for a done flag, and those are our plain Ruby objects carrying out tasks within the domain. Now, once we mark an item as done, we need to save it. How that save operation is defined may differ significantly: in a browser, saving could mean transmitting data to an API, while in Rails, it might simply involve saving to a database. There might also be conditions where saving occurs asynchronously or at another point in time. For instance, marking a to-do item as done might invoke further actions requiring another level of interaction with our application.
00:17:39.400 We can implement this in many different ways, such as having an abstract interface, using notifications, and engaging in aspect-oriented programming. An abstract interface necessitates modifying our domain class to accommodate the outside world. For example, we could create objects that save a to-do item and inject them into our to-do class, which requires changing the mark-as-done method. This process can be beneficial or detrimental, depending on your needs. Alternatively, we could utilize a notification system, possibly through events or data binding, which means we wouldn’t have to alter the core logic. The mark-as-done method wouldn’t need to know anything about saving the to-do item but would just signal that its action may interest others.
00:19:59.410 So, while the core remains unaffected in its logic, it must be aware that some interactions could trigger a notification. Therefore, for every significant action, we must provide a trigger that sends out those notifications. This method works especially well when the use cases across platforms are similar. However, one must remain cautious of memory leaks, as it's quite easy to form cycles in dependencies within RubyMotion.
00:21:29.210 Aspect-oriented programming is yet another intriguing concept. It’s somewhat a callback provided from a different perspective. For instance, if we want to avoid changing our to-do class, we can simply delegate the task of saving to another system—a.k.a. keeping our to-do class focused solely on domain logic. As you might imagine, to adequately save objects under such a scenario, our iOS or Rails applications require an external library.
00:22:41.880 That said, we aim to extract the common code base of our applications into a form we can share across different platforms while being aware of how different gems might need different configurations. When using Opal, for instance, it's crucial to let your upper library know, or you risk complications when bundling. If you're developing a RubyMotion gem, remember it won’t allow 'requires' at runtime. You need to set up your project to include all files from a gem within the configuration file. One effective methodology is to create a single folder for your applications along with separate families—Rails, iOS, etc.—for convenience. Extract common patterns into gems that are easily accessible.
00:24:53.600 Is this approach suitable for everyone? Certainly not! For example, at my company, Base, in Krakow, we’ve developed a product that brings joy to thousands of people daily, and we've cultivated a team of talented developers for each platform. Therefore, we wouldn't venture into cross-platform Ruby, although we do extract some functionality into gems when appropriate, particularly in our service-oriented architecture. However, creating cross-platform Ruby could be a brilliant strategy for certain scenarios, such as developing a multiplayer game prototype. In such a case, it's efficient to manage game logic in a shared gem, enabling smooth multi-platform functionality.
00:26:44.350 Does this work? Absolutely! I wanted to showcase this through examples—initially a sophisticated economic simulation, but since we're at a serious conference, I’ve created a tic-tac-toe application instead! Let me share it with you. So, what do we have here? This is essentially an iPhone emulator. You can start a game here, join a game, and even if I haven’t implemented automatic reloading, it’s still your turn! Okay, let’s try that again. Ah, this game is quite complicated, but there we go! I’ve won, and I lost. Fascinating! This application isn’t merely a tic-tac-toe game; it serves as a platform for creating multiplayer board games. Once it’s entirely developed, adding new games like chess will be a breeze!
00:29:40.390 To change the game, all that’s needed on each platform is to create a view for the game board. The rest of the architecture will remain constant, needing only a few modifications in perhaps three or four classes within the domain logic. The left side of the application runs Opal, including classes from the domain logic, while the iOS side operates similarly. Both communicate through a Rails application that employs the same classes from the domain.
00:31:04.330 Do we have five minutes? Awesome! Let me show you some source code; I hope it will interest you. Let's start with the directory. You’ll notice I have one directory for the iOS application, one for the Rails application, and another for my gem called 'turn-based.' Within this gem, there's an entire engine for the game, tailored classes for tic-tac-toe, and components known as interactors. These are quite simple. For instance, a 'move' is a plain Ruby object that can be saved and loaded as a hash or through something convertible to JSON. The other project component is the board game, which encompasses all the game's rules.
00:32:34.300 The true highlight lies within the interactors; although it might not be the perfect solution, it works effectively for my case. The intent is to abstract communication between the game logic and the backend. When a move is made, the local client verifies if the move is valid; if so, it transmits the data to the Rails application, which processes and validates it before saving it to the database. The response is then sent back, though it doesn't need validation on the client side—this is how they communicate.
00:34:52.610 In my case, saving procedures are consistently the same, whether through API calls or database interactions. Thus, I developed an interactor class used across all instances. It consistently accepts objects from the domain logic and delivers domain logic objects as well. For instance, looking at how we make a move, we take the game, player info, column, and row, instruct the game to build a move for the player, and finally, execute that move. At this stage, we ensure the domain logic has validated all elements, and our objects are valid for saving or sending.
00:36:55.490 In summary, I want to encourage you to consider using this concept in production. If I have captivated your interest, please visit cross-platform ruby.com, share your email, and I’ll keep you informed about further developments. I'd also love to hear your thoughts!
00:37:09.690 In conclusion, I firmly believe that Ruby is alive and thriving, steadily taking over the world, one platform at a time. Thank you! Any questions?
00:39:12.110 If you seek alternatives to Sprockets, can you still utilize Opal? Yes, Opal can be employed on multiple platforms, including Node.js. Currently, you cannot require files directly in Node.js, but that capability is on the horizon.
00:40:00.000 You mentioned that some code is shared between the server side and the Opal side. I would be concerned that someone could read the JavaScript code and discover how my server operates, posing a security threat. Yes, that is a valid concern. It's essential to decide what information you want to make publicly available, as anything in JavaScript is accessible for all to read. It’s wise to position your gems strategically and only expose what is necessary, maintaining a semblance of privacy.
00:40:48.960 Don’t you think developers tend to over-engineer solutions for front-end needs? Yes, indeed! I structured my solution this way because I plan to enhance it in the future to allow others to play against AI. Therefore, it was pivotal to set up the domain logic correctly from the outset.
00:41:13.930 Any more questions? Thank you!