It's been funny to watch how more and more static type systems are getting bolted on to dynamically typed languages in recent years.
Typescript (with stellar adoption), native type annotation support in Python, Sorbet, PHP 7, Elixir + Dialyzer, ...
I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.
I guess in part because it's a big challenge to come up with a coherent type system that allows this, the bifurcation in the ecosystem, and often a somewhat leaky abstraction. Eg in Typescript you will often still run into bugs caused by malformed JSON that doesn't fit the type declaration, badly or insufficiently typed third party libraries, ....
Google's Dart is the only recent, somewhat popular language (only due to Flutter) that allows this natively - that I can think of right now.
I do think such a language would be very beneficial for the current landscape though, and projects like this show there is a clear need.
Edit: just remembered another option: Crystal. Also Julia, as pointed out below.
For whatever it's worth and without wanting to start a language war (I like Python just fine), I think Python/Ruby-style typing is a false economy for prototyping. There are a lot of things that make Go slower to write than Ruby, but mandatory typing isn't one of them. Rather, Ruby's total lack of typing makes it harder to write: you effectively end up having to write unit tests just to catch typos.
I wonder whether the perception that type safety slows down Ruby (or ES6) development comes from the fact that the type systems are bolted on after the fact.
I would say that dynamically-typed languages are great to prototype as they do not emphasize on being correct too much.
Being correct from day one will cause too much unnecessary friction.
You (usually) don't know the entire program architecture until you make a full prototype, and even if you have a plan, there will always be some part where some unexpected consequences force you to rearchitect some parts.
And since you don't know the entire program architecture you architect your program bottom-up, and most of the dynamically-typed languages allow an interactive development environment at a flexibility that typed languages can't provide.
Think about developing Python inside a Jupyter notebook, or Common Lisp inside a REPL.
You turn on a REPL, open up a file, write a function, send the function to the REPL, test the function you just wrote, and when I find a mistake I can just redefine any function I would like to change.
This process allows fixing mistakes on-the-fly. Typed languages don't allow this (even ones that have a so-called REPL) at this flexibility, (since they emphasize on being correct all the time), and cause too much unnecessary friction while prototyping.
Thus a need for a dynamically-typed language that can enforce types after prototyping.
I think about this a lot, as someone who loves Ruby and loves Rust. I think it’s because we do a bad job of looking at the total costs. It feels like you can knock something out really quickly in Ruby. And in some sense, you can! But the time you spend debugging later doesn’t get factored into the way that it feels when you’re just cranking out features.
I've never quite understood the notion that languages like Python and Ruby are amenable to fast prototyping. Ultimately, you wind up just having to carry complicated type information in your head rather than write it all down in your code, all whilst the compiler utterly fails to help you in any way in case your memory ain't what it used to be.
I'm sure those languages and their ilk have advantages for prototyping, but I agree, mandatory typing in other languages isn't a burden. If you already have to reason about what arguments are acceptable in functions, what objects can receive what messages and what those messages should contain, you already have typing — just inefficiently stored in short term memory.
Those who are the biggest proponents of these languages as useful for prototyping are also not the ones who rewrite their code in type-safe languages, so being able to add type annotations for the sake of their colleagues who eventually have to turn these prototypes into code upon which a team can collaborate can only be a useful thing.
Python and Ruby are extremely malleable, like Smalltalk and lisp before them. You can inspect anything, you can override / replace / proxy anything. You can use a few built-in collection types for nearly anything.
This is by design. This is great for prototyping.
These languages are to programming what breadboards and wires are for electronics.
Of course, as your system grows, you start to want static checks, or having your schematics on a PCB. But at the very start, tweaking a piece of Python code, or a breadboard, is easiest. Then, of course, you may not want to afford a a rewrite to rust, or would put your breadboard in a box and ship it :)
> There are a lot of things that make Go slower to write than Ruby, but mandatory typing isn't one of them. Rather, Ruby's total lack of typing makes it harder to write: you effectively end up having to write unit tests just to catch typos.
No, you don't. You need unit tests to verify behavior (values) whether or not you have static typing (except for output types with only zero or one values). Now, it's true, that having such tests also verifies, at no extra charge, things that Go’s type system would verify, but without the additional effort of type annotations. But there's no added cost there, it's a net savings.
Nowadays I write unit tests for any nontrivial code I create. It takes a little more time than writing type declarations, but IMHO they offer much more value.
For prototyping sure, because the prototype might grow, but otherwise it depends on the type and scope of the project I think. For the kind of CRUD web app projects I pick Ruby for I still fail to see how I could benefits from types instead of slowing me down. I may be missing something, so I’m interested if you have any resources with concrete examples for common errors.
I agree: I think most people that look at typechecking as a serious friction are thinking about it wrong; the problem is probably languages with bad ergonomics. Many people who use TypeScript actually end up using it with strict defaults for even new or toy projects - it’s too valuable to not, honestly.
Wasn't that perception around long before the 'bolted on' type systems appeared? To me, it seems like some of the clunkiness and sharp edges of the type systems of commonly-used typed languages at the time - C++ and Java had a lot to do with it along with RoR enthusiasts portraying static typing as some sort of tool of oppression.
I think it's mostly the REPL that allows for quickly spiking out things, with quick feedback. Elixir has a similar experience, but less of the warts you get with Ruby (like monkeypatching).
Unless you've used a language with a high level of strictness, i.e. OCaml/Haskell/Rust, it can be hard to see the sheer power and utility of typechecking. If someone has only used Java, they may not understand the true power of types. But if you've familiar with OCaml/Haskell/Rust, why bother writing dynamically typed code? Sure there's some niche usecases where it's more powerful, but generally you can do as much with say, Rust, or even more pragmatically, C# 8/Kotlin.
While if you've only used dynamic languages or badly typed languages, then having to deal with this stupid naggy compiler is just annoying. A big part of learning a strongly statically typed language is learning that the compiler is your friend, and that errors are good. I've noticed that a lot of people new to TypeScript try to get the compiler to shut up, often resorting to any or @ts-ignore, while more advanced users will see it as a dialogue. The compiler complains? Okay, something's wrong: Let's find the root cause here.
TypeScript took off because people had no choice but to write JS, so any benefit was better than no benefit. Sorbet was also borne out of an existing codebase. But a new language wouldn't have this lock in factor.
I am an "expert" in static type systems (I'm familiar with Java, Scala, OCaml, Haskell, Rust, Go, TypeScript, C++ ... and keep up to date with latest type systems research like 1ML, MLsub, Liquid Haskell, ...), but I have a really hard time imagining how one would develop a statically typed library that would even approximate the usefulness and convenience (for rapid prototyping and interactive data analysis) of Python's Pandas (although if I was a betting man, I'd wager the best language to implement it in would be Scala, with it's advanced macros & almost-dependent type system).
I don't think it is hard to see the power of stronger typing - surely everyone who has written any code in Python or Javascript has made a typo that would have been caught in a more strongly typed language?
> a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.
You mention it in your edit, but Crystal has been exactly that for me. A rubyist for a decade I found Crystal to have the type system I was expecting all along.
Meh. Julia does nothing to help me catch errors before runtime, it's no different than Python in this regard. Although it does use the types to generate fast code (and in my experience it does live up to its performance claims).
I've seen some talk of Julia doing compile time checks, maybe in the future it will?
Dart isn't optionally typed any more. It's now a fully statically typed language, that also has a special "dynamic" type. That puts it in the same boat as C# and Scala, among others.
Optional or gradual typing does seem like an obvious brilliant idea from the outside. Start out dynamic when the program is small, layer in types when it grows to the point where you need them. Capture the union of both dynamically typed and statically typed users. Everyone wins!
In practice, we found ourselves in an uncanny valley where we were too typed for the dynamic typing folks, and too unsafe for the static ones. We couldn't deliver the user experience either camp expected. We learned, the hard way, that a statically typed language is not simply a dynamically typed language plus some type annotations. Everything about how you use the language is different.
---
The way you design APIs is different
Python's tuple type has a subscript operator to return an element at the given index. That's a perfectly reasonable, simple, clean API in a dynamically typed language. If you want to have statically typed tuples, that API doesn't even make sense:
t = (1, True, "three")
x = t[datetime.datetime.today().weekday() % 3]
What is the static type of x?
Another example: Python's list type has a sort() method. It takes an optional "key" argument that is a callback that converts each value to a key of some time and then sorts using those projected keys. If you pass a key function, then sort() needs to be a generic function that takes a type parameter for the return type of the key function, like:
sort<R>(key: (T -> R))
But if you don't pass the key function, the R type argument is meaningless. Should it be a generic method or not?
An even gnarlier question is "What kinds of lists can be sorted at all?" The sort() method works by calling "<" on pairs of elements. Not all types support that operation. Of those that do, not all of them accept their own type as the right-hand operand. How do you design the list class and the sort() method such that you ensure you won't get a type error when you call sort()?
To handle this kind of stuff, the "best practices" for your API design effectively become "the way you would design it in a fully statically-typed language". But those restrictions are one of the main reasons people like dynamic languages.
You can mitigate some of this with very sophisticated type system features. Basically design a type system expressive enough to support all of the patterns people like in dynamically typed languages. That's the approach TypeScript takes. But one of the main complaints with static type systems is that they are too complex for humans to understand and too slow to execute.
This makes that even worse. TypeScript's type system is very complex and type-checking performance is a constant challenge. In order to let you write "dynamic style" code, TypeScript effectively makes you pay for a super-static type system.
---
User expectations are bimodal
Once you ask people to design their APIs such that they can be statically typed and then let them start writing type annotations, we observed that they very quickly flipped a mental bit and expected the full static typing experience. They expected real static safety where certain errors were proven to be absent. They expected the performance of a statically-typed "native" language.
But most optional or gradually typed languages are unsound in order to allow typed and untyped code to intermingle. That means type errors can still sneak through and bite you at runtime. It means you get none of the compile-time performance benefits of static types. Sorbet asks you to write your code with all of the discipline, restrictions, and cognitive effort of a statically-typed language. In return, it gives you the runtime performance of... Ruby.
Worse, actually, because it is checking your type annotations dynamically at runtime. It basically turns your type annotations into assertions. So you get even more potential runtime failures.
This was how Dart 1.0 worked. I used to joke that we gave you the best of both worlds: the brevity of Java and the speed of JavaScript. And then I cried a little.
---
This sounds like I'm criticizing this approach to languages. I actually think TypeScript, Flow, Sorbet, and others are a really smart solution to a very challenging problem. If you have a very large corpus of dynamically-typed code that you want to keep extracting value out of, they give you a way to do that while getting some of the benefits of types. If I was sitting on a giant pile of JS or Ruby that I had no plans to rewrite, I would absolutely use one of these tools.
But for new development, I think you're much better off choosing a modern statically typed language if you think there's a chance your program will grow to some decent size. By that, I mean C#, Go, Swift, Dart, Kotlin, etc. Type inference gives you most of the brevity of dynamic types and you'll get all the safety and performance you want in return for your effort to type your code.
If you're going to do the work to make your code typable, you should get as much mileage out of it as you can. So far, no one I know has figured out how to do that with an optionally or gradually typed language.
---
This is, of course, just my personal preference. And I'm biased because I've already walk the long painful educational road to understand static types. One of the real large benefits of dynamic types is there is much less to learn before you can start writing real code. For new users, hobbyists, or people where programming isn't their main gig, this is huge. I love that dynamically typed languages exist and can serve those people.
But my experience is that if you're a full time professional software engineer writing real production code eight hours a day, it's worth it to get comfortable with static typing and use it. The fact that basically every large software shop that had a big investment in dynamically typed languages is trying to layer static typing on now probably tells us something. Google with Closure Compiler and Dart. Microsoft with VB.Net, TypeScript, and Pyright. Facebook with Hack and Flow. Apple with Swift.
TypeScript actually handles the first example quite well. If you simply have a heterogenous array type, the type of its members will be the union type of `number | boolean | string`.
If you’ve used a typed tuple, then the type after access is based on what TypeScript statically knows. So array[0] would be number, but array[random() % 3] would be the union type.
> But for new development, I think you're much better off choosing a modern statically typed language if you think there's a chance your program will grow to some decent size.
> By that, I mean C#, Go, Swift, Dart, Kotlin, etc.
I think all of those require compilation. While I loved writing C# in a previous job, the compilation step added some small amount of friction to regular web development.
Though working with PHP requires more thought for the big-picture stuff (no PHP ORM can touch what C# offers) I find it easier to get into a state of flow when developing new features. Once I've completed a given task I can run a static analysis tool (I've made one at Vimeo, but there are others to choose from) that can automatically add most of the types I neglected to add, and can suggest more.
Thanks for bringing up Dart (underrated IMHO). The built-in optional typing helps with productivity and readability. There is also a great official style guide which goes over when static/inferred/dynamic typing are preferred: https://dart.dev/guides/language/effective-dart/design#types
core.typed never really caught on though, what seems more popular is Schema which focuses on annotating and validating the structure of lists and maps: https://github.com/plumatic/schema
I'm actually working on solving this problem at the moment with https://darklang.com. Our approach is to allow the quick prototyping of python via tooling built-into the editor, within a language that has strong static types (similar to Haskell or OCaml).
As an example, you never change types in Dark, you only make new types and switch over to them, so if you want to test out a type change for just one HTTP route, you can do that.
Dark also doesn't have nulls or exceptions because they're hard to reason about. The usual tools to replace them (Result and Option/Maybe types) require you to handle all the cases when you write code using them. We're allowing you to write code that doesn't handle these cases (again, using editor tooling). Instead it tells you exactly what errors can happen at every point in your code. Once you have your initial prototype/algorithm figured out, you can use that information to handle all the edge cases.
> Instead it tells you exactly what errors can happen at every point in your code. Once you have your initial prototype/algorithm figured out, you can use that information to handle all the edge cases.
While I can't say much about Dark (the available blog posts [1] are shallow), I do think that the automation may be one key aspect of future programming languages. For example, when I'm thinking about gradual typing I don't only want to mix the hodge-podge unitype and actual types but also convert the former to the latter, and a large portion of the process can be automated in various ways (for example, one can track the typical runtime types that unityped variables have; the programmer can solidify compile-time types using that fact).
> I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code
Unless I'm misunderstanding something, PHP7 can do exactly that.
With regards to the general trend you mention about adding static-ish typing to dynamically typed languages, the opposite is also happening to some extent. I work in a medium sized company that does mainly .NET and I see C# devs use `var` a lot (in order to let the compiler infer the type instead of having to declare it explicitly). I'm not sure if the `dynamic` type is also seeing increased use, but just the fact that it was added to the language in v4 says at least a little.
I think what is really happening is that the more popular languages will sort of naturally converge as development progresses and more and more people request features. So while Mr. PHP-dev-turned-to-C# will maybe want more dynamic-ish typing in C#, Mr. C#-dev-turned-to-PHP will request more static-like typing in PHP.
> I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.
Fast prototyping without types? Meh! I need types to be able to prototype and change things really fast, knowing that it won't break.
I feel a lot more confident to prototype in OCaml/F# and then "downgrade" to an 'ordinary' language that needs more people to understand what is written, than the other way around (prototype in Python and move to something 'real' later)
I think that recent movement to put types in dynamic languages is just because of the need to fix existing projects, people are finally "getting it".
TypeScript is awesome in this regard. Almost makes JS bearable.
Just to be a little pedant, Dialyzer (an Erlang success typing lib) precedes Elixir and the other static typing efforts you mentioned by many years, way before this so called “static typing renaissance”.
Another thing to point out is that (dialyzer) typespecs are extremely prevelant in both Erlang and Elixir libraries and especially the core language libraries. So not only does dialyzer precede the others, it’s become a core part of Elixir and Erlang. In contrast, mypy & sorbet appear to be largely second class tools. TypeScript though appears to have made more inroads.
Type inference is where it's at. Everybody loves types when there's hardly any overhead. Typescript is the best example of this.
My theory, the first languages of most tend to be untyped. Over the years you get tired of dealing with type errors and move to more complex languages with strong typing. After a while you get tired of typing a bunch of useless crap because the compiler isn't smart enough to figure out the type for you and land in Typescript or similar
here's one more (not so popular) one: https://inko-lang.org
I'm also a fan of clojure.spec, though not a substitute for a proper type system, is extremely helpful in achieving the same goal.
> Eg in Typescript you will often still run into bugs caused by malformed JSON that doesn't fit the type declaration, badly or insufficiently typed third party libraries
there's a fantastic typescript library, io-ts (https://github.com/gcanti/io-ts), that provides the ability to declare runtime types variables that you can infer compile time types from that solves exactly this problem. it's deifnitely work taking a close look at if you want to ensure type safety at runtime for data coming from third parties.
Apache Groovy is two languages. The Groovy 2.x download, first released in 2012, bundles two different compilers that were both forked from the Groovy 1 compiler. Only one of them has upgraded to the JDK-7 invoke-dynamic capabilities, and the other (which hasn't) is the one actually used by Gradle and Jenkins and everyone else. Last month, the Groovy project managers at the ASF announced they were keeping the upcoming Groovy 3 as two separate languages also. The long-awaited parser upgrade from Antlr 2 to Antlr 4 is being bolted on to the invoke-dynamic compiler only -- the compiler no-one uses. They talked about Groovy 4 reverting back to a single language, but i'm guessing that's many years away because the original purpose of making Groovy be two languages in the first place was to not change the language that users actually use in any way, while simultaneously appeasing their own developers by bundling the code they wrote (invoke-dynamic bytecode generation, Antlr 4 parser upgrade, etc) in the language.
I prefer how this evolves organically if / when there's a need. Once the need is proven by adoption numbers (say this ruby w/ type checking gets popular) you'll have a good idea of what people want (and any issues they might have with this implementation).
> I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.
With the addition of `var`, I think Java is that language.
A fascinating part about Sorbet is it did not have to introduce any additional syntax to Ruby (unlike TypeScript, for example). This really speaks to the expressiveness of Ruby. Very cool.
Was that additional syntax in TypeScript actually neccessary for type inferrence? Or is it rather to avoid API hazards when you change some internal code and suddenly the API of your library breaks because the inferred type has changed.
It is necessary because of some limitations, but IMO it's also a great idea nonetheless. Types' names are a great documentation, one which you can't get with pure inference.
Even languages that have (close to) the best possible inference, like OCaml, still have additional syntax for defining types, because it 1. gives you documentation and 2. allows you to do things that are mathematically proven to be impossible via inference.
TS is great, and also moving really fast and becoming better every 2-3 months.
It has a few overlapping features with Sorbet, with one major difference being that Solargraph type checking relies on YARD documentation instead of annotations.
That's a pretty unintuitive use, since "sigil" is more commonly used (in programming languages) as a single symbol, as in a non-alphanumeric character that's used as some kind of syntax.
I remember there was this CEO once who was new to the software industry and was looking for a word to describe non-small-business customers. When people suggested "Enterprise", he instantly dismissed it, and when they insisted this is already the word we all use to mean this, he actually opened the dictionary to prove that it wasn't quite accurate. What I took from this is that, when cultures or conventions already have momentum, sometimes you just have to go with it. This is the same reason I don't like Go.
Bummer. That kind of name overloading has the potential to be needlessly confusing down the line. "Sigils, I mean well, they're like pragmas but in Sorbet we call them sigils." [x a billion]
Now's the time to fix that stuff.
(In any case, I'm quite keen to start playing with sorbet, looks great!)
The terms all overlap a bit, but I would interpret "directive" or "pragma" to indicate that it's conveying meaning to the Ruby interpreter. This is exactly the opposite -- it's a comment as far as the Ruby language is concerned while conveying meaning to an external tool.
I've never understood the so called advantages of dynamic typing. To me it looks like a land mine in one's project waiting to blow at run time. And what for? Do developers code so fast that the time spent on typing something like "int i" will provide any real savings? Now vendors are trying to patch those with bolted on top syntax extensions/derived languages that need to be transpiled. What a mess.
Most people fixate on the terseness that it can afford in a language. I suppose I do like that but for me it is not a huge deal.
What I think is more important is the flexibility that it brings to express design patterns that in other languages, like Java for example, can become very cumbersome. I can’t tell you how many times I have been in the bowels of some Java code and found some method that takes a concrete implementation of something that could or should be an interface when I really want to pass in something different. Then you are like “let me extend and fix this class” and then you end up just extending and fixing 1/2 the code base to get done what needs to be done. In a language like Ruby I would just pass in an object that responds to all the needed methods and it would happily work. Ideally you wouldn't get into these type of messes in statically typed languages because people would follow good design principles all the time. But people are fallible and in reality messes are everywhere in statically typed languages.
So I think the approach of adding type enforcement if desired is a nice approach considering there is a large amount of code out there that probably doesn’t benefit much from it.
When you're consuming JSON that has deep nesting, arrays that contain multiple types, etc. Something that may be two lines of code in a dynamic language could be as much as 100 lines in some static typed languages.
Sure, you could write one line of code to read it in as maps and arrays, but at some point you need to validate your input from the outside world to convert it you your domain objects. Using a dymically typed language doesn't magically make that problem go away.
It makes unit testing much easier. That’s the best explanation I’ve found. Rapid prototyping too, but that just means you’re backloading tech debt so that’s at best neutral in pure technical terms. In a startup context backloading tech debt is deeply desired.
Type checking isn’t really related to typing “int.” Many languages infer types. In fact, Hindley Milner type systems should be able to infer all types without explicitly specifying any of them.
Sometimes I do code that fast. When you’re messing around just trying to find out if something is possible you want to write as much code in as short a time as possible, and Python shines for that. Every second wasted typing is a second that could have been spent moving forward.
Of course the problem is that the prototypes are terrible to maintain and eventually need unit tests and typing. But you don’t want to waste time adding those things if you’re not even sure your idea will work. I use strongly typed languages in production and couldn’t imagine using Python for that.
If typing speed is your main limitation, it means either you can improve your productivity dramatically by improving your typing skills, or your language is limiting you so much that you don't have good abstractions to enable you to think at a higher level.
I don't see how typing F# is slower, longer or less convenient than typing Python (spoiler alert, it's not). And you also get things like actual lambdas, pattern matching, currying, real parallelism and more and more.
It's only that people believe there is no need to learn anything beyond Python because it's "easy", which it's not, once you go beyond several hundred lines of code. But the myth somehow continues to persist.
This logic transpiles(TM) to this in my brain:
a) I am messing around and need to write 10 lines of code. Can I write it fast and without thinking? Sure and not needing to type int/float/whatever will save me couple of seconds. If I iterate and rewrite that short piece 10 times then I just saved 20 seconds. Chirp chirp ... . Do I need special language for just that? Lemme guess ...
b) I am messing around and need to write few hundred or thousands lines of code. Can I write it fast? Maybe but it is likely that good chunk of time will be spent thinking. I doubt that at this point not declaring type will save anything worth noticing.
But that of course is my opinion
sig {params(person: Person).returns(Integer)}
def name_length(person)
Not sure if I dig the syntax. Furthermore arguments seems to be the official names for method arguments, not parameters. eg, `ArgumentError`. `params` also feels like it's linked to Rails `params` variable in controllers. It can be confusing.
Something like this will also feel more Rubyist:
def name_length person: Person, return: Integer
But it probably requires a deeper hack or a change in MRI.
As for the syntax change, we are actually on our 8th iteration of the syntax. We really wanted this to NOT be a fork of Ruby so finding something compatible was very important. For example that's why it has the weird `sig {` syntax too, we didn't want to have to cause load-time and cyclic dependencies from adding type signatures.
I'm not sure how consistent it is with everything in Ruby, but parameters is technically the correct term here. A parameter is a variable definition, while an argument is the value that is passed to the parameter.
ArgumentError is still consistent with this definition (it's an error with the value you passed, not with the definition). However, params in a Rails controller does violate this definition.
Different levels of abstraction/concerns. Params in Rails come from HTTP params i.e the conceptual merge between GET query strings and POST body (e.g forms, but also JSON).
In computer science, "formal parameters" is the name for those named variables that are established on entry into the function and immediately receive external values. "arguments" are the values that they receive. A function has only one set of parameters, but a new set of arguments in each invocation.
Dont know if this applies, but my understanding is that in functions, a parameter is the name of a declaration which when called will receive an argument.
It’s my understanding that the Sorbet team is involved with bringing types to Ruby 3. I’m unclear on whether it will be Sorbet itself or if it’s elements of it. Can’t dig up the source right now, maybe someone can corroborate this?
Though I haven’t yet used it for anything in production, I think if I were starting something greenfield and wanted “Ruby with static types” I would go with Crystal. I really enjoy writing it and the performance you can get is quite a significant boost over Ruby.
I’d still go Ruby. A language’s ecosystem and community are as much factors in why someone should choose or avoid it as its syntax. Both of those things are fantastic for Ruby — I’d argue that they’re some of its best features, in fact. Crystal? Not so much.
I’m a long time veteran of Ruby and someone who deployed production Rails apps in EARLY v1. I absolutely love and adore it and it’s by far my favorite language to work with. That being said, when I can write in a very stunningly similar language and get 10 to 100x performance with very little extra effort I am going to strongly consider it when deciding on my stack. Also the ecosystem for crystal is not terrible at all. I think it’s a great project and shouldn’t be ignored because “the ecosystem”
Typescript (with stellar adoption), native type annotation support in Python, Sorbet, PHP 7, Elixir + Dialyzer, ...
I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.
I guess in part because it's a big challenge to come up with a coherent type system that allows this, the bifurcation in the ecosystem, and often a somewhat leaky abstraction. Eg in Typescript you will often still run into bugs caused by malformed JSON that doesn't fit the type declaration, badly or insufficiently typed third party libraries, ....
Google's Dart is the only recent, somewhat popular language (only due to Flutter) that allows this natively - that I can think of right now.
I do think such a language would be very beneficial for the current landscape though, and projects like this show there is a clear need.
Edit: just remembered another option: Crystal. Also Julia, as pointed out below.
I wonder whether the perception that type safety slows down Ruby (or ES6) development comes from the fact that the type systems are bolted on after the fact.
Being correct from day one will cause too much unnecessary friction. You (usually) don't know the entire program architecture until you make a full prototype, and even if you have a plan, there will always be some part where some unexpected consequences force you to rearchitect some parts. And since you don't know the entire program architecture you architect your program bottom-up, and most of the dynamically-typed languages allow an interactive development environment at a flexibility that typed languages can't provide.
Think about developing Python inside a Jupyter notebook, or Common Lisp inside a REPL. You turn on a REPL, open up a file, write a function, send the function to the REPL, test the function you just wrote, and when I find a mistake I can just redefine any function I would like to change. This process allows fixing mistakes on-the-fly. Typed languages don't allow this (even ones that have a so-called REPL) at this flexibility, (since they emphasize on being correct all the time), and cause too much unnecessary friction while prototyping.
Thus a need for a dynamically-typed language that can enforce types after prototyping.
I'm sure those languages and their ilk have advantages for prototyping, but I agree, mandatory typing in other languages isn't a burden. If you already have to reason about what arguments are acceptable in functions, what objects can receive what messages and what those messages should contain, you already have typing — just inefficiently stored in short term memory.
Those who are the biggest proponents of these languages as useful for prototyping are also not the ones who rewrite their code in type-safe languages, so being able to add type annotations for the sake of their colleagues who eventually have to turn these prototypes into code upon which a team can collaborate can only be a useful thing.
This is by design. This is great for prototyping.
These languages are to programming what breadboards and wires are for electronics.
Of course, as your system grows, you start to want static checks, or having your schematics on a PCB. But at the very start, tweaking a piece of Python code, or a breadboard, is easiest. Then, of course, you may not want to afford a a rewrite to rust, or would put your breadboard in a box and ship it :)
No, you don't. You need unit tests to verify behavior (values) whether or not you have static typing (except for output types with only zero or one values). Now, it's true, that having such tests also verifies, at no extra charge, things that Go’s type system would verify, but without the additional effort of type annotations. But there's no added cost there, it's a net savings.
While if you've only used dynamic languages or badly typed languages, then having to deal with this stupid naggy compiler is just annoying. A big part of learning a strongly statically typed language is learning that the compiler is your friend, and that errors are good. I've noticed that a lot of people new to TypeScript try to get the compiler to shut up, often resorting to any or @ts-ignore, while more advanced users will see it as a dialogue. The compiler complains? Okay, something's wrong: Let's find the root cause here.
TypeScript took off because people had no choice but to write JS, so any benefit was better than no benefit. Sorbet was also borne out of an existing codebase. But a new language wouldn't have this lock in factor.
I am an "expert" in static type systems (I'm familiar with Java, Scala, OCaml, Haskell, Rust, Go, TypeScript, C++ ... and keep up to date with latest type systems research like 1ML, MLsub, Liquid Haskell, ...), but I have a really hard time imagining how one would develop a statically typed library that would even approximate the usefulness and convenience (for rapid prototyping and interactive data analysis) of Python's Pandas (although if I was a betting man, I'd wager the best language to implement it in would be Scala, with it's advanced macros & almost-dependent type system).
You mention it in your edit, but Crystal has been exactly that for me. A rubyist for a decade I found Crystal to have the type system I was expecting all along.
https://julialang.org/
The first three selling points on their home page are Fast, Dynamic, Optionally Typed.
I've seen some talk of Julia doing compile time checks, maybe in the future it will?
Deleted Comment
Optional or gradual typing does seem like an obvious brilliant idea from the outside. Start out dynamic when the program is small, layer in types when it grows to the point where you need them. Capture the union of both dynamically typed and statically typed users. Everyone wins!
In practice, we found ourselves in an uncanny valley where we were too typed for the dynamic typing folks, and too unsafe for the static ones. We couldn't deliver the user experience either camp expected. We learned, the hard way, that a statically typed language is not simply a dynamically typed language plus some type annotations. Everything about how you use the language is different.
---
The way you design APIs is different
Python's tuple type has a subscript operator to return an element at the given index. That's a perfectly reasonable, simple, clean API in a dynamically typed language. If you want to have statically typed tuples, that API doesn't even make sense:
What is the static type of x?Another example: Python's list type has a sort() method. It takes an optional "key" argument that is a callback that converts each value to a key of some time and then sorts using those projected keys. If you pass a key function, then sort() needs to be a generic function that takes a type parameter for the return type of the key function, like:
But if you don't pass the key function, the R type argument is meaningless. Should it be a generic method or not?An even gnarlier question is "What kinds of lists can be sorted at all?" The sort() method works by calling "<" on pairs of elements. Not all types support that operation. Of those that do, not all of them accept their own type as the right-hand operand. How do you design the list class and the sort() method such that you ensure you won't get a type error when you call sort()?
To handle this kind of stuff, the "best practices" for your API design effectively become "the way you would design it in a fully statically-typed language". But those restrictions are one of the main reasons people like dynamic languages.
You can mitigate some of this with very sophisticated type system features. Basically design a type system expressive enough to support all of the patterns people like in dynamically typed languages. That's the approach TypeScript takes. But one of the main complaints with static type systems is that they are too complex for humans to understand and too slow to execute.
This makes that even worse. TypeScript's type system is very complex and type-checking performance is a constant challenge. In order to let you write "dynamic style" code, TypeScript effectively makes you pay for a super-static type system.
---
User expectations are bimodal
Once you ask people to design their APIs such that they can be statically typed and then let them start writing type annotations, we observed that they very quickly flipped a mental bit and expected the full static typing experience. They expected real static safety where certain errors were proven to be absent. They expected the performance of a statically-typed "native" language.
But most optional or gradually typed languages are unsound in order to allow typed and untyped code to intermingle. That means type errors can still sneak through and bite you at runtime. It means you get none of the compile-time performance benefits of static types. Sorbet asks you to write your code with all of the discipline, restrictions, and cognitive effort of a statically-typed language. In return, it gives you the runtime performance of... Ruby.
Worse, actually, because it is checking your type annotations dynamically at runtime. It basically turns your type annotations into assertions. So you get even more potential runtime failures.
This was how Dart 1.0 worked. I used to joke that we gave you the best of both worlds: the brevity of Java and the speed of JavaScript. And then I cried a little.
---
This sounds like I'm criticizing this approach to languages. I actually think TypeScript, Flow, Sorbet, and others are a really smart solution to a very challenging problem. If you have a very large corpus of dynamically-typed code that you want to keep extracting value out of, they give you a way to do that while getting some of the benefits of types. If I was sitting on a giant pile of JS or Ruby that I had no plans to rewrite, I would absolutely use one of these tools.
But for new development, I think you're much better off choosing a modern statically typed language if you think there's a chance your program will grow to some decent size. By that, I mean C#, Go, Swift, Dart, Kotlin, etc. Type inference gives you most of the brevity of dynamic types and you'll get all the safety and performance you want in return for your effort to type your code.
If you're going to do the work to make your code typable, you should get as much mileage out of it as you can. So far, no one I know has figured out how to do that with an optionally or gradually typed language.
---
This is, of course, just my personal preference. And I'm biased because I've already walk the long painful educational road to understand static types. One of the real large benefits of dynamic types is there is much less to learn before you can start writing real code. For new users, hobbyists, or people where programming isn't their main gig, this is huge. I love that dynamically typed languages exist and can serve those people.
But my experience is that if you're a full time professional software engineer writing real production code eight hours a day, it's worth it to get comfortable with static typing and use it. The fact that basically every large software shop that had a big investment in dynamically typed languages is trying to layer static typing on now probably tells us something. Google with Closure Compiler and Dart. Microsoft with VB.Net, TypeScript, and Pyright. Facebook with Hack and Flow. Apple with Swift.
If you’ve used a typed tuple, then the type after access is based on what TypeScript statically knows. So array[0] would be number, but array[random() % 3] would be the union type.
> By that, I mean C#, Go, Swift, Dart, Kotlin, etc.
I think all of those require compilation. While I loved writing C# in a previous job, the compilation step added some small amount of friction to regular web development.
Though working with PHP requires more thought for the big-picture stuff (no PHP ORM can touch what C# offers) I find it easier to get into a state of flow when developing new features. Once I've completed a given task I can run a static analysis tool (I've made one at Vimeo, but there are others to choose from) that can automatically add most of the types I neglected to add, and can suggest more.
Gilad was (religiously) in favor of optional types. Did he come around to agreeing with the direction that Dart has taken (i.e. full static types)?
Deleted Comment
an anonymous (Integer | Bool | String) sum-type, of course.
As an example, you never change types in Dark, you only make new types and switch over to them, so if you want to test out a type change for just one HTTP route, you can do that.
Dark also doesn't have nulls or exceptions because they're hard to reason about. The usual tools to replace them (Result and Option/Maybe types) require you to handle all the cases when you write code using them. We're allowing you to write code that doesn't handle these cases (again, using editor tooling). Instead it tells you exactly what errors can happen at every point in your code. Once you have your initial prototype/algorithm figured out, you can use that information to handle all the edge cases.
While I can't say much about Dark (the available blog posts [1] are shallow), I do think that the automation may be one key aspect of future programming languages. For example, when I'm thinking about gradual typing I don't only want to mix the hodge-podge unitype and actual types but also convert the former to the latter, and a large portion of the process can be automated in various ways (for example, one can track the typical runtime types that unityped variables have; the programmer can solidify compile-time types using that fact).
[1] https://medium.com/darklang
> Dark also doesn't have nulls or exceptions because they're hard to reason about.
How do you handle OutOfMemory errors?
Unless I'm misunderstanding something, PHP7 can do exactly that.
With regards to the general trend you mention about adding static-ish typing to dynamically typed languages, the opposite is also happening to some extent. I work in a medium sized company that does mainly .NET and I see C# devs use `var` a lot (in order to let the compiler infer the type instead of having to declare it explicitly). I'm not sure if the `dynamic` type is also seeing increased use, but just the fact that it was added to the language in v4 says at least a little.
I think what is really happening is that the more popular languages will sort of naturally converge as development progresses and more and more people request features. So while Mr. PHP-dev-turned-to-C# will maybe want more dynamic-ish typing in C#, Mr. C#-dev-turned-to-PHP will request more static-like typing in PHP.
Popularity aside, Perl 6 supports exactly this.
I feel a lot more confident to prototype in OCaml/F# and then "downgrade" to an 'ordinary' language that needs more people to understand what is written, than the other way around (prototype in Python and move to something 'real' later)
I think that recent movement to put types in dynamic languages is just because of the need to fix existing projects, people are finally "getting it".
TypeScript is awesome in this regard. Almost makes JS bearable.
My theory, the first languages of most tend to be untyped. Over the years you get tired of dealing with type errors and move to more complex languages with strong typing. After a while you get tired of typing a bunch of useless crap because the compiler isn't smart enough to figure out the type for you and land in Typescript or similar
there's a fantastic typescript library, io-ts (https://github.com/gcanti/io-ts), that provides the ability to declare runtime types variables that you can infer compile time types from that solves exactly this problem. it's deifnitely work taking a close look at if you want to ensure type safety at runtime for data coming from third parties.
Also Dart 2.0 is strongly typed with type inference, they rebooted the type system.
Apache Groovy is two languages. The Groovy 2.x download, first released in 2012, bundles two different compilers that were both forked from the Groovy 1 compiler. Only one of them has upgraded to the JDK-7 invoke-dynamic capabilities, and the other (which hasn't) is the one actually used by Gradle and Jenkins and everyone else. Last month, the Groovy project managers at the ASF announced they were keeping the upcoming Groovy 3 as two separate languages also. The long-awaited parser upgrade from Antlr 2 to Antlr 4 is being bolted on to the invoke-dynamic compiler only -- the compiler no-one uses. They talked about Groovy 4 reverting back to a single language, but i'm guessing that's many years away because the original purpose of making Groovy be two languages in the first place was to not change the language that users actually use in any way, while simultaneously appeasing their own developers by bundling the code they wrote (invoke-dynamic bytecode generation, Antlr 4 parser upgrade, etc) in the language.
With the addition of `var`, I think Java is that language.
Even languages that have (close to) the best possible inference, like OCaml, still have additional syntax for defining types, because it 1. gives you documentation and 2. allows you to do things that are mathematically proven to be impossible via inference.
TS is great, and also moving really fast and becoming better every 2-3 months.
It has a few overlapping features with Sorbet, with one major difference being that Solargraph type checking relies on YARD documentation instead of annotations.
I'm going to give Solargraph a look-see.
> # typed: true
Isn't this called a directive/pragma? A sigil is a symbol on a name.
Either way, I'm excited to see this finally out after seeing the past presentations on it.
> Google defines sigil as, “an inscribed or painted symbol considered to have magical power,” and we like to think of types as pretty magical
https://sorbet.org/docs/static#fn1
https://en.wikipedia.org/wiki/Sigil_(computer_programming)
Now's the time to fix that stuff.
(In any case, I'm quite keen to start playing with sorbet, looks great!)
EDIT: I'm guessing "sigil" was chosen because it's closely matching the "signatures" or Sigil.sig method name?
https://ruby-doc.org/docs/ruby-doc-bundle/UsersGuide/rg/vari...
What I think is more important is the flexibility that it brings to express design patterns that in other languages, like Java for example, can become very cumbersome. I can’t tell you how many times I have been in the bowels of some Java code and found some method that takes a concrete implementation of something that could or should be an interface when I really want to pass in something different. Then you are like “let me extend and fix this class” and then you end up just extending and fixing 1/2 the code base to get done what needs to be done. In a language like Ruby I would just pass in an object that responds to all the needed methods and it would happily work. Ideally you wouldn't get into these type of messes in statically typed languages because people would follow good design principles all the time. But people are fallible and in reality messes are everywhere in statically typed languages.
So I think the approach of adding type enforcement if desired is a nice approach considering there is a large amount of code out there that probably doesn’t benefit much from it.
Example
This is my go to tool (and language) nowadays when I need to do something JSON heavy for a prototype.
Deleted Comment
Of course the problem is that the prototypes are terrible to maintain and eventually need unit tests and typing. But you don’t want to waste time adding those things if you’re not even sure your idea will work. I use strongly typed languages in production and couldn’t imagine using Python for that.
It's only that people believe there is no need to learn anything beyond Python because it's "easy", which it's not, once you go beyond several hundred lines of code. But the myth somehow continues to persist.
Something like this will also feel more Rubyist:
But it probably requires a deeper hack or a change in MRI.We used `params` because Method#parameters was what they called it in the standard library. I actually had it as `args` originally until someone pointed this out. https://ruby-doc.org/core-2.6.3/Method.html#method-i-paramet...
As for the syntax change, we are actually on our 8th iteration of the syntax. We really wanted this to NOT be a fork of Ruby so finding something compatible was very important. For example that's why it has the weird `sig {` syntax too, we didn't want to have to cause load-time and cyclic dependencies from adding type signatures.
Super interesting. We should probably have being consistent for naming parameters vs. arguments in stdlib. It's too late though!
ArgumentError is still consistent with this definition (it's an error with the value you passed, not with the definition). However, params in a Rails controller does violate this definition.
`ArgumentError` is consistent with an error during call time.
As far as ruby goes, though, it would conflict with keyword arguments.
Deleted Comment
[0] https://developer.squareup.com/blog/rubykaigi-and-the-path-t...