(I'm not sure if this still holds under a world where LLMs are doing the majority of writing code but this is my opinion from prior to LLMs)
From someone who has worked mostly in Ruby (but also Perl and TypeScript and Elixir) I think for web development, a dynamic language with optional types actually hits maybe the best point for developer productivity IMO.
Without any types in a dynamic language, you often end up with code that can be quite difficult to understand what kinds of objects are represented by a given variable. Especially in older poorly factored codebases where there are often many variations of classes with similar names and often closely related functions it can feel almost impossible until you're really familiar with the codebase.
With an actual fully typed language you're much more constrained in terms of what idioms you can use and how you can express and handle code by the type system. If you're not adept or knowledgeable about these things you can spend a lot of time trying to jam what you're attempting into the type system only to eventually realize it's impossible to do.
A gradual type system on top of a dynamic language gets you some of the best of both worlds. A huge amount of the value is just getting typing at function boundaries (what are the types of the arguments for this function? what is the type of what it's returning?) but at the same time it's extremely easy to just sidestep the type system if it can't express what you want or is too cumbersome.
> Without any types in a dynamic language, you often end up with code that can be quite difficult to understand what kinds of objects are represented by a given variable. Especially in older poorly factored codebases where there are often many variations of classes with similar names and often closely related functions it can feel almost impossible until you're really familiar with the codebase.
One of the worst parts of exploring an unfamiliar codebase written in a language without type labeling is tunneling through the code trying to figure out what this thing you see being bounced around in the program like the a ball in a pinball machine actually is.
Even in functional Elixir with immutability, I had to jump to various callsites to understand what was being passed in and what I could actually do. Pinball is apt. Types drastically reduce pinballing. The larger the codebase, the more pinball.
This is our experience. We have added Sorbet to a 16 year old Rails app. It is a big win in avoiding errors, typos, documentation, code completion, fewer tests are required, etc.
And the LLMs take advantage of the types through the LSP and type checking.
One of the big advantages of types is documenting what is *not* allowed. This brings a clarity to the developers and additionally ensure what is not allowed does not happen.
Unit tests typically test for behaviours. This could be both positive and negative tests. But we often test only a subset of possibilities just because how people generally think (more positive cases than negative cases). Theoretically we can do all those tests with unit testing. But we need to ask ourselves honestly, do we have that kind of test coverage as SQLLite? If yes, do we have that for very large codebases?
We have some tests that ensure the interface is correct - that the correct type of args are passed say from a batch process to a mailer and a mail object is returned.
For these tests we don’t care about the content only that something didn’t get incorrectly set or the mailer interface changed.
Now if the developer changes the Mailer to require a user object the compiler tells us there is an error. Sorbet will error and say “hey you need to update your code here and here by adding a User object”
Before we would have had test coverage for that - or maybe not and missed the error.
First one that pops to mind is some old python code; the parameter that came in on some functions could be a single string or a list of them. Lots of bugs where arg[0] was a character rather than a string. So tests had to be written showing both being passed in.
That is a fair opinion. My opinion is different, but that's totally fine - we have different views here.
What I completely disagree with, though, is this statement:
> Without any types in a dynamic language, you often end up with code that can be quite difficult to understand what kinds of objects are represented by a given variable.
I have been writing ruby code since about 22 years (almost) now. I never needed types as such. My code does not depend on types or assumptions about variables per se, although I do, of course, use .is_a? and .respond_to? quite a lot, to determine some sanitizing or logic steps (e. g. if an Array is given to a method, I may iterate over that array as such, and pass it recursively into the method back).
Your argument seems to be more related to naming variables. People could name a variable in a certain way if they need this, e. g. array_all_people = []. This may not be super-elegant; and it does not have as strong as support as types would, but it invalidates the argument that people don't know what variables are or do in complex programs as such. I simply don't think you need types to manage this part at all.
> Especially in older poorly factored codebases where there are often many variations of classes with similar names and often closely related functions it can feel almost impossible until you're really familiar with the codebase.
Note that this is intrinsic complexity that is valid for ANY codebase. I highly doubt just by using types, people automatically understand 50.000 lines of code written by other people. That just doesn't make sense to me.
> With an actual fully typed language you're much more constrained in terms of what idioms you can use
I already don't want the type restrictions.
> A gradual type system on top of a dynamic language gets you some of the best of both worlds.
I reason it combines the worst of both worlds, since rather than committing, people add more complexity into the system.
The times I've been bitten by type safety issues is far less than the hassle of maintaining types. Seriously, it is a much smaller issue than people make it out to be. I will say that I do get bitten by the occasional `NoMethodError` on `nil`, but it really doesn't happen often. Since ruby is very dynamic it is hard to say how many of those errors would be caught even with type annotation. I also don't find myself needing to write specs to cover the different cases of type checking. For me it is a tradeoff with productivity.
That said, I do like it when an LSP can show some nice method signature info, and types are helpful in that way. I think it depends. At the surface level, I like some of the niceties that type annotations can bring, but I've seen how tricky defining more complex objects can get. Occasionally I would spend way too much time fighting types in elixir with dialyzer, and I've often not enjoyed TypeScript for the verbosity. So I understand the cost of defining types. To me, the cost often outweigh the benefit of type annotation.
I fully agree with this. I'm building a site in OCAML, and I just this week spent 90 minutes debugging some weird error I didn't understand because an implicit type was being pulled through in a global context. It was pretty irritating.
Maybe this isn't a fair comparison, since I'm pretty new to OCAML and I'm sure an experience developer would have seen what was happening much quicker than I would have. But I'm not sure I spent 90 minutes TOTAL on type errors doing Python web dev.
Maybe I'm exaggerating, and I probably just don't remember the first time I hit a type error, but my experience with type errors was that I would very occasionally hit them, and then I would just fix the type error. Pretty easy.
When I'm writing code that will be distributed to other devs, I feel type annotations make more sense because it helps document the libraries and there is less ambiguity about what a method will take. As with everything, "it depends"
That's true, but it can also add unnecessary constraints if done thoughtlessly.
E.g. if you require an input to be StringIO, instead of requiring an object that responds to "read".
Too often I see people add typing (be it with a project like this, or with is_a? or respond_to?) that makes assumptions about how the caller will want to use it, rather than state actual requirements.
That is why I prefer projects to be very deliberate and cautious about how they use types, and keep it to a minimum.
Well said. There are many problems you have to deal with when writing code and type annotations only solve one particular kind. And even type annotations can be wrong: when you're dealing with data from external sources, dynamic languages like Python, JavaScript and Ruby will happily parse any valid JSON into a native data structure, even if it might not be what you specified in your type hints. Worse yet, you may not even notice unless you also have runtime type checks.
The kind of messy code base that results from (large) numbers of (mediocre) developers hastily implementing hacky bug fixes and (incomplete) specifications under time pressure isn't necessarily solved by any technical solution such as type hints.
I personally find Claude Code has no real issues working and producing code in the 40k LoC Ruby on Rails repo I work in nor in the 45k LoC Elixir/Phoenix repo I work in. For the last few months I'd say 99% of all changes I do to both are purely via Claude Code, I almost never use my editor anymore at all. It's common things don't work on the first try or aren't exactly what I want but usually just giving an error to Claude or further instructions will fix it in an iteration or two.
I think the code organization isn't amazing, but it's fine and frankly not that much of a concern to me usually as I'm usually just reading diffs and not digging around in the code much myself.
Totally of topic, but the other day I was considering trying out elixir for a mainly vibe coded project, mainly because i thought the way you can structure code in it should be pretty much optimal for LLM driven development.
I haven't tried it yet, but I thought elixirs easily implementable static analysis of code could make enforcement whenever the LLM goes off rails highly useful, and an umbrella architecture would make modularity well established.
Modules could all define their own contexts via nested CLAUDE.md and subagents could be used to give it explicit implementation details.
Did you try something like that before MGriisser? (successfully or not?)
Unfortunately I don't do anything nearly that sophisticated, I honestly barely even know Elixir, I had just used it a little bit at a previous job and thought it would be a nice choice to try for the web server part of an application I was building.
I mostly use Claude in that repo for controllers, DB access, and front end via heex templates, often with LiveView. I find it can get a bit mixed up with heex stuff occasionally given the weirdness of nested code into the HTML and all that but I think on pure Elixir it usually does a good job.
Is R2 subject to Cloudflare's universal API rate limit? They have an API rate limit of 1200 requests/5 minutes that I've hit many times with their images product.
And they won't increase it unless you become an enterprise customer in which case they'll generously double it.
I am absolutely sure. Look at inflation, for one. We haven't seen these inflation numbers since the 1980s, and this is in just 2 years since the biggest central banks opened the money spigots. I'm a millenial so I don't relish it, but "you ain't seen nothin' yet."
From someone who has worked mostly in Ruby (but also Perl and TypeScript and Elixir) I think for web development, a dynamic language with optional types actually hits maybe the best point for developer productivity IMO.
Without any types in a dynamic language, you often end up with code that can be quite difficult to understand what kinds of objects are represented by a given variable. Especially in older poorly factored codebases where there are often many variations of classes with similar names and often closely related functions it can feel almost impossible until you're really familiar with the codebase.
With an actual fully typed language you're much more constrained in terms of what idioms you can use and how you can express and handle code by the type system. If you're not adept or knowledgeable about these things you can spend a lot of time trying to jam what you're attempting into the type system only to eventually realize it's impossible to do.
A gradual type system on top of a dynamic language gets you some of the best of both worlds. A huge amount of the value is just getting typing at function boundaries (what are the types of the arguments for this function? what is the type of what it's returning?) but at the same time it's extremely easy to just sidestep the type system if it can't express what you want or is too cumbersome.