I would love to find an MVC framework with the extense library support of Rails but for Javascript. Being able to simply `gem 'devise'` and have full authentication at your hands (including views, email templates, etc) is a godsend.
At many places where I've worked at this paradigm is exactly what is needed, but people know and want to use JS. If we choose to go that way then, we need to write a lot of glue code and think about setting up architecture standards, folder and naming conventions, ORM support, authentication, job queues, etc; all of this comes by default with Rails and makes life so much easier.
Lately some options seem to be gaining steam:
* RedwoodJS [1]: by Tom Preston-Werner (founder of Github), comes with React and GraphQL baked in, pretty new project though
* Next.js [2]: brings a lot of nice conventions to the frontend, but still some things missing on the backend side
* SailsJS [3]: similar to Rails, but small community
* Strapi [4], hapi [5] et al: plenty of backend functionality, but frontend agnostic.
Has anybody had experience with any of these (or others) and can report back?
I think this is a very uphill battle because JS and most other languages are missing a key ingredient that Rails has had for 10+ years. It's having a long running large real world SAAS app that Rails is extracted from.
That means every feature of Rails is derived from a practical use case, and then it's well tested in that app before it's pushed out to the community. That's tested from an API and functionality point of view. By the time end users start to use it, the feature has likely been running in production for 6+ months. Maybe even serving billions of page views through it if you factor in Shopify and GitHub also running off Rails master.
It's all constantly in use, kept up to date and iterated on as a whole.
Creating a starter kit / boiler plate of libraries and opinions in another language / web framework is not the same. I think this is why even after 10+ years there's only a handful of popular Rails alternatives in other languages.
There's plenty of JS code written out of the real needs and by teams running huge projects, take just Facebook as an example. Problem is that there are thousands of solutions for everything, and that saturation and bad signal to noise ratio makes it hard sometimes to pick the right lib (or even hear about it in the first place). Rails have a huge benefit of having a single team behind it, so you always have a recommended way of doing things, and the whole community is smaller, less libs to test, less rubbish to avoid, making the experience much more linear...
I don't have a recommendation, but here is a quick test I use when looking for a replacement for Rails:
How quick can I create a minimum simple SaaS like application that has the following functionalities:
- User management (Signup/Login/Reset password/Confirmation...)
- Simple payment processing (like have on single plan, one time fee, and after paying the user will see some other default page then not paying)
- Sending emails in HTML and plain text (for user management and after payment is processed)
- Use slugged/permalink like URLs
- Processing some task in async queue
And so far there are a lot of alternatives out there but for me Rails provides the easiest experience to have this up and running quickly as there are already battle-tested gems for almost all these functionalities.
It's got a very helpful transaction block wrapper structure that allows you to do some really interesting things with database transactions.
To be perfectly honest, one of the things that I always love about Rails is exactly how good the database layer is with ActiveRecord. I like using the advanced features of my database, especially PostgreSQL, and I've found that it's really easy to do with Rails.
The "Scopes" feature of active record that let's you save small snippets of a query and chain them together is fantastic. I've used it to create complex filter functions that combine full text search and coordinate distance as simple methods that can be attached to any query.
Being able to add a `.search("fender").distance_from(location, 5, :miles)` onto any query made my life so much easier.
It's been a few years, but I wrote about some of my favorites.
Rails has support for database transactions, but depending on what you want to do, things won't be magically transactional all by themselves (that said, it's really just a matter of wrapping transactional code in a block).
There are a couple of footguns in Rails involving transactions - by default, validating uniqueness of a column in Rails will be done at the app layer which is prone to race conditions, and with a typical CRUD model for an application, there's nothing in particular preventing overwrites of model data if say, a field was present on a form and two people were editing it, even if the "later" editor made no changes to that particular field.
When multiple systems and things other than databases (or multiple databases) are involved, you're generally on your own as well.
It's important to say that these footguns are not exclusive to Rails. I don't know a single web framework which avoids them - if it exists I'd like to know so I can learn how they do it.
Elixir's Ecto has the best solution to this problem in my experience--it doesn't do database validations on things like uniqueness, but provides easy error handling when constraints are hit.
It encourages you to write validations for things that don't need to hit the DB, but actually uses the DB for handling constraints, since it's the real source of truth for data consistency.
Most CRUD applications go the one extra step of putting a uniqueness constraint on the column instead of just relying on the app layer validation, and comparing hidden time stamps on the form if multiple people might edit the same row simultaneously. It’s exceedingly straightforward.
Did you really think thousands of web apps up to and including GitHub and shopify were squeaking by on blind luck? If basic database transactions and integrity weren’t doable in Rails, that’d have had to have been fixed years ago.
Most of your basic questions are covered by the starter Rails Guide for ActiveRecord, eg) transactions, validations, how to add a uniqueness constraint to the DB in a migration
Strapi [4] is much less stable than it looks. It's pretty buggy, isn't designed very well, and is just all around messy. I used it in production once and never will again.
Next.js [2] is well designed and well maintained, but is totally a frontend framework. It's not just "missing a few things" on the backend side, there is no backend side. Works great if you already have an API (JAMStack) but can't stand alone in the way that Rails or Laravel might be able to.
Despite being the youngest, RedwoodJS [1] is the best option here. I was actually involved pretty early on as a contributor on the project before I had to step away (unrelated, personal reasons) and was seriously impressed by their progress. TPW runs a tight ship, and all of the devs that work on the project are smart and amazing. They ship very quickly and it's rapidly growing into the closest thing to a Rails competitor in Javascript. I wouldn't be surprised if it takes the JS world by storm in a year or two.
> Being able to simply `gem 'devise'` and have full authentication at your hands (including views, email templates, etc) is a godsend.
I'm on the other end of the spectrum.
I mean, I see the benefit of such "roll X quick by adding a gem" for a proof of concept, a demo or even an MVC.
But even for the latter, it breaks down rapidly. Such gems bring their own libraries. They force you to "do it their way". A gem like devise comes with its own addons (omniauth) that paint you even quicker into a corner.
I've consulted and developed at many Rails codebase where most of the problems were caused by years of development on top of such gems. A weak, wobbly base, and the entire castle built on top of that.
Where 90% of the devise features were unused or worked around. Where more code was present to customise and work around devise-isms, AASM-weirdness, CanCan-troubles, than a complete "roll your own from scratch" would have ever caused.
So, my "devise advise" always is: sure, use it to get your product to market rapidly. But be sure to pull it out and replace it with a focused, fitting architecture, sooner rather than later.
Such gems fit very well in the problematic development-workflow where one "searches for an existing gem which offers some version of Foo" rather than "figuring out how we want to do Foo, then look for a gem that does it our way".
I think this is due to the extent of the maintenance efforts by the team that develops the app, not due to the gems themselves. A monolithic app means that no code is legacy code: a conscious, gardening-like effort needs to be continuously done to keep interfaces standardised, modules documented, code clean, and critical components (like user management) top-notch with focused testing and code quality metrics. I've worked in different teams on 5+ year codebases that hold up to this day by being disciplined on this terms and never buckling down on complexity.
What I've also seen is that a team with plenty of churn or no time for maintenance will have exactly the problems you describe. As knowledgeable people leave the team, the old code becomes "legacy", new people come and partially refactor it, maybe adding some new gem and planning to migrate the entire codebase later on to it (which is very hard to make it happen). Stakeholders' pressure will force the team to focus on delivering features and only tackling debt when it gets to an unattainable state, and then is when consultants are called. I think this is why many freelancers suffer the worst of Rails (or any other framework) if they don't usually work with greenfield projects.
To continue the "gardening" analogy: sure. Continuous pruning, weeding (refactoring) and fertilizing (keeping up to date) is needed.
But this is easier and more rewarding in a garden that is well layed out and allows flexibility to move things around.
An old, crumbling shed, in the middle of that garden limits what you can do with it. It is always in the way, always visible and often defines the entire garden, when you really don't find it that important.
A gem, like devise, defines what your users are (it limits your freedom to define the Domain model), how your onboarding flow works (which is probably your most important money-maker) and what features you can implement and what not.
When you started with the garden, it was handy to have a quick and ready place to store your tools. So the first cheap, off-the-shelve shed, is the best solution. But once your garden is defined, you probably want to move it aside, nicely integrated in your garden and well maintained and exactly large enough for your tools, so it leaves as much space for your flowers and trees -the stuff that matters-.
It's crazy to me that every other language doesn't have a Rails, literally just a clone. It has been so obviously valuable, and is now so well understood.
We were using AOLServer and its clones, Vignette, Zope before Rails was invented.
Most of the Java and .NET world eventually moved into full blown CMS, with component frameworks, graphical layout editors, user management and third party connectors to distributed platforms.
For me, Rails just feels too little, with a quite slow language.
I like using Django [0] with django-rest-framework [1] and dj-rest-auth [2]. This gets you Rails-inspired organization, with a nice abstraction for REST endpoints in general, and a login/logout/registration/email verification/social auth in particular. Everything is built on the very well maintained allauth [3] module. Here's a good quickstart [4]. I've used Ruby in production, but I like Python's "one way of doing things" approach.
I've had good experience using Nest.JS [1] for the backend and next.js for the frontend. Nest has defaults for ORM, validation, task scheduling, authentication, etc, I find it a lot easier to work with when compared to plain express projects with tons of glue.
I'm still amazed at how Python code with type hints looks so different to Python without them. Consider the program near the end of the article - if we remove the type hints:
- The `AstNode` abstract base class becomes completely unnecessary and can be deleted.
- We can't use `@dataclass` and are forced to write an `__init__` method for each class. But this is good, right? "Explicit is better than implicit"?
- It's then clear that our three classes each have two methods, one of which is __init__. Following the advice from "Stop Writing Classes" [0], each class should just be a function (in this case, a generator).
These changes reduce the code above "AST representation of the Ruby statement" from 70 lines to 40, and simplify some of those lines as well - for example, `arg_reprs = (arg.representations() for arg in self.args or [])` becomes simply `arg_reprs = args`.
The knock-on effects of type annotations seem to be doubling the complexity of the code. This is why I'm far from convinced that static type checking in Python is worth the cost.
> We can't use `@dataclass` and are forced to write an `__init__` method for each class. But this is good, right? "Explicit is better than implicit"?
I don't think that applies. @dataclass is an explicit wrapper with specific result and can be inspected and interacted with.
Implicit would be something like "when a class has more than one attribute and doesn't define init or repr, add those methods assuming they're wanted/needed".
> The `AstNode` abstract base class becomes completely unnecessary and can be deleted.
You're mixing type annotations with class usage. You can have either one without the other.
For example without AstNode you could still have an `Expr|MethodCall|Lambda` type hint on relevant variables. Or even alias that expression to AstNode without having the class and inheritance.
Finally, classes help describe/construct the structure present in "stmt". Sure, you could replace those with functions, but that means your data is either reduced to untyped / string-tagged lists/dicts, or you're running the exact function immediately inline. It doesn't matter for this toy example, but it would in any practical use of this code.
@dataclass is one of those ones where it depends on how you look at it.
To me, it's a great application of "explicit is better than implicit." Because @dataclass explicitly indicates that this class has a certain set of behavior, and is meant to be used a certain way. If you manually implement exactly the same behavior, you've made the individual statements more explicit, but you've made the class's actual function implicit. A reader needs to figure it out by reading the code and working backward from there. An editor of the code needs to recognize the intended behavior and not break it, otherwise the understanding that other people built up by reading the code may break in subtle ways.
YAGNI is a double-edged sword. It's almost no additional overhead to write a class now if you think you might need to add methods and internal state later. But it's quite a bit of refactoring work if you decide not to write a class and then realize you need one after the fact.
Maybe this is an indictment of Python as a language, but I think it's a case of not taking anything too literally. YAGNI fetishism is like DRY fetishism all over again.
I guess? At the same time, working in some languages (Racket and C come to mind), it's surprising how long I can go without anything ever becoming a class, ever. Seemingly indefinitely.
My sense is that many of us are taught to think in terms of classes and methods instead of data structures and functions, so it's very easy to slip into thinking you need objects in order to manage state, even when that's not the simplest way to do it.
Strictly speaking, though, I think that the only time in Python that you really have no workable alternative to classes is when you also need dynamic dispatch.
Personally: it's much easier to YAGNI in languages with relatively-strong compile-time checks and relatively-sophisticated tooling.
If you lose either of those, a little bit of preventative work goes a loooooong way. The friction (largely due to risk) of correcting things later is so large that it just starts snowballing unless your team is extremely attentive.
In that sense, Python is... middle of the road, with an extremely broad spread. Tooling is an odd blend of surprisingly good vs incapable of doing simple inference / detecting all calls, and type safety depends heavily on how you write things (some tactics are fairly strong, many are so weak as to be useless or make things much worse).
Python has a great type checker. Several, actually. They're not compile time, because Python doesn't exactly have a compile time, but still. 11/10, use everywhere, spend less time on unit tests, be happy.
Second, to me, these sorts of concerns about refactoring are. . . maybe not a smell, but worrisome, all the same. They suggest poorly-modularized designs with no internal component boundaries that might have limited the scope of impact of such a change. That said, even if you do find yourself needing to do a far-reaching refactor like that, refactoring in a dynamic codebase isn't necessarily harder, it's just different. Not being able to lean on the compiler to help you boil the ocean, for example, is fine, because boiling the ocean wasn't necessarily the best approach in the first place. The strangler pattern will help here, not just with type errors, but also with other problems the compiler might not have been able to help with, and will also be friendlier to the people who do your code reviews.
> Python has a great type checker. Several, actually. They're not compile time, because Python doesn't exactly have a compile time.
Python does exactly have a compiler time, since it actually compiles things.
The typecheckers are run before that (but I think that's usually true of “compile time” checkers, too, they just don't break between checking and compilation; the analysis I would imagine almost invariably comes before actually generating compiled code; especially for languages where, unlike Python, typing effects what code gets generated rather than only whether the typechecker passes or fails.)
To echo the sibling - I believe the most important skill a developer can have is reworking* code, and doing it readily and continuously. If you can do that, then you don't ever have to worry about what code MIGHT do in the future, you just design it for what it DOES do and then rework it later if necessary. It's quicker and the result is cleaner. Also, even if you tried your hardest to predict what was going to be needed, there are always things you can't predict and at that point you either rework your code or have bad code.
* I could have said refactoring, but some people have very specific definitions attached to that word so I use something more general here
I don't think you can draw that conclusion from that code. I've been using Python for a few years and moved some projects to static type checking. It's not always worth the cost in every situation, of course. For such a simple script I wouldn't add type checking. But for larger projects where there are multiple classes and a lot of types, I've found that the effort is very much worth it: a lot of type errors are found faster (some of them hadn't been caught by unit testing), the code becomes more consistent and easier to read, and developing is easier if you have an IDE that understands the types and gives you appropriate suggestions.
It's also worth it in plumbing code, as it tends to be more difficult to test properly and having types will catch a whole class of errors.
The fact that the program at the bottom of the article can be simplified is not an issue of typing, in fact static typing could also be used in your approach without any issue.
> The knock-on effects of type annotations seem to be doubling the complexity of the code. This is why I'm far from convinced that static type checking in Python is worth the cost.
If adding types to your existing code cannot be done without creating new types, that’s a sign that your type-system lacks expressiveness.
For instance C# 1 lacked a good and general way to describe types for functions (that is, decoupled from a class).
In later C# revisions this have been greatly improved, making this pain go away, and accepting type-safe function-value parameters as easy as any other parameter.
I’m guessing Python is lagging quite a bit in that regard. That doesn’t mean that type-safety (when done right) isn’t massively useful and worth the effort 200 times over.
I think you’re missing their point. They’re not necessarily saying types are not useful, they’re saying types encourages a form of thinking which tends towards class-based overcomplications.
Now this is also a very yagni view, and maybe TFAA uses the structure they build for inspection before the final consumption, but their point that the posted script could be half the size and work the same without loss of readability… is compelling.
Typing in Python is, at times, at odds with the language.
Said another way, patterns which were once Pythonic are now discouraged by the type system (or, mypy I suppose, since the door is still open for other type checkers to emerge). Examples:
- Duck typing. I was taught that's Pythonic, yet mypy really only accepts nominal subtyping i.e. it infers type compatibility from the type hierarchy. There is typing.Protocol, but I've never encountered that in code. Ruby's new type annotation language RBS introduces interfaces, so maybe it'll stay true to its duck typing roots.
In both cases, mypy reminds me way more of Java's type system than of Python's, which is maybe why heavily typed Python code often ends up looking like Java.
> Said another way, patterns which were once Pythonic are now discouraged by the type system (or, mypy I suppose, since the door is still open for other type checkers to emerge).
There are several typecheckers, and while they are at slightly different points, they evolving together and together with the core typing library.
> Duck typing. I was taught that's Pythonic, yet mypy really only accepts nominal subtyping i.e. it infers type compatibility from the type hierarchy. There is typing.Protocol
Which entirely undercuts “mypy only accepts...”
> but I've never encountered that in code.
That's a library author issue, not a mypy (or typing, the standard library) issue.
> In both cases, mypy reminds me way more of Java's type system than of Python's
Python’s type system has always supported both nominal and structural subtyping.
> heavily typed Python code often ends up looking like Java.
That's probably because Java (and languages with closely related type systems) are the typed languages most people writing typed Python have prior experience with, so it's where their typing instincts go.
The sentiment in Python has been trending toward using "classes" (either with dataclass or attrs [1]) as essentially namespaced logicless containers, and away from dicts (or TypedDicts [2]) and namedtuples/NamedTuples [3]) for that purpose.
This is very different from the useless "you don't need classes" classes.
Think of classes nowadays serving as being both traditional OO classes and Scala case classes: they can be types of stateful objects, or just dumb record types, which you now can easily extend with logic if you do happen to need it (and you often do, in nontrivial applications).
Now this is also a very yagni view, and maybe TFAA uses the structure they build for inspection before the final consumption, but their point that the posted script could be half the size and work the same without loss of readability… is compelling.
I find this "it would be half as long and work the same" argument totally bizarre. Do you look at a basic Rust program and say "it would be half as messy and look the same if we did away with all the move/borrow stuff"?
Of course not. Yes, YAGNI is fine and all, but at least recognize that there is an upside to this tradeoff. I am not going to need internal state on the object, but I sure as hell am going to want the type checking.
And yes, you could (perhaps should) use Typescript or Kotlin for new web server projects instead. But some people want Python with types, including me.
I wouldn't accuse Python of being "concise" in the first place, so I am happy to add some up-front verbosity in exchange for a much smoother developer experience later in my project, with significantly less mental overhead as my IDE can do a lot of work for me.
> The sentiment in Python has been trending toward using "classes" (either with dataclass or attrs [1])
That is not even remotely what is being done here. And just because you can do it doesn't mean you should or must do it. Not every argument needs to be bundled in its own object.
> I find this "it would be half as long and work the same" argument totally bizarre. Do you look at a basic Rust program and say "it would be half as messy and look the same if we did away with all the move/borrow stuff"?
Again, missing the point. The point was not about the typechecking but the design perspective it engenders and encourages.
> Of course not. Yes, YAGNI is fine and all, but at least recognize that there is an upside to this tradeoff.
Why would I? There is none in the article's snippet.
> I am not going to need internal state on the object, but I sure as hell am going to want the type checking.
Type checking doesn't require classes. You can typecheck a funtion.
> I wouldn't accuse Python of being "concise" in the first place, so I am happy to add some up-front verbosity in exchange for a much smoother developer experience later in my project, with significantly less mental overhead as my IDE can do a lot of work for me.
You're still missing the point entirely, but I hope you're happy about all those strawmen you beat down.
Type checking doesn't require classes. You can typecheck a funtion.
I am not talking about type checking a function. I am talking about type checking data structures, replacing this:
record = { "a": 1, "b": 2 }
with this:
record = Record(a=1, b=2)
The latter can be statically type checked much more easily than the former.
This is analogous to Scala case classes and Haskell records. And it can be a significant productivity improvement in big codebases, because it reduces your reliance on documentation and working memory. Instead, it lets you depend on the type checker and IDE to provide auto-completion to prevent a whole class of errors.
In fact, it lets you write less runtime code and have shorter, simpler code paths in some cases, because you can mostly skip checks like "if x is an int" (at least in internal/private functions), and even skip the corresponding unit tests, and let the type checker deal with it instead.
Edit: as for the example in the article... really? This is just single dispatch. What would you write instead, one big if/else statement? A dict of functions to dispatch to? `AstNode` is an interface, and all of these subclasses implement the interface.
> The sentiment in Python has been trending toward using "classes" (either with dataclass or attrs [1]) as essentially namespaced logicless containers, and away from dicts (or TypedDicts [2]) and namedtuples/NamedTuples [3]) for that purpose.
But...TypedDicts and NamedTuples are classes (that is, the thing returned from those callable is a class and can be subclassed or have methods added just like any other class), so it's kind of odd to say you are using classes instead of...classes.
Then types in python are not expressive enough. Other languages most of the time you don't even necessarily need to specify the types - the compiler/interpreter can figure it out and still type check the program.
Maybe, maybe not. Data classes are meant to feel like Scala's case classes, or Haskell's record syntax.
The point is to stop passing around dicts/hashmaps/tables and tuples, and to start passing around containers with a known and fixed set of keys/names, and known types for the values/elements.
I don't know of any language where the compiler automatically infers field names and types, and I don't know if I would ever want such a thing.
I've already started checking out Typescript as a general-purpose scripting language to replace Python, Ruby, Perl, and to some extent Bash/Zsh. Knowing this makes it even more attractive. The only annoyances I've had so far are: difficult-to-search docs, and the `tsc` command-line compiler is slow.
Even in languages where this is possible, it's usually idiomatic to explicitly type the public interface including exposed fields, and often it's common to explicitly type them narrower than the inferred type to avoid implementation details leaking into an unintentionally broad backward compatibility contract restricting future evolution.
If you're going to use Python with types, then you may as well use Nim. Nim has a similar syntax, but with better performance. You can also interface with Python code via Nimpy.
I love using Nim, it's an absolute pleasure after years of fighting with Python's "I hope I got this right..." style of type guessing. The biggest problem most people run into is that python has a huge library ecosystem.
I haven't actually tried interfacing them, it's next on my docket. Have you found any good learning resources for that?
I've tried it and it works fine. The only problem I ever had was trying to call a specific Python function at the same time from multiple Nim threads. I'm not sure where the fault lay exactly, I just made that part of the code single-threaded and moved on.
Can we talk about Ruby’s laissez faire approach to syntax. Ignoring all the purely technical stuff every ruby project I’ve seen that has at least a small team behind it looks like a kitchen sink. Bob does it X way, Tina does it Y way, Joe does it X way. Every person does it their own way and readability becomes an eyesore as the project grows.
If I didn’t know any better I’ve seen teams so bad it’s almost a game of “look how gross this block is, now let’s see how much worse you can make the next one”.
I’ve always found ruby very off-putting for this reason alone. I don’t want to inherit or work on something where everyone just wants to show off. How do organizations manage this? Is it just me?
There are multiple ways to do things in many languages. And Ruby is a rather sane one.
The worst language in that regard is Scala by far.
There are the "Scala is Haskell" people where everything is a pure function and written in point free style.
There are the ones who think Scala is just Java with type inference.
And the everything needs to be a DSL people.
Or everything needs to be implicit and generic in a static singleton.
There are a bazillion ways to write the most trivial stuff. Use for comprehensions, recursion, higher order functions, or your usual for each loop. If you hate your coworker use some fancy type system trickery. Does not matter pick and choose.
Ruby is mostly confined compared to many similar languages like Python.
I would say that even JavaScript is way more chaotic in that regard.
When it comes to showing off with Ruby, I think that only occurs in two cases: creating unnecessary DSLs (cough Rspec) when plain Ruby was already adequate, and doing as many chained collection operations in one line as possible.
The former adds a layer of abstraction which is often unnecessary, and the latter can go from succinct to slightly-too-clever to fully obfuscated.
As others have mentioned, Rubocop can help keep some uniformity (although some of the default rules suck, imo). And beyond that, the company just has to have people who agree to be decent. It will happen that developers of different skill levels will write more or less readable code depending on the perspective of the viewer, but that's true in all languages. You _could_ write Ruby code like you would write Basic.
The solution is the same as in many other programming languages: adopt a coding standard and enforce it in an automated fashion, i.e. in the CI/CD pipeline. Code style then becomes mostly a non-issue and not an object of discussion.
I just would like to point out that even though that is the most sane way, it comes with it owns set of problems. One of them is when developers start to code to cheat the linter, or they complicate the code just to "make the linter happy", another is when the linting rule introduces problems/errors like https://github.com/rubocop-hq/rubocop-rails/issues/418
Yeah I would never recommend relying on just a linter. The linter can reduce scut work, but you always want to have at minimum a thorough code review process that’s looking at things like “ok, we made the linter happy; but are we happy with the result, or should we have just disabled the linter here?” and “the linter didn’t catch that we solved this thing with X here but with Y&Z last time. Let’s rationalize our approaches and get everything on the same page”
I wouldn't really go that far though, as you'll end up with a codebase that the linter likes (and appears clean) but is still an utter nightmare to read because of all the forced indirection. I'm talking about things like: restricting method length to 10 lines, restricting class length to 100 lines, and similar ones that prevent you writing code in a certain way because the preferred approach to writing code is 'out of sight, out of mind'.
Default Rubocop is why you have codebases where code is still complicated, it's just hidden away inside single use modules (concerns) or inside single-use private methods. As far as I'm concerned you just can't defer the architecture of your codebase to a tool like that and I'm sure it teaches more bad habits than good ones.
Instead, decide these things for yourself and keep it in check during code review or the stage before that. Maybe even design rubocops of your own to help. The linter can still do a good job without telling you how to handle complexity.
Very refreshing to see this take and I largely agree. People all too often refer to linters as The Rule rather than The Tool, and forget that a robot will never be able to evaluate the quality of a codebase. That requires human input (though Rubocop is very efficient for people that want to avoid human input--often the root cause of the problem).
That said, I am a fan of single-use private methods and think they have their place, with or without method-length rules.
Couldn't agree more. Our Rubocop is FULL of customizations and the odd override too (though there is a case some overrode a rule which caused massively confusing code).
"Single use private methods" is a little triggering to me... not even rubocop's fault. I've seen the likes of this before (single use):
def redirect_to_home_unless_user_present
redirect_to :home unless user.present?
end
That's a horrible usecase for single use private methods, agreed. But I think they're an excellent way to breakdown a complex chunk of code into smaller components, using the method names to describe what they're doing. That requires good naming, though; without good names, the effort is moot.
I think they serve a good purpose once you realise you've stepped out of OOP. You have the idea of a command, or a service, and it does a bunch of procedural things as part of its execution. It only has one public method, so the private methods are a way to describe the steps of the command.
But that's an architectural decision, it's not code style. If rubocop tells you how to write a function then you're going to see nonsense like the example in our parent comment.
> I think they serve a good purpose once you realise you've stepped out of OOP. You have the idea of a command, or a service, and it does a bunch of procedural things as part of its execution. It only has one public method, so the private methods are a way to describe the steps of the command.
It doesn't have to be a step out of OOP, and their usage is not limited to "Single Public Method Objects". Single use methods are just a convenient and clean way to make complicated code easy to digest, OOP or not.
I'd argue the issue is better acknowledge in ruby than in many other popular languages, which just assume having few options means everyone can mostly code as they want from there.
Sister comments are pointing to rubocop which mostly makes it a solved problem. I'd add that having multiple potential ways to do things, and being able to also favor a specific way through linting is very powerful.
You can actually choose the best option for your current project and enforce it, while choosing a different tradeoff for another project and still enforce it there. And you still can opt-out of the rules on a case by case basis in a standard way.
> Can we talk about Ruby’s laissez faire approach to syntax. Ignoring all the purely technical stuff every ruby project I’ve seen that has at least a small team behind it looks like a kitchen sink. Bob does it X way, Tina does it Y way, Joe does it X way. Every person does it their own way and readability becomes an eyesore as the project grows.
This is why Python has deliberately chosen to not add Ruby's blocks. To make any code readable by everyone. (And to prevent bad coding patterns that blocks promote, but that's besides the point.)
That's not exclusive to Ruby, though. Is there a language in which a project of at least a small team would not diverge?
Devs at my current dayjob have established, in multiple rounds, that it is impossible to find at least a minimum of viable coding standards, where every draft has been heavily opposed.
> Devs at my current dayjob have established, in multiple rounds, that it is impossible to find at least a minimum of viable coding standards, where every draft has been heavily opposed.
Tangential, but this mostly sounds like a leadership issue? Ultimately software development is a team sport, and teams work if and only if individuals put the team's interests ahead of their own – and it's not realistic to expect a group of individuals to develop that mentality by consensus. Absent leadership, all you have is a bag of individuals who will bicker about the bikeshed endlessly.
A good leader would hear that feedback and accommodate it to an extent, but they also have to take the responsibility for saying "alright folks, I've heard your feedback, but ultimately it's more important to the team as a whole that we have something than it is that everybody got that something to be what they liked most. This is arbitrary, but this is what we're running with, so it's time to fall in line".
(This is something of a pet peeve of mine, as my team occasionally has to work with an ops group that is stuck in the stone age solely because they've lacked clear leadership for a decade or more. Every time they meet to discuss aligning and modernizing their approach ends in a lack of consensus as to how, and consequently in 2021 they continue to manage their servers each by hand, each in their own way, a collection of expensive single points of failure. Literally any approach they discuss would improve on the status quo, so any competent leader at this point should simply roll the dice, pick one at random, and get on with the task of moving forward)
While not perfect, Go has much more "developer convergence" than any other language I've worked in. Ruby has numerous ways to do the same thing, but one of Go's design objectives was to minimize that.
So divergence does occur, but some languages make it a lot easier than others.
I'm real confused about generators, but maybe I'm just missing something about what Python generators do?
Like, aren't generators just a block of code that can produce the next value, wrapped in an iterator?
'Cuz like, sure, they're not front in center in Ruby programming and if they are in Python I can see why one language would teach you the other, but ruby blocks and yield make this pretty damn easy. So what am I missing?
Ruby has generators, but it’s not just blocks and yield. Generators are special because after they’ve yielded (in the Python sense) a value, they are actually no longer on the stack at all. But when their next value is requested, they pick up execution right where they left off with all local variables intact, as if they had never suspended execution.
If that doesn’t make sense to you, it’s definitely worth taking a few minutes out to look up some tutorials on generators in Python or any other language that supports them.
In Ruby, you can create a generator by passing a block to Enumerator#new; the block’s parameter is a “yielder” object whose << operator behaves like Python’s yield keyword. Keep in mind that this block is NOT invoked once per iteration; it is invoked once, it might suspend and resume though (or suspend and not resume!).
Yes, the difference is between internal iterators vs. external iterators.
For external iterators, Ruby's Enumerator syntax is a little less convenient. But I think it's a small price to pay for not having 2 colors of functions [1]. In Ruby, an Enumerator is just an object with methods like any other object. And there are things built in to core to make working with lazy enumerators just as easy as regular ones.
To GP's point, you rarely actually need enumerators because of a block's ability to break in Ruby. It's the most useful thing that's in so few languages [2]. For example, if I really wanted to write an infinite loop yielding fib -- not saying I would do it this way but -- the quickest way to do it in Ruby, adapting the Stack Overflow example, is probably more like this:
def fib()
puts "Enter function"
a = b = 1
i = 0
loop do
puts "Inside loop"
yield i, a
puts "i: #{i}, a: #{a}, b: #{b}"
a, b = b, a + b
i += 1
end
end
n_from_user_input = 5
fib do |i, x|
break if i == n_from_user_input
puts "Use value: #{x}"
end
Not a huge amount in concrete terms of capability, but the little differences add up in how the languages are used in practice. Python has functions you can refer to and pass around directly (as opposed to Ruby's blocks and procs), and that makes functions that are also generators just a little bit more friction free and more likely to be used. Python also has explicit support for differentiating between generators and regular functions in other parts of it's syntax (e.g. list comprehensions) that tend to make people reach for generators more quickly in Python than other languages.
Most (all?) of Rail's iterators are available as lazy iterators, and I think ditto for core Ruby (it's literally `foo.lazy.each`). Not sure what you mean by "composable" that isn't also the case in Ruby; community standard definitely seems to be that an iterator dot chain doesn't change type (or when it does it's real obvious) making it easy to `.map`, `.select`, etc.
It really feels like Ruby has reached the point of featuritis. Nobody wants to admit that the language is "done". Well, done enough.
None of the recent changes in Ruby are game changers for me except maybe for pattern matching. But since I've fully drunk the Elixir cool-aid, I won't be needed that now. Sorry Ruby. It was a great ride, and I'll miss you. But it's time for me to move on.
I’ve used both, but am a long-time Python user and prefer it for most things. It’s my “first and true love of languages”.
In Python, “format-strings” were a welcome improvement and “f-strings” took me a bit to warm up to but are worth it. The walrus operator, though? Sure, it’s useful in a narrow case, but it’s extremely narrow. Likewise the pattern marching stuff - it just doesn’t feel like “Python” to me, and doesn’t seem to solve any real-world problem that’s remotely close to justifying the cognitive overhead.
> the language designs are so similar that I could just as well imagine a world where Python is the web development lingua franca, and Ruby has all the machine learning libraries.
Well aside from the startling implication that Ruby is a web development “lingua Franca.”...the latter statement is reasonable, as it turns out language design isn’t actually that important here. But the former is pretty far off the mark. I mean, Ruby doesn’t even have first class functions and is very strongly smalltalkish in its OO purism, it has mutable strings a-la Perl. The async story is obviously quite different. Python has a much more complicated interpreter, which has contributed to it being more difficult to get even simple optimizations that are done in Ruby. They’re really only similar in the most superficial sense... in the same way that all current dynamic interpreted languages will do certain things similarly.
While I agree with most of your comment, "Ruby doesn’t even have first class functions" sticks out. Not because it's not true, but because it doesn't really matter as Procs are first class in Ruby and can be used in all the same ways, so it doesn't really affect things except introducing something you might need to learn coming from JS or any other language where functions are first class.
This way can be argued for almost every language, as they almost all allow some form of passing a piece of code to a function. The difference is what matters - whether that's elegant enough to be widely used or not. Ruby is, frankly, somewhere in between.
I'm genuinely curious what is it that you miss in Ruby regarding functions (my Ruby is pretty OOPish and I try to avoid functional magic).
AFAIK you can do stuff like currying/partial application with lambdas and you can get a hold of any method by name and use it as a lambda, pass it to other functions, use it as a block etc. Is there something we're missing compared to, say, JavaScript?
Ruby can definitely pass around several varieties of closure and related constructs, including procs, blocks, lambdas, bindings, continuations, fibers, and both bound and unbound methods.
Whether we should is another matter, and the syntax and idioms certainly lean towards preferring a symbolic late binding, but the language is multi-paradigm, and one may write purely functional Ruby if desired, immutable values and all.
No, passing a symbol and sending is a different mechanism.
The send method is basically the same thing as calling a method by name, as in "obj.foo" == "obj.send(:foo)". If you only pass a symbol into your "caller" method, the symbol goes through the normal lookup: does the receiving object respond to this? If yes, then that implementation (at that exact moment) is called, if not, you get a method_missing.
You're right that that's
not how you do first-class functions in ruby.
Your example in ruby would be:
As you can see there are two ways to do that -- either you create a Proc (a lambda if you care about arity), which is the first class function in Ruby, and you call that, or you define a method (but that's a method, an OOP concept, a procedure that implicitly operates on an object), you get a hold of it using the "method" method and then you call it.
> You must pass it as a symbol in Ruby and then send to it.
This is just plain wrong. You can absolutely do this, as other commenters have pointed out.
It is common to see Ruby code passing around a symbol and sending it, but my guess as to why this pattern became common is that it can be serialized into plaintext like YAML, stored in a database, and called later, like for a background job in a web app. But there's no need to do it this way.
Another reason you don't tend to see it this way in Ruby is because Ruby optimizes for the common case: calling methods [1]. Because of this, you can easily create very concise DSLs in Ruby, which would never be quite as clean in Python.
Maybe I misunderstand your use of the word "function" here, but in addition to Proc/lambda you can certainly reference a method, pass that around, return it, call it?
Yeah, and that's not what first-class usually means. I simply can't say "my_method = Kernel.puts" the same way I can say "my_class = Kernel". Ruby authors seemed to want to make parentheses optional by any price, and that killed the opportunity to make methods first class.
You can literally write my_method = Kernel.method(:puts) and it’ll give you the method to do what you wish with.
As far as defining anonymous functions, literally all you have to do is:
my_func = -> (input) { output }
That makes a “lambda” which is literally just a function you can pass around anonymously.
You call it via either my_func.call(input) or simply my_func[input].
That capability is used all over the place.
Ruby absolutely has first class functions, the only thing you can say is it the syntax is slightly different if you want to use them anonymously (with no object context).
That's exactly what being first-class is not. You tell Kernel to wrap you a method called puts into an object of class Method, rather than assigning a method to a variable. You don't have to look further than JS for a counterexample.
Ruby has two kinds of functions. Methods are functions attached to objects. Lambdas are functions that are not. Ruby lambdas are exactly like javascript arrow functions, or python functions.
There's a _reason_ for the difference, since it determines the implicit self, which is a meaningful part of how Ruby works. Because that difference is meaningful it is reflected in the syntax. `lambda`, or the shorthand `->` creates a function not attached to an object, and `def` creates a function and attaches it to the calling context (making it a method).
Perhaps you don't like this, that's fine. But to say "nope I want my methods and functions to be interchangeable and not to be called lambdas" is purely a subjective argument.
Objectively, Ruby has first-class functions that can be assigned to variables and passes around and returned from other function calls etc etc. They're just called lambdas.
This is clearly a misunderstanding of what a first-class function is. If it doesn't meet your criteria in the colloquial sense of "first-class", that's one thing. But in computer science and programming in general, "first-class function" has a very specific meaning [1], and Ruby absolutely meets the criteria.
The other bit of confusion is that the actual syntax in Ruby optimizes for the common case, which is calling methods. Yehuda Katz wrote a good article explaining this [2]. Among other things, it allows you to create very concise DSLs in Ruby that would never be quite as clean in Python.
So the functionality is equivalent, but ruby's syntax is slightly longer. Is there something other than ruby's slightly longer syntax that you find lacking?
Ruby's Method class matches the design "everything is an object" (strings, numbers, modules & classes themselves...). Are regular expressions first class in ruby? There is top-level syntactic sugar for REs (/.../) but REs are just instances of `Regexp`. It's just syntax. I don't think you can argue Python REs are first class: I have to drag in `re` and then `re.compile()` a string (and my editor won't know to syntax highlight metachars or interpolation). I use REs more often than method references in either language...
But I'm not trying to "whataboutwhataboutwhatabout" here. I'm trying to say there's a difference of philosophy. Ruby Methods are just more objects (Method<Object). They aren't some sort of blessed type, just like everything else. Everything descends from Object. Kernel is just a module. Method is just a class. And so on. In that context, what does it mean to be "first class"? Objects are first class and methods are objects. QED. No?
(FWIW I agree on ruby's optional parens, too much inconsistency for a few less lit pixels on the screen).
> In that context, what does it mean to be "first class"
It's easy. First, what does it mean to be "first class"?
> a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as an argument, returned from a function, modified, and assigned to a variable
Secondly, how does that apply to functions?
> [First class functions] means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures
In practice, because of Proc, you can use Proc wherever you'd use functions and get the same effect in practice, so this is not a big issue. But just because Ruby has Procs, doesn't mean it has first class functions, which is a specific concept.
Thanks, I had also looked for a definition. Instances of Method fulfill both of the cited criteria -- example in this thread, above. Also: Method != Proc, but the article's comments help.
That is: #method doesn't return the Kernel.puts method, it returns a new Kernel.puts Method instance on each invocation. So the returned object from Kernel#method isn't the "real" (first-class) method object.
I feel like I'm a lot closer but I'd still like a better definition :)
Isn't the main issue that Ruby simply doesn't have functions? Ruby is pure OOP, and there is no way to define a method without the implicit 'self' argument. Maybe the reasoning was that methods themselves aren't really meant to be passed around. Because in today's idiomatic Ruby, they mostly aren't -- the late binding behavior of 'send' feels much more Rubyish.
Sorry, Ruby methods are not objects. They can surely be wrapped in objects of class Method, but this feature is used quite rarely. Whether this is due to the syntax, the confusion regarding the evaluation scope, or something else, I have no idea.
The only way of obtaining a method in Ruby returns it as an object. Whether or not they "are" an object is thus entirely an implementation detail.
I think part of the confuction here is that "ob.puts" for example, does not directly access a method. It denotes passing a message to "ob". That message may or may not be routed to a "puts" method.
But at no point does Ruby allow you to get hold of any kind of reference to a method that is anything but an object.
This is incorrect. If you want an anonymous function (with no object context) you can write a lambda, store it to a value, make other lambdas that call it and return yet another lambda etc.
You can do the exact same thing with object methods if you want, eg. my_puts = Kernel.method(:puts).
You can even do this in one line and stop the auto parenthesis behavior if you want.
foo = method def foo
...
end
Now foo will reference the method and you have to use foo() to call it.
The reason that people don’t do a bunch of that in Ruby is that the support for anonymous blocks is so convenient That there aren’t that many cases where you really need anonymous functions detached from any object.
Not really the same since lambda in Python is just a syntactic variant that is restricted. A function defined using `def` can be passed around without restrictions, which you simply cannot do in Ruby.
Yes, but they're not popular these days. Ruby 3 almost defaulted to frozen-strings by default (I wish it had) and a lot of the ecosystem moved to that style resulting in some nice memory saving. Rails enforces it on its code for example: https://github.com/rails/rails/blob/0f09dfca363410f51f6f6078...
I’m amused that a number in this thread saw these observations as a “diss”... that’s more on you then me. I didn’t pass any value judgement on these differences, merely that they exist and demonstrate some fundamental differences in the history of these languages. While Ruby lambdas are essentially first class anonymous functions they were added late in the language and they are distinct from methods that predate them. You can’t just drop them in seamlessly where a method is used.
The point stands that while both Ruby and python have accreted more stuff as time wears on, their initial design principles were starkly dissimilar.
Yo bro dialamac / Caught in a falsehood / Tryin' to dial it back / Feels misunderstood
/ Sez lambda came late / But changelog don't lie / Since v0.8 / Ruby so fly.
Or, in prose form: I don't see anyone disagreeing that Ruby & Python are dissimilar both in principle and in practice, but "Ruby doesn't even have first-class functions" was most unreservedly an epic howler, and once played, folks were inevitably gonna have some fun passing that football around, and despite most of the changelog from 1995 being in Japanese there are nevertheless references to lambdas that early on, although the more concise "stabby" syntax didn't rear up until ca.2008.
NARRATOR: A common assumption, but no. Equating object methods to functions is a furphy. The argument along the lines of:
"Ruby's object methods are Ruby's functions, but you can't pass them around, ergo they're not functions"
is using the term "function" in two different ways, but assuming they're the same; this is not an argument based on substance, but upon mislabelling. The conclusion is bogus because the premise is bogus.
It may arise from a category error, assuming that the thing depends intrinsically upon the literal representation of the thing, or (worse) the common name of the thing, but this is a) wrong anyway, and b) loses coherence entirely in a language in which function literals can be conjured and lexically rebound at runtime.
In actuality, Ruby's lambdas are functions, and first-class, by the only definition with substance: they are closures capable of higher-order expression, taking functions as parameters when invoked, and returning functions as results.
Which is why saying "it don't have them" on a forum named after a fixed-point combinator is to invite: a) ridicule, and b) lambda calculus expressions in rap battle form.
I was just telling a colleague that I love working with Ruby specifically because it makes me happy. It's not the fastest nor the most elegant, but it really does make me smile to work with Ruby. That's a huge win for me, personally, and will drive me to continue using it whenever possible.
That said, I've been playing with Rust lately and it's giving me very similar vibes. My hope is that once I get the basics down I'll be able to use Rust for those things that require a bit more performance and I won't have to give up being happy to work on.
No idea what that statement does and I'm a rubyist. Anyway I think that Enumerator::Lazy is what one should use in most cases. Quite syntax heavy compared to Python though, but what is not in the very core of a language is always more verbose.
Hopefully you do actually recognise what that does: the standard library has many methods documented to return an enumerator if no block is given. This is how.
The article misleadingly states “Ruby 3.0 has Fibers”. Not false, but Ruby 2.0 had fibers, they are used for building a variety of cooperative concurrency behaviours, and they go back years.
It creates an Enumerator (for practical purposes consider it a continuation) over the method "each" unless you've passed in an argument named "block", which by convention would be a block argument (a closure passed after the argument list effectively) that will evaluate to Proc object.
Consider this example:
class Foo
def each
yield(42)
yield(:hello)
yield(:world)
end
end
You can now do:
Foo.new.to_enum(:each)
(In this case the ":each" is redundant - to_enum defaults to :each, so you could do Foo.new.to_enum)
And you will get back an Enumerator that supports the Enumerable interface, giving you methods like map, inject, to_a (to array) etc. as a facade to the original object so you can do (for example:
Foo.new.to_enum.to_a
=> [42, :hello, :world]
(you can achieve a similar thing by including Enumerable into the class in question, but to_enum lets you do this on arbitrary objects without messing around with the class)
It allows any method to return something that behaves like a collection class without needing to pass more than a method that implements "each" by yielding each value of the collection.
You can also do this with a bare lambda, btw., but the syntax is a bit awkward as "yield" is syntactic sugar over calling the "call" method over an implicit (unnamed) block argument that isn't quite as transparent as you might like:
ob = ->(&block) { block.call(42); block.call(:hello); block.call(:world) }.to_enum(:call)
The explicit "&block" argument is needed because "yield" has lexical scope where defined.
"ob" is now an enumerator that supporters each, to_a, map etc.:
> for practical purposes consider it a continuation
But it's not though. While coroutines can be used to implement one-shot continuations and vice versa, they are very different features and behave completely differently.
While you're technically right, in terms of using it as a shortcut to give superficial understanding of how Enumerator functions the distinctions are largely irrelevant.
You can write a lot of functional, or at least simply procedural (modular) without ever needing to define your own class. Sure, you'll be instantiating some objects implicitly or explicitly at times, but you don't have to make your code be class-based.
You can write pure functions, although you do have to be aware of space/time concerns depending on the situation. Obviously you want to avoid copying huge data structures frequently. But in a lot of cases, the performance cost is low, so you can respect your inputs by not mutating them and just return some value/collection.
I still think Ruby on Rails is the #1 choice for launching a non-static web project in 2021. The lifecycle I've seen with the 4 companies I've built on Rails:
1. Pure vanilla rails
2. Then the front end grows into an SPA as functionality becomes more complex
3. More microservices like search emerge into their own thing
By step 3, I've got a dozen engineers and things are still as smooth as one engineer. Engineering is never the bottleneck. The tech debt is super easy to spot. It's incredibly simple to know how to scale horizontally to many more engineers.
Rails can get you very very far. It's not sexy, but it gets me to market faster than anything out there.
Phoenix and Elixir, if you understand the choices made, is also a wonderful choice. The whole contexts thing allows you to design your system as micro services before you even add in network partitions, and Ecto, not being an ORM (it’s a DSL for writing SQL in elixir) is incredible, better than it’s possible to be in active record. Then there’s channels, presence, performance etc.
> Phoenix and Elixir, if you understand the choices made, is also a wonderful choice
Until you want to integrate Stripe, BrainTree, PayPal or Paddle and then you can't find an official library for Elixir. Even common things like pagination isn't fully solved with well maintained libraries in Elixir. Then there's things like wanting to create notifications (showing unread icons, sending email / sns notifications, etc.) where Rails has this solved with well supported gems but there's nothing like this in Elixir. There's dozens of other things too, and all of this adds up to you having to be an expert in Elixir and have to write all of this functionality from scratch.
If you're in it for the long haul and are investing years into the project with a big team that's fine, but if you're a solo developer building an app and want to focus on writing your app's features, in my opinion Phoenix can't really be compared to Rails. Especially with Hotwire Turbo being recently announced, now you can get all of those awesome real time features into a Rails app quite easily (IMO even easier than Live View).
I started a SAAS app with Phoenix a while back and ultimately backed out of it because all of the above. It just felt like I was in a perpetual state of having to write libraries instead of my application. It was one of those things where I got super excited initially but after 2 years of trying, waiting for certain features to come, etc. it ended up not being for me.
I have no regrets tho because I learned a really big lesson from that, which is if you want to build something now, use whatever tech stack you know right now and if you're very much inclined to deviate from that and learn something new then pick something based on what exists today in that tech stack. Not what's "on the horizon" or "coming soon". You could set yourself up for waiting forever, or if that feature you wanted in the future does come out it could be way different than what was originally claimed and now it feels like you've delayed what you wanted to do for so long for nothing.
In other words, nothing is ever going to be perfect. Just pick something and build it. Also trust what folks do, not what they say.
I think often the things that don’t exist are not there for good reasons... using Stripe’s api for example from a module is pretty trivial in my experience, it’s just HTTP and you don’t need to be super clever about it. What specifically are you struggling to paginate, I’ve never found that difficult to implement even with quite clever options. Again notifications doesn’t sound particularly difficult and I don’t see why I’d want to rely on some complex gem that does every option when I don’t need them. Elixir+Phoenix I agree is definitely lower level but there are things it does much better than Ruby on Rails. I’d rather not have to rely on gems and write the simplest implementation specifically for my use case. It’s a trade off, but one I’m much happier with.
> I think often the things that don’t exist are not there for good reasons... using Stripe’s api for example from a module is pretty trivial in my experience, it’s just HTTP and you don’t need to be super clever about it.
It's way more involved than inserting an auth token header into an HTTP request and calling some API endpoint.
For example, what about verifying webhooks? The official libraries for Stripe (Python, Ruby, Node, PHP, Go, JS, etc.) deal with this for you.
But with Elixir, you're on your own. This is very low level code to have to deal with and it's extremely important you get it right.
You're left having to parse Stripe's specification on this and then implement the code yourself in Elixir. It's so tricky and involved that the Dashbit company (the creator of Elixir and members of the core team work there) wrote a blog post on it at https://dashbit.co/blog/how-we-verify-webhooks.
But before a few months ago that blog post didn't exist. Also this isn't the only thing you'll have to do yourself when it comes to interacting with Stripe using Elixir.
Then you'll have to do similar things for other payment providers all which are different in a lot of ways, but with Rails you have the combination of having official Ruby clients from those payment providers and even the Pay gem which lets you support payments from multiple providers. That could easily be a few months of dev time just for that abstraction alone if you had to go about that from scratch and your implementation wouldn't have any track record until you start using it and ironing out the bugs from real world experience.
> Again notifications doesn’t sound particularly difficult and I don’t see why I’d want to rely on some complex gem that does every option when I don’t need them
Don't take this the wrong way but this seems to be the mindset of almost everyone I chatted with when it comes to Elixir. When someone asks how to do something, the answer is it's trivial or easy to implement but there's rarely any examples posted on how to do it and almost never in a production ready way.
In my mind trivial or easy means I can sit down in maybe a few hours or a day and write a production ready solution, complete with tests and have it work exactly how I want without running into any major roadblocks.
I'd be curious to see how you would implement https://github.com/excid3/noticed or https://github.com/pay-rails/pay in Elixir / Phoenix. Based on your responses of saying these things are easy I'm guessing you've written large apps with Phoenix where you've developed features like this in a production app? It would be fantastic if you could post some code examples or a blog post on how you went about this. Not just to answer my specific question but I'm sure the community would appreciate having concrete examples of how it's done. This way maybe more folks would use the framework.
Global pay solution that supports everything: they are all a bit crap you're right, the best I've found is https://github.com/aviabird/gringotts and ex_money really is amazing that integrates with it (I would suggest it's better than the equivalent ruby gem). To be fair I'm not sure I'd want to use the pay gem with anything complex as you need to be able to use the specific quirks of each API properly (for example it can't pause subscriptions on stripe right now even though stripe supports this: https://github.com/pay-rails/pay/blob/master/lib/pay/stripe/...).
You're also right about noticed, after looking into it more it would be worth building for elixir for sure. Ravenx represents a start but it's unmaintained and doesn't have a huge number of strategies. It depends on how much I needed to do notifications like this. For the apps that I've built we've just needed database and grouped emails sent once per day, no need for texts or slack etc. There's no reason these couldn't be added fairly simply but I agree noticed is very neat. It's definitely a few weeks work to roll your own from scratch so to be honest I'd probably just integrate with Twilio and just pay for someone else to handle this for me.
> Stripe, including webhooks support, actively developed
I've looked into Stripity Stripe. For some time it was unmaintained and ended up getting taken over by another maintainer. It's also not as comprehensive as the official Stripe libraries. There's also a very big difference in using an official Stripe library and hoping for the best with a random one someone developed. Just skimming the code base it looks like the Checkout module is missing features that exist in the official Stripe library in every other supported language.
According to the README file for Stripity Stripe it's also using Stripe's API version from 2019. There have been multiple major API updates since then, and there's been an open issue since November 2020 to add support for newer API versions with no replies. Personally I would be using one of those major features too.
And this really is the point I'm trying to drive home. With Ruby, Python, Go, PHP, Node, Java and .NET these are problems you don't even need to think about. You just pick the payment provider's official SDK and start coding immediately, often times there's also an abundance of resources to implement the billing code itself into your app too through blog posts, official docs, YouTube videos, and even paid products like https://spark.laravel.com/. Stuff that makes integrating billing into your app (through Stripe, BrainTree and Paddle) being something you get done in 1 day instead of 3 months.
With Elixir it becomes weeks of comprehensive research, evaluating questionable libraries, opening PRs, and becoming a full time library developer just to get to the point where you could even maybe begin to start accepting payments with just Stripe. Then you have to repeat the whole process for PayPal, BrainTree and / or Paddle. These are all super popular payment providers.
I asked the Gringotts developers if they would be supporting PayPal about 5 hours after they announced the project ~3 years ago. He said it was coming and to stay tuned. It's now ~3 years later and PayPal support isn't there. Neither is BrainTree or Paddle. Here's the open issue for PayPal support from 2018 (not by me, I asked on another site) https://github.com/aviabird/gringotts/issues/114. The Stripe integration is also missing a ton and hasn't been touched since 2018.
By the way, the Pay gem is really good. It's a smart abstraction and supports a ton of different subscription / 1 off payment use cases. Even complex ones like the type of app I was building.
> It's definitely a few weeks work to roll your own from scratch so to be honest I'd probably just integrate with Twilio and just pay for someone else to handle this for me.
Twilio ends up being 1 potential delivery method, it's not really someone you pay to solve the problem for you.
There's wanting to show notification in the app over websockets, saving them into a database, emailing them out only if they are unread, maybe sending a text through Twilio, Slack and other providers.
The noticed gem handles all of this for you (and supports Twilio too).
Notifications in general is another example where other frameworks have this solved in very good ways, but it becomes another example where you have to stop developing your app and start developing a notification library with Elixir.
At this point we've only talked about payments and notifications too. There's lots of other examples and these are all things to think about if your goal is to build an application.
Now don't get me wrong, it still takes a lot of custom code and effort to build an app with Rails but the time spent isn't on low level generic problems. It's letting you focus on your custom application's business logic.
And with less opinionated frameworks like Flask you can lean on Python being extremely well supported by almost every service that offers an SDK and also being able to find solutions to nearly every web dev problem online in some form or another (blog posts, youtube, stack overflow, etc.). That goes a long ways because at least you're not starting from scratch.
What I love with Phoenix/Elixir is that there's plenty of features I wouldn't bother with in a more traditional language. You want a PubSub? It's just there, you can use it in one line. You want a Redis? Registry is there, also in one line. It's like a whole tech stack in one language.
I saw a presentation about Phoenix/Elixir that described this once and it really impressed me. It's been 4 or so years that I've been meaning to pick up Phoenix for a project but still haven't.
It was a slide from a conference I believe but I wouldn't know how to find it - some elixir people in the thread might know. It was something illustrating all the external tools you'd normally reach for and how Erlang/Elixir just had all of this built into the language and VM.
I want to use this, but every time I try to switch it is not there yet.
Sure it has the core stuff, but things like ActionMailer and the built in S3 support in ActiveStorage, Devise, keep me going back. Too many of those things are not there... yet ... with Phoenix
Also nothing beats active record after they added Arel which turns your query into an AST and optimizes it before sending it out - Ecto is not really there yet and doesn't look like it will get there
I think most packages are just as good if not better than their Ruby equivalents these days (and really we can do all of the things you mentioned in elixir), the active record pattern is obviously easier to understand than Ecto, but if you know how to write well optimised SQL you never want to go back to an ORM (even if it tries to rewrite your queries for you with magic).
ActiveRecord and the scenic gem for database views is a very powerful combination. For the vast majority of queries, you don't need to write optimized SQL by hand.
When you do, you can push that work to a version controlled database view, and wrap it in a ActiveRecord class interface and still get all of the ORM niceties.
The difference here is between two different philosophies:
1) I want the thing to just work, it doesn't matter about underlying complexity, give me the magic and the version controlled database view and the ORM niceties.
2) I don't trust magic as it has bitten me in the arse before, let me make and be able to fix things myself as I'm working at a (slightly) lower level.
I'm not saying either is correct but in Phoenix/Elixir magic stuff tends to be kept to a minimum. One man's "for the vast majority of queries" is another persons "will work fine until you need to do something complex".
It's all trade offs and I choose mine carefully as I'm sure you do.
This is interesting - I'm evaluating what to use for the backend of a side project right now after having focussed more on front-end/apps for the last few years, so I'm catching up on the latest progress in this area. Interesting to see that Rails is still often considered the best option, seems like they must be doing something right.
Ideally in my mind, I'd like to use JS (well, TS), because it's the language I work in most of the time these days... but I can pick up different languages as needed, that's not a major issue and I've done some Ruby way back when - more important to me is making the development of the backend as simple and as quick as possible (the less code the better!), and that it will run reliably. A good selection of well-tested off-the-shelf modules to integrate other services is always a good thing. It seems like Rails is well ahead of any JS-based solution in these regards, from what I can see.
I had been evaluating PostGraphile, which appeals because of the "low-code" approach, but I guess I'm a little concerned about doing most of the logic in database functions, because I've worked on a system like that years ago and found things like testing and debugging pretty painful. If anyone has evaulated PostGraphile (or Hasura etc) vs. Rails, I'd love to hear your thoughts (or equally, any other solutions to check out!).
Replace Rails with any framework and you'll have the same amount of success. I've done the exact same thing across Ruby, PHP, Golang and tons of other frameworks/languages and never had problems if following those steps.
Problems usually come from when teams start with point 3 and 2 when in reality they should stay lean in the beginning.
This argument has been made for years and I’ve yet to see it really be true.
You can make anything with anything, but Rails is the closest thing to an Aspect Oriented Programming environment that I’ve seen in the wild. It’s not easy to replicate that type of ecosystem with any other framework. I’ve spent ample time on teams using Go, Python, PHP, Java and C#.
There’s just no comparison vs what a small Ruby team can accomplish.
I really agree with this statement. I've never seen anything like Rails in Java/JavaScript land. In Java there's SpringBoot but its nowhere near a cohesive package with a strong online community behind it like Rails. It's more of a collection of libs put together with a good enough attitude.
Same experience. I've seen nice PHP projects on the internet, but not yet in RL. I know its possible, i guess its just that the language does not reward you for corecctness.
PHP used to actually have a performance penalty for splitting things up into modules and using object oriented design, because it meant loading and parsing more files on each request.
I was trying to do that on my first big PHP project, and got called out. The architect said "your code is too good, PHP is for writing crap". If you are going to write good code, you should do it in another language.
Symfony framework is a great example of a PHP framework written by Java programmers. All the same verbosity as Java, but without the benefits of Java.
Ok? I've seen projects go pear-shaped in probably 20 different programming languages at this point, and I guess after a certain point of experience, you start to realize that failures seldom have to do with the programming languages themselves, and more about the people actually writing and managing the code (and everything else around development).
Well, if you're not interested in factual discussions and you simply "choose to believe trends", all bets are off since you're pulling in your feelings about a programming language into your arguments. Hope all goes well for you anyway!
As a counter-point, the biggest disasters I've seen has been involving either C++ or C#, but in the end I think the reason they were disasters were because of the programmers (and their ego) rather than the programming languages they worked with. C++ is plenty open and without safeties, but that doesn't mean you have to use it like that.
Jokes aside, Laravel is probably a more likely contender to provide a Rails experience for PHP.
And it's easy to get clouded by anecdotal data, I'm sure I'm biased as well. But after some time in the industry, it matters more who the person is that is using the framework, than the framework itself. If you put an equally skilled developer with the same amount of experience in either Symfony or Rails, I'm sure they can be as productive as the other. But then skilled developers tend to gravitate towards Rails more than Symfony, so hard to judge in the real world.
I myself usually opt for the "small set of composable libraries" way of development nowadays, but I've worked with teams using Buffalo and Beego which kind of provides a "batteries included" approach that Rails follows too.
NodeJS has even more "batteries included" frameworks than Golang. Sails, Hapi, NestJS and Adonis are probably the most popular right now, at least from my viewpoint (traditional SaaS web development)
Give it a try. The languages are actually very similar, on roughly the same level of "high-low-level-ness", they both come batteries included, both are very mature and refined.
Some things you may find nice are: the elegance and ease of use of blocks instead of explicitly declared lambdas, the standard library full of nicely named methods with aliases for the most used ones (e.g. array.map / array.collect do the same thing, some people hate this), the {do,if,case,loop}-end syntax that doesn't fuck your code up when you just quickly comment these parts out without reindenting the rest, and if you're into OOP (as in OOP, not explosion-in-the-factory-factory), then of course the hardcore OOP-ness.
Depends on your use-cases - Rails is unparalleled in my opinion for getting a web app up and running, testing startup ideas, and building quickly in response to customer feedback.
I've been using Python recently as I've done more ML work, and I do prefer Ruby (though the syntaxes are similar and you won't have any difficulty).
If you learn for Rails, start with 5.2.3, webpacker and the other black magic in Rails 6 will frustrate you and currently eliminates a lot of the ease of getting started and building relatively sophisticated web apps quickly.
"If you learn for Rails, start with 5.2.3, webpacker and the other black magic in Rails 6 will frustrate you and currently eliminates a lot of the ease of getting started and building relatively sophisticated web apps quickly."
Webpacker is here to stay and takes about 5 minutes to understand. Might as well take advantage of it so you can quickly add complex front end components via React or whatever flavor of FE you prefer.
I agree, webpacker makes learning Rails harder. It's a shame. But learning on a non current version is a short term solution. A good tutorial, like Michael Hartl's, will get you over that hump.
I'd go with something that has static/strong types when picking a new tool. Not saying I dont love Ruby, but when the codebase/team becomes large i've always regretted not having strong type safety. Today I'd go with Kotlin.
I tried (am trying at current company) and still don't like it. I completely see the appeal for getting an idea up and running quickly, it absolutely delivers on that.
However, I joined a rails company about a year ago and the codebase is just a mess at this scale. I find it annoying that in order to know where dependencies are coming from, I can't just go to the top of the file and see what's imported. I have to know how rails injects it and then track it down from there.
Don't get me wrong, there are issues with django, Go, etc.. but for the most part I can jump in and track down what's happening much easier.
Yeah, that's gotta be one of my biggest complaints after years of working with Rails. Eventually you sort-of memorize the conventions and can relatively accurately guess where a file lives, until someone decides to get clever and put things in a weird place.
It also implicitly discourages you from asking yourself if you should be accessing the thing you are. IMO a lot of the tight coupling in Rails codebases begins with being able to grab literally anything and use it with no one the wiser unless the read that specific line of code.
I love Ruby. Have used it for 15 years, but I can't stand Rails. It's just massive overkill for most things.
I prefer to start with Sinatra + Sequel for web projects. Sometimes Padrino + Sequel. As your project grows, sure, you'll pull in many things similar to Rails, and probably pull in some projects that started out with Rails too, but it allows you to be much explicit about which dependencies you pull in, why, and to limited where it gets pulled in.
Sinatra is great - it's small enough to read the whole thing and understand it. But it's also very minimalist, so you might also want to look at Padrino, which layers neatly on top of Sinatra, yet is still small and ORM agnostic.
It's also fairly painless to start with Sinatra, and layer in components from Padrino if/when you need them. E.g. you can simply register Padrino's helpers, mailer, routing, rendering or cache components in a Sinatra app if/when you need them. They're just straightforward Sinatra plugins.
The single most important part of rails is the orm. That, honestly, seems to be the "thing" that people are actually getting excited about when they gush over rails. A close second would be rspec. Finally, in production, it can be very helpful to be able to inspect the state of the db using the rails console (as long as the developer is wise enough to sandbox their actions).
I find the tooling (I use vim with solargraph) to be not only underwhelming but frustrating. No I am not willing to use a different editor. Rails code is not at all discoverable because I cannot follow method calls/objects half of the time. In order to alleviate this, I suppose, rails tries very hard to standardize things. Ok. Fine.
The ease of creating model relationships can also produce a nasty set of relationships between tables if you are not careful. One project I worked on used the fast_jsonapi where one table had relationships spanning most of the system. Dealing with that was borderline ridiculous.
ruby is one of my favourite languages, but there are two things I wish it had done python's way - inline type annotations, and imports acting as namespaces. I wouldn't have thought to miss the latter before I got a day job working in python, but it really does make code in larger projects a lot easier to read and maintain when you can tell at a glance where every symbol is defined.
Ruby has had the equivalent of Python generators for a very long time (it also has things called Generators, but they aren't exactly the same thing though they occupying some of the same space.)
> And Ruby 3.0 comes with Fibers
Fibers were introduced in Ruby 1.9 more than a decade ago. And before that, Ruby had general-purpose continuations; Fibers were more tractable for alternative implementations, and easier to optimize, and covered the main use cases of continuations, though they are strictly less powerful an abstraction.
I'm currently learning Ruby as I plan to implement the 'Building Git' book by James Coglan. Ruby has lot many features/syntactic sugar and a weird OO model. Makes me feel quite uncomfortable. Also, no good IDE except RubyMine.
The OO model is very clean. Pretty much Smalltalk's model. Wanna see weird OO, look at JS or even Python (both were not OO in first version).
IDE's will never be really good for Ruby (or any dynamically typed language), as it can only infer so much from the code. Type hints in the new Ruby version will maybe change this, but it is an addon so it may take a while to materialize.
Ruby is --to me-- a dynamically typed language with very clean OO, expressive syntax, and as much FP goodness as possible without sacrificing it's OO essence.
There is a language that has --in my view-- similar fundamentals, except that it is strict/strongly typed. This language is Kotlin: very clean OO, expressive syntax, and as much FP goodness as possible without sacrificing it's OO essence. IntelliJ is a great IDE for Kotlin and IDEs for Kotlin can actually deliver (due to the static typing). And yes, Kotlin is null-safe (no reason to carry this mistake around in static typed langs any further).
Oh and the syntactic sugar in Ruby is called cocaine. :)
> JS or even Python (both were not OO in first version)
This isn't true for Python. Quoting Guido: "classes were added late during Python’s first year of development at CWI, though well before the first public release" [0].
I don't think it's true for JS either - the first versions didn't have classes, but I believe they already had prototype-based OO.
I thought Python had classes in the first release but not to be used by library/application makers. But I was probably mistaken. Thanks for setting that straight.
What I think is rather unidiomatic in OO langs is Pythons "function/methods" like `len(x)` which is implemented as `__length__()`. This is just weird/unintuitive/unidiomatic OO... I do not know what Guido was on when he thought this was a good idea.
Fair enough - in some ways it’s like half-way to an OO system. Yes, it’s theoretically elegant to have everything be an object and not to have classes. However in the vast majority of cases it seems prototype-based OO is only used as a foundation upon which to build a class system.
> What I think is rather unidiomatic in OO langs is Pythons "function/methods" like `len(x)` which is implemented as `__length__()`.
AFAIK the reason for the double-underscore method for `len` is to avoid it accidentally working for, say, a Rectangle class that exposes `length` and `width`.
ruby has enumerators which are quite a bit like pythons generators -- i actually think they are easier to reason about and do complicated things with -- although i learned python a long time ago and haven't tried to use generators for anything in a long time.
Maybe I'm part of the hate train, but the natural progress is either Python to Java or Python to Go. Ruby is alright, but not scalable unless you're trying to be a startup where you can just drop in a bunch of gems and make it work.
I think this is a kind of fallacy to take into consideration scalability as the main metric when choosing the programming language unless you are truly doing a project which has as main selling point scalability.
I would also say that that 90% (it is just a guess based) of projects started with any of these languages including Python, Java, Ruby, Go, Elixir, Javascript will not reach a level where scalability is truly a problem.
There are some things to think about when choosing a programming language like:
1. It is a language which has by default solutions to problems I would encounter. See for example choosing Python over Ruby as there are more AI libraries in Python than Ruby, or choosing Elixir over Ruby if serving multiple concurent connections will be at the core)
2. How quick do you want to iterate the product? How many libraries in your specific domain are available? ...
3. Availability of developers in your area or with a specific domain knowledge if needed
4. How much the infrastructure cost to serve some users per month
Also I agree with you - even if you presented it as a negative trait - that if I want to just spin up a quick prototype, I will almost always choose Rails. I have by default a lot of gems providing a lot of functionalities out of the box: user management, payments, activity feeds, image/video/audio upload and processing, .... Add there some TailwindUI/Bootstrap/Bulma screens and already I can have a good looking prototype to test an idea without too much work.
Coming from operations perspective Ruby is only scalable because it is great for protyping. It as a deliverable can be challenging due to size and complexity involved with compiling gems. I'm sure it has gotten easier to deliver with Kubernetes but Java and Go are much easier to ship and deploy.
In my case I managed to scale Rails (a Ruby web framework) both vertically (of course this is always simple with resource lakes like some cloud providers are offering) and horizontally.
For example I can spin up as much pods on GCP or dynos on Heroku without doing almost any changes in my Rails app. Things are even more simpler if I would use Sinatra.
Of course I am not asserting here that Ruby is a fit for all types of web apps. But what I disagree with your statement that sounds like a very general conclusion stating that Ruby is not scalable.
So talking specifically about programming languages and not frameworks, how do you scale Java or Go without operational overhead?
I'm a Java developer who became a Ruby developer who became an SRE.
Ruby is far easier to deploy and use than Java or (in my limited experience) Golang, specifically because it has a single, mature, sane packaging system i.e. bundler.
Only Elixir + hex come close to the ease of packaging with bundler. The only time you need to compile gems is if you're shipping ones with native extensions - and there are enough alternatives available that you shouldn't need to use gems with native C extensions.
If you're running ruby on a modern web server - like puma - it will scale far further than your architecture will likely permit without redesign.
At many places where I've worked at this paradigm is exactly what is needed, but people know and want to use JS. If we choose to go that way then, we need to write a lot of glue code and think about setting up architecture standards, folder and naming conventions, ORM support, authentication, job queues, etc; all of this comes by default with Rails and makes life so much easier.
Lately some options seem to be gaining steam:
* RedwoodJS [1]: by Tom Preston-Werner (founder of Github), comes with React and GraphQL baked in, pretty new project though
* Next.js [2]: brings a lot of nice conventions to the frontend, but still some things missing on the backend side
* SailsJS [3]: similar to Rails, but small community
* Strapi [4], hapi [5] et al: plenty of backend functionality, but frontend agnostic.
Has anybody had experience with any of these (or others) and can report back?
[1] - https://redwoodjs.com/
[2] - https://nextjs.org/
[3] - https://sailsjs.com/
[4] - https://strapi.io/
[5] - https://hapi.dev/