Learning Ruby: Things I Like, Things I Miss from Python
medium.com
medium.com
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/
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.
I think ActiveRecord has #lock (see https://api.rubyonrails.org/classes/ActiveRecord/Locking/Opt...) and that together with #transaction (https://api.rubyonrails.org/classes/ActiveRecord/Transaction...) are enough for my use cases.
In case of file uploads to cloud providers: there I don't know for specific method to help with race conditions when transforming the resources.
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 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
https://guides.rubyonrails.org/active_record_basics.html
The ActiveRecord Queries guide also covers some mechanisms for handling row-level write contention
https://guides.rubyonrails.org/active_record_querying.html#l...
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.
https://www.brightball.com/articles/rails-gems-to-unlock-adv...
Disclaimer: I have never tried Rails
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.
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'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".
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.
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-.
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.
[0] - https://www.djangoproject.com/ [1] - https://www.django-rest-framework.org/ [2] - https://dj-rest-auth.readthedocs.io/en/latest/index.html [3] - https://django-allauth.readthedocs.io/en/latest/overview.htm... [4] - https://www.rootstrap.com/blog/registration-and-authenticati...
- 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.
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.
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.
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.
Typescript can do this to unannotated code, and it’s surprisingly useful.
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.
1: https://www.attrs.org/en/stable/
2: https://docs.python.org/3/library/typing.html#typing.TypedDi...
3: https://docs.python.org/3/library/collections.html#collectio... and https://docs.python.org/3/library/typing.html#typing.NamedTu...
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.
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.
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.
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.
- Lists with mixed types. I might as well bookmark this page, because this behavior of mypy always surprises me: https://mypy.readthedocs.io/en/stable/common_issues.html#inv...
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.
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.
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.
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.
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.
Expr = Callable[[str]], Callable[..., Iterable[str]]]
AstNode = Expr | Callable[... ], Callable[..., Iterable[str]]]
You then don't event need to call representation() , as each node is callable. No classes needed.But my instinct tells me a node will eventually have more attributes and methods. Stuff to get parents, children, siblings, filtering and so on.
So it's probably a good call to use a class for later. Also you want to keep AstNode as a node could be an expression, a statement or a comment.
I couldn't agree more with Jack Diederich's thoughts on that sentiment, at 5:40 in the video linked above:
https://youtu.be/o9pEzgHorH0?t=340
> Lots of times people think they might need something later. You don't. Or, you can just do it later.
Experience helps with this.
It's a middle path as usual: naive approaches waste as much time as overenginering.
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.
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.
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 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.)
* I could have said refactoring, but some people have very specific definitions attached to that word so I use something more general here
I haven't actually tried interfacing them, it's next on my docket. Have you found any good learning resources for that?
A good test as an example: https://github.com/yglukhov/nimpy/blob/master/tests/tpyfromn...
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.
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?
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.
If you find the defaults to be not good (which I agree), then simply update the rubocop.yml.
"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
Why!?? (that is a rhetorical "why")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.
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.
That said, I am a fan of single-use private methods and think they have their place, with or without method-length rules.
In the case of Ruby:
* most popular style guide: https://rubystyle.guide/
* automated tool for linting and enforcing code style: https://rubocop.org/
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.
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 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.
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)
So divergence does occur, but some languages make it a lot easier than others.
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.)
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?
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!).
https://stackoverflow.com/questions/35826375/enumerator-new-...
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
1: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...2: https://yehudakatz.com/2012/01/10/javascript-needs-blocks/
With async/await, type hints, the walrus operator and now pattern matching, Python’s designers are just as guilty of this as Ruby’s.
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.
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.
Yes they are, as seen in the _why's old project:
https://github.com/whymirror/unholy
> Compile Ruby to Python bytecode. And, in addition, translate that bytecode back to Python source code using Decompyle (included.)
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.
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...
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?
def f(x):
return x
def g(fn, x):
return fn(x)
g(f, 1)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.
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:
def get_and_call(fn, x)
fn.call(x)
end
def upcasinator_method(str)
str.upcase
end
upcasinator_lambda = lambda {|str| str.upcase}
get_and_call(upcasinator_lambda, 'foobar')
=> "FOOBAR"
get_and_call(method(:upcasinator_method), 'foobar')
=> "FOOBAR"
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. f = ->(x) { x }
g = ->(fn, x) { fn[x] }
g[f,1] h = g.curry(2)[f]
h[1]
function composition is available: f = -> x { x * 2 }
g = -> y { y + 4 }
j = f >> g
k = j << g
j[1] #=> 6
k[1] #=> 14
and, topically, the Y combinator: y = -> f {
-> g { g[g] } [
-> g { f[-> v { g[g][v] }] }
]
}
hence fib = y[-> f {
-> n { n < 2 ? Array(0..n) : f[n-1].then { _1 << _1[-1] + _1[-2] } }
}]
fib[6] #=> [0, 1, 1, 2, 3, 5, 8]
although the more idiomatic Ruby might be fibo = Hash.new { |this, n| this[n] = n < 2 ? n : this[n-1] + this[n-2] }
(0..6).map(&fibo) #=> [0, 1, 1, 2, 3, 5, 8]
in which the Hash self-converts to a closure via the & operator.Anyone who says "Ruby doesn't have first-class functions" can fight me.
--------------------
⁽¹⁾ notwithstanding that I recall Matz once remarked the curry method is only included as an elaborate joke.
I never thought I would ever write these words, but the Python equivalent is more beautiful. Ouch.
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.
1: https://yehudakatz.com/2010/02/21/ruby-is-not-a-callable-ori...
I'm pretty sure it does and that it is full-fledged, compared to Python's intentionally restricted `lambda` syntax.
What you do have in Ruby is Proc, that can be used kind of like first class functions, so you don't really miss out on much.
(Please see my sibling comment for example).
See:
* https://ruby-doc.org/core-2.7.1/Method.html
* https://ruby-doc.org/core-2.7.1/Object.html#method-i-method
$ irb
2.3.3 :001 > p = Kernel.method(:puts)
=> #<Method: Kernel.puts>
2.3.3 :002 > p.call "Hello"
Hello
=> nil
2.3.3 :003 > def run_a_method(meth, *args)
2.3.3 :004?> meth.call(*args)
2.3.3 :005?> end
=> :run_a_method
2.3.3 :006 > run_a_method(p, "Hi!")
Hi!
=> nil
2.3.3 :007 >
ps. this also works with the "to_proc" operator (&) so you can do this: [1, 2, 3].each(&Kernel.method(:puts))
(but I wouldn't necessarily recommend this since it's not very readable, at least to my eyes).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).
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 what way doesn't Ruby have first class functions? This article explains it better: https://codequizzes.wordpress.com/2014/05/19/ruby-methods-ar...
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.
Different heritage
I, aha, notice this:
$ irb
2.3.3 :001 > Kernel.method(:puts).object_id
=> 47165335040720
2.3.3 :002 > Kernel.method(:puts).object_id
=> 47165334993360 # not the same!
2.3.3 :003 >
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 :)
IntelliJ editors do this and it’s insanely useful. Works with SQL, regex and any other language.
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.
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.
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.
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.
1: https://en.wikipedia.org/wiki/First-class_function
2: https://yehudakatz.com/2010/02/21/ruby-is-not-a-callable-ori...
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.
y = -> f {
-> g { g[g] } [
-> g { f[-> v { g[g][v] }] }
]
}
/ see ya later.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.
RECORD SCRATCH. THE ROOM FALLS SILENT.
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.
CROWD: Yeah!
MUSIC STARTS / GLITTERBALL CLOSEUP
Edit: and yes.. uh comment deleted as charged, since you had edited after I replied.
And yet,
> go full on neckbeard
Here you are,
> you seem to have balled up tightly in your own self worth
In full-on pompous balloon ad hominem mode,
> https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/fun...
With a reference page that is 22 years out of date,
> you can finish this argument with whatever hand you prefer
And a bitter, resentful, dick joke.
Well, I don't have a beard, and I ain't the dick here.
> How does one call a lambda after all
Why not fire up the interpreter and go
f[x]
yourself.Which is the title of this song.
POSTSCRIPT:
Oh look, a comment deleter / throws around shit / but too late for this meter / Yo' can check it all out / at https://inopinatus.org/i/all_the_sweeter.png
It had me at happiness.
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.
Excuse me, what?! Python has stackless symmetric coroutines; Ruby comes along and brings you stackful symmetric coroutines, what's there to miss?
How is `return to_enum :each unless block` not infinitely nicer than pythons yield? (and waaay more powerful as a little cherry on top)
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.: ob.to_a
=> [42, :hello, :world]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.
It's funny because the first is exactly how you write this in ruby.
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.
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.
Problems usually come from when teams start with point 3 and 2 when in reality they should stay lean in the beginning.
I would say it is as productive as rails too.
There is no ActiveRecord and way way less magic which slows (initial) development speed but it pays off in less bugs to fix.
So overall development speed is roughly the same. But with Go you gain co routines and nice performance which can make life a lot easier.
Gos type system, while somewhat poor, still helps too.
The worst part of the whole thing is actually the weird Go std lib templating.
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.
Yes, you can write bad code in any language.
PHP makes it hard NOT to. The footguns are all sitting out in the open, loaded, and the safeties are off.
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.
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.
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.
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.
Now, Ruby itself isn't bad at all.
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 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.
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.
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.
Anyone else agree with this?
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.
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.
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
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.
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.
GOTO 2019 • The Soul of Erlang and Elixir • Saša Jurić
https://www.youtube.com/watch?v=JvBT4XBdoUE
https://elixirforum.com/uploads/default/original/2X/a/a2524c... (also from Saša Jurić's book)
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.
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.
Stripe, including webhooks support, actively developed: https://github.com/code-corps/stripity_stripe
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.
> the best I've found is https://github.com/aviabird/gringotts
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.
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.
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!).
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.
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. :)
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.
[0] http://python-history.blogspot.com/2009/02/adding-support-fo...
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`.
Python is surely OOP all the way, just ask the type system about what are the metaclasses of every, single, basic, data type.
30.downto(0) do |n| print a[n] end print((30).to_bytes(2, byteorder='big'))Everything is an object with methods.
Gitlab size? GitHub size? AirBnB size? Shopify size?
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.
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.
Python to java is not progression, it’s criminal malfeasance.