I first time learn Ruby from zero to "hero" in production confidently is in just under a week. And as far as i know, no other language could bring me such experience.
I first time learn Ruby from zero to "hero" in production confidently is in just under a week. And as far as i know, no other language could bring me such experience.
You get objects you don't know what's in there, 6 month later someone changed it, no compile error but it will break when you run it.
And so to overcome those major issues they added really ugly stuff that is not core to the language, linters, annotations etc ...
But static typing languages, library stacks, and environments have come a long way in the last 30 years. Now I prefer even to "prototype" in static languages because the crossover to when they start providing me net benefit is around a week or so into my development process.
I will not claim one is completely better than the other, but the balance is a lot more even than I would say it was ~20 years ago.
I've moved from Ruby to Crystal as much as I can, and at first I found it painful but now I appreciate the myriad complaints from the type checker. It's eliminated 99% of the specs I used to have to write.
I learned programming in the late 1990s. Looking at Windows programming at the time looking like gibberish to me and I looked forward to the day I would understand it. Honestly, it still looks like gibberish to me. I understand the whys and the wherefores better now and if I had no choice, yeah, I could learn it and write it, but the code from that still reads absolutely awfully to me, just unbelievably bad by almost every modern standard. Even when you're writing approximately equivalent code in C# at the same low level of acquiring handles and manipulating them with very low level APIs, it's just so much better now.
But in truth, you're right. Very few people know/cared about type inference at the time.
Optional typing, at least in TypeScript, will not give you the guarantees of type checking. It finds many of your type errors, but not all of them because the type system is deliberately unsound.
All you really needed is:
for (auto it : myMap)
and you still get compile type safetyIf you write code the way you write statically typed code, then sure, you're in for a world of hurt. I could imagine using a statically typed language again if it made me as productive as Ruby, but most statically types languages have near useless type systems that I wouldn't wish on my worst enemy.
Beyond that, it also forces me to think structurally from the start. But I tend to be working on rather complex applications, I almost certainly wouldn't use a static lang for data processing or similar tasks.
> It feels a lot like the people who used to try to convince everyone functional programming was the only way to go and object-oriented programming was for dinosaurs.
What do you mean "used to"?? Haha
However, once you have strongly + statically typed languages, you can very often create safe-by-design (tm) APIs, APIs that are simply impossible to misuse (for some definition of misusing). A trivial example (which among industrial languages works only in Rust and perhaps Ada) is a file system API in which the compiler detects that, in some codepaths, you're attempting to try to write to a file after closing it.
Exactly how much value this has for your programming depends on how many invariants you need to guarantee. If you're writing (for instance) a CRUD for non-critical data, that's usually not many. If you're writing a network protocol or a garbage collector, this can save you from worlds of pain. I have had to fix data loss and privacy issues in Firefox that would have been detected years earlier if we had used a strongly-typed language such as TypeScript (which didn't exist at the time) instead of JavaScript for the front-end.
In fact, I wear a large number of scars from fixing Firefox bugs in JavaScript (or C++) code. These days, I prefer strong, static typing. I sleep more soundly :)
But, as usual, we're not in a one-size-fits-all industry. There are developments for which static typing gets in the way of zero-to-deployment.
As usual, tradeoffs everywhere!
Note: Strongly + statically typed languages also typically offer strong support for other forms of static analysis (e.g. model checking, abstract interpretation, etc.) that are considered critical in some industries (e.g. aeronautics – or writing Windows device drivers). But we're getting into something of a niche.
What about Dart doesn't feel sufficiently typed for you?
Apparently, these days, Dart claims to be sound... except they seem to have redefined the meaning of type soundness along the way: https://dart.dev/guides/language/type-system#runtime-checks
I've certainly seen that happen in Ruby, but I've also seen that happen in every other language - most of them statically typed - I've worked with, as very few languages have type systems expressive enough to prevent it (and it tends to massively destroy productivity to try check everything strictly enough anyway).
IIRC someone looked at Github issues a while ago to understand the impact of dynamic typing on defect rate. What he found was that around ~1% of bugs in JS/Python/Ruby enterprise systems were type-related.
It might be just anecdotal, but I noticed people often say that writing in Ruby makes them feel happy.
Static typing (or functional programming, or TDD) doesn't seem to make people happy. It makes them feel superior.
Whether it truly makes them superior, or just taps into their insecurities is an open question.
These days more inferred typing in many of the static languages has lessened the gap, so the reason to hate on them has been reduced.
I still would love better static analysis tools, including for Ruby, but at the same time the way I write code has changed drastically in ways that makes that harder (e.g. leveraging the dynamic features of Ruby more)
Ruby specifically is a separate question IMO because it has a lot of convenience features that are kinda orthogonal to the whole typing debate. Crystal is a good example of how you can retain those in a statically typed language.
https://en.wikipedia.org/wiki/Crystal_(programming_language)
But keep in mind this includes run-time dynamic meta-programming. E.g. one of my projects bootstrapped a large proportion of the web frontend by having the backend introspect the database schema and dynamically generate models, routes, access control and metadata endpoints that the frontend then used to instantiate the UI.
The typing in that case was controlled by the database migrations.
I've done "generate a whole CRUD backend from a single source of truth for what my domain looks like", although I've done it by defining the model classes, generating the database schema from them, and deriving everything else from them. All the building blocks are there but I haven't found a framework/library that puts it all together (that's one of those projects I keep meaning to get around to). I've heard there are some database access libraries that let you use a macro to derive model classes from the schema, but I haven't used them yet.
If you see static typing as essential, then I can see the appeal. But my starting point is that it's not essential to me (and we're increasingly getting part of the value via optional and gradual typing via things like Sorbet which also let us keep the clutter out of sight, reducing the gap even further), and so losing even some of what brought me to Ruby is too much of a sacrifice.
Unfortunately, most people got a taste of static typing with Java and the initial language and library design were done in such a way that types were well, if not entirely useless, then certainly under-used. But if you look at many libraries for languages such as Rust, Scala, OCaml, Haskell, F#, ... (I haven't looked at Java in a while, but I don't hold high hopes on this specific front) you'll find many examples in which the API and the type system cooperate to guarantee that high-level protocols are enforced.
That being said, I absolutely agree that strong static typing does not mean that you don't need tests. As everything, if you want to be safe, you need a defense in depth, with good API design and many test layers.
The solution is to become a software atheist and just break things down when they no longer fit in your head.
Interestingly, this seems to be what microservices originally were about until they got twisted by hype.
It is also what OOP was about, until it got twisted by another religion.
Sure, test for sane failure modes in line with the documented contract, and that may include the occasional test that is de facto a type test, but often testing for types, especially in languages with poor type systems, but also in Ruby where we have alternatives, ends up with tests for classes which is frequently the wrong thing.
E.g. in Ruby never, ever check for somearg.kind_of?(IO) if all you ever do is somearg.read - if you absolutely must typecheck somearg, the Ruby way is to check for presence of "#read", e.g. somearg.respond_to?(:read), or try and fail responsibly (and often just allowing the NoMethodError to bubble up is the right way to fail).
Also think it's just fine for people to add and ship Sorbet type declarations for gems etc. to then signal those contracts so people can verify them if they choose. There's no reason not to offer that when it's reasonable to do so.
In Python or JavaScript/TypeScript, though, my experience is that failing to validate types at the borders (i.e. every single function/method/constructor/generator/... exposed in your API) is pretty much guaranteed to end up, months later, with developers attempting to sherlock out surprising breakages in production from logs that make no sense and traces that do not show anything remotely close to the actual culprit.
I have the scars to show it :) Of course, YMMV.
Hence don't check for an IO object when what you care about is the presence of a "#read" method.
Or don't check if something is an Array if what matters is that it supports "#map". Instead, either somearg.respond_to?(:map), or if it needs more than map, somearg.respond_to?(Enumerable) (this seems broad, but supporting Enumerable only requires implementing "#each" and including the module, so a caller "worst case" can reopen a class or wrap their object), or call one of Array(somearg) (tries "#to_ary" then "#to_a" then falls back to returning [somearg]) or Array.try_convert(somearg) (tries "#to_ary" then falls back on nil).
Consistently picking the most generic applicable options when faced with choices like that still enforces the contract on the boundary, but also tends to lead to code with much less ceremony. E.g. far fewer [something]Adapter classes, or glue code to convert data before calling methods that are being overly prescriptive.
But for most statically typed languages, when people talk about static typing, they still talk about a class and a type as interchangeable (there are exceptions, and it's getting better, and with increased type inference coupled with increased support for type annotations and analysis of dynamic languages, I expect there to be an increasing convergence, though).
Note that I'm writing "types" and not "classes". Interfaces do just as well, if not better!
For what it's worth, in the static realm, OCaml introduce features that let code check for presence of a method (instead of implementing an interface) ~25 years ago. Unfortunately, this proved rather unwieldy and, to the best of my knowledge, nobody uses that feature.
When there is a meaningful name to give, in Ruby you might define a module, and include it and use that as an interface:
module HelloWorld
def hello
raise "Not implemented"
end
end
class Foo
include HelloWorld
end
Foo.new.hello # Raises an exception
Foo.new.kind_of?(HelloWorld) # Returns true
(And anytime you want something that behaves somewhat like an ordered collection, you typically just want object.kind_of?(Enumerable)).There are plenty of contract-checking frameworks for Ruby [1] but they see little use, because most of our contracts tend to be extremely simple. Along the line of "implements method X" or "includes module Y".
Sorbet w/inference tools and optionally storing the types in a separate files (it supports inline too) might slowly change that (and effectively let you use modules as "proper" interfaces), since the biggest opposition to types in Ruby tends to be visual noise and forcing refactoring, and if it can be turned on/off in your IDE and regenerated, a lot of the objections fall away. It's not that we (well many of us) don't want the extra type checks, but that we don't want to pay the cost of making the code less readable.
[1] Here's one: http://egonschiele.github.io/contracts.ruby/ and of course there's Sorbet: https://sorbet.org/blog/2020/07/30/ruby-3-rbs-sorbet
Do you have examples? That's not my experience.
Of course, we may simply be working on very different types of code :)
But e.g consider basically almost any case with abstract member functions / methods. While that happens in Ruby (or the equivalent: Leaving a method which raises an exception unless overridden) it's far less common.
Typically you'll instead implement defaults in a module if there is lots of functionality that a client might want for itself). These are generally fine to name, and type check by the module name.
But when that is not* the case (any class consisting mostly of abstract methods), a Ruby implementation would be more likely to instead expect the presence of a single method and embed the default functionality in the calling class. That's the kind of scenarios where you end up with the kind of long, contrived class names people tend to mock Java for in particular.
But going further, a Ruby implementation will instead often take an argument that is expected to support "to_proc" or a block argument, so that you can provide a lambda/closure to do what you'd otherwise put in an "adapter" or "shim" class.
Conversely, any time where you demand (whether in a static language by type annotiations or in a dynamic one by checking for the class) an object of a very specific class and only call one or a few methods on it, you're sidestepping this problem by avoiding the creation of a more precise interface that'd clutter up the code. You don't create and name interfaces that encompass the specific operations needed by each method because it'd be a total mess.
A few common examples for which I rely upon static typing: - I have a proof that operation X is complete, because I have a protocol to follow; - this string is a Matrix room id (and not a Matrix user id or a Matrix server id, also strings); - this time is in milliseconds.
Sounds like very different use cases :)
But to counter myself, the perceived lack of developer speed and flexiblity of a strongly typed language (with Rust, I always get the feeling that I'm fighting with the compiler!) is also solved in the long run with practice and tooling.
If you have a preexisting codebase I believe the way you can convert it is to add the types that you know on commits and eventually you will have enough types that adding the missing ones should be easy. For the missing ones Any is a good choice.
https://pyre-check.org and https://github.com/python/mypy are popular.
One hurdle I've stumbled over recently is the question "what is a type?", the answer can be surprising. Unions, for example, are types but not `Type`s. A function that takes an argument of type `Type` will not accept a Union. So if you want to write a function that effectively "casts" a parameter to a specified type, you can't. The best you can do is have an overload that accepts `Type` and does an actual cast, and then another that just turns it into `Any`. This is, in fact, how the standard library types its `cast` function [1]. The argument I've seen for the current behavior is that `Type` describes anything that can be passed to isinstance, but that's not a satisfying answer. Even then, `Union` can be passed to isinstance and still does not work with `Type`. Talk currently is to introduce a new kind of type called `TypeForm` or something to address this, which is certainly an improvement over nothing, but still feels like technical debt.
[1]: https://github.com/python/typeshed/blob/main/stdlib/typing.p...
YMMV
https://github.com/Shopify/tapioca
Tapioca is used to generate type signatures on gems and code that creates functions at runtime. Sorbet ships with some of that behavior, but they updated their docs to recommend tapioca over sorbet where the behavior overlaps.
(Ruby is strongly typed in the "doesn't automatically coerce types" and "maintains a fixed type for an object" sense; using the term "strong typing" is inherently ambiguous because it has so many partially conflicting meanings)
Monkeypatching is evil.
Rubocop custom cops to reign-in what's allowed in CI/CD.
But anytime someone introduced a new gem (namely act_as_paranoid) or writes a new concern that modifies defaults, then you're back to need to do live code.
Ruby requires you to be aware of much more "state" than any other language that I have coded in, because all assumptions about methods cannot be confirmed until the code is run. For example, a gem might modify the default_scope of a portion of active record models. You have to memorize all these tweaks and even then you don't know if the code you've written will do what you want until it is actually been run.
https://survey.stackoverflow.co/2022/#technology-most-loved-...