Me too. Or whenever I see it mentioned in this Slack I'm in. It was very confusing last December, when one guy kept solving Advent of Code in Crystal, and doing so with speed.
> Be statically type-checked, but without having to specify the type of variables or method arguments.
My experience with both Go (type annotations always) and Python (type annotations sometimes) has me pretty skeptical about this misfeature (perhaps someone with Crystal or OCaml experience can tell me why I'm wrong).
When implemented properly (i.e., like Go, not like Java), code with annotations is more readable (this isn't controversial), and adding them is a matter of keystrokes. Note that this isn't a static vs dynamic argument; Crystal is still a static language and you still have to deal with type errors, so you don't get the "get the happy path working at the expense of bugs in other paths" rapid-prototyping benefits that dynamic languages boast about; you just hit fewer keys (maybe this is Crystal's workaround for supporting multi-char scope delimiters!).
I'm guessing in practice you have a mediocre editor that will show you the type annotations with some nonzero amount of effort, and when you're doing code review, you're just forced to jump around to figure out what types are? Or depend on docstrings that grow stale? These seem like a lot of costs, not to mention the effort from the dev team to support this "feature", all to save users a few keystrokes?
In case it's not clear from the above: Crystal supports type annotations, they are enforced by the compiler, and it's not uncommon to use them in practice.
However, there are some cases where the interface for an object is obvious but hasn't been named formally. For example, consider the following valid Crystal method:
def json_vs_yaml(obj)
puts obj.to_json
puts obj.to_yaml
end
In a more strictly typed language (that doesn't support intersection types), I couldn't do that without first defining some interface that includes `to_json` and `to_yaml` and then maybe convincing the compiler that my objects qualify for the interface (by extending their class definitions or casting at the call site), which is a lot of work to enable a trivial method whose purpose is already clear. In Crystal, the compiler just checks that whatever you're passing into the method has `to_json` and `to_yaml` defined and lets you get on with life.
In more complex cases, I almost always find myself including type annotations in method signatures (which will be enforced at compile time) anyway because it makes writing the method easier. And I certainly won't tell you you're wrong to prefer a language with more stringent requirements, especially if you have to work with other people's code a lot. But I do believe that there is a minority of cases where leaving off the type increases the clarity and conciseness of the code.
Another good case is removing redundancy on wrapper methods:
def foo(x : Int32) : Bool
x == 12
end
def add_and_foo(left, right)
foo(left + right)
end
Here, `foo` is fully specified and doesn't rely on type inference at all. `add_and_foo` automagically works on any types that return an Int32 when you add them. If I later update `foo` to operate on a different numeric type, `add_and_foo` still just does the right thing without needing any edits. Even though in practice `add_and_foo` as written is probably going to operate on other Int32's all the time, conceptually the method's parameters are of type "whatever foo takes" and its return type is "whatever foo returns" and letting the compiler enforce that via type inference communicates this intention more clearly than specifying those types concretely.
> In case it's not clear from the above: Crystal supports type annotations, they are enforced by the compiler, and it's not uncommon to use them in practice.
Yes, I considered that when I was writing my post. :)
> In a more strictly typed language (that doesn't support intersection types), I couldn't do that without first defining some interface that includes `to_json` and `to_yaml` and then maybe convincing the compiler that my objects qualify for the interface (by extending their class definitions or casting at the call site), which is a lot of work to enable a trivial method whose purpose is already clear. In Crystal, the compiler just checks that whatever you're passing into the method has `to_json` and `to_yaml` defined and lets you get on with life.
> Another good case is removing redundancy on wrapper methods:
I fully agree that there are cases where the relative number of keystrokes saved are considerable; however, these cases are quite rare and keystrokes are much, much cheaper than readability. Also, structural subtyping (aka protocols aka 'static duck typing') allows the compiler to determine whether or not a type implements an interface without an explicit `implements` declaration.
In any case, I don't want to put too fine a point on it. In the worst case it's a relatively minor inconvenience and perhaps it adds more ergonomics than I'm giving it credit for.
From the docs:
"Crystal is a programming language with the following goals:
Have a syntax similar to Ruby (but compatibility with it is not a goal).
Be statically type-checked, but without having to specify the type of variables or method arguments.
Be able to call C code by writing bindings to it in Crystal.
Have compile-time evaluation and generation of code, to avoid boilerplate code.
Compile to efficient native code."
See: https://crystal-lang.org/docs/