Skip to content
Johannes Müller edited this page Jan 29, 2020 · 17 revisions

Why isn't there Windows support yet?

Windows support is under development. The reasons it's not currently supported are:

  1. Windows APIs are different than linux/mac, which are mostly POSIX-compliant.
  2. None of the core developers use Windows so there's no "dog-fooding" need for it. Core developers use mac/linux, either as desktop machines or servers.
  3. Travis doesn't support Windows, so even if we add basic support for it, if the language and standard library continue evolving and we don't have a reliable way to test that Windows support doesn't break then it's not of much use; nevertheless, we are also working on a CI redesign.

You can follow the progress of Windows support in #5430.

Why isn't the language indentation based?

Apart from the "Crystal has Ruby-inspired syntax" reason, there are more reasons:

  1. If you copy and paste a snippet of code, you have to manually re-indent the code for it to work. This slows you down if you just wanted to do a quick test. And, since Crystal has a built-in formatter, it can re-indent the code automatically for you.
  2. If you want to comment some code, for example comment an if condition, you have to re-indent its body. Later you want to uncomment the if and you'll need to re-indent the body. This slows you down and it's cumbersome.
  3. Macros become harder to write. Consider the json_mapping macro. It defines defs, uses case ... when ... else ... end without having to bother whether the generated code will be indented. Without end, the user would have to correctly indent the lines that would be generated.
  4. If you want a template language like ERB or ECR for a language that doesn't care about whitespace, you'll have to put those end to signal where conditions/loops/blocks end.
  5. Right now you can do: [1, 2, 3].select { |x| x.even? }.map { |x| x.to_s }. Or you can do it with do .. end. How would you chain calls in an indentation-based language? Usage of { ... } is not valid, only indentation should be used to match code blocks.
  6. Assuming one day we have a REPL, in which you tend to write code quickly, it's tedious and bug-prone to match indentation, because whitespace is basically invisible.

Because of all the above reasons, know that the end keyword is here forever: there's no point in trying to suggest changing the language to an indentation-based one.

Why don't you add syntax for XYZ?

Before suggesting syntax additions, ask this question:

  • Can it be currently done with the current syntax?

If the answer is "yes", there's probably no need to add new syntax for something that can be already done. Adding syntax means we have to be sure it doesn't conflict with the existing syntax. all users will have to learn something new, and it needs to be documented.

Maybe the current syntax is long to write or involves a couple of composed methods, but we should favor method composition instead of specific rules for specific problems.

Language X has feature Y. Why don't you have such feature?

If language X is not similar to Crystal (for example, language X has no mutable data, or is purely functional) then chances are that feature Y exists in language X because without it programming would be tedious or maybe impossible. In this case chances of adding Y to Crystal are null.

Some examples:

  • Making the GC optional: impossible, the whole language needs to change.
  • Making all data immutable by default: impossible, the whole language and standard library needs to change.
  • Adding Elixir's pipe operator (|>): in OOP languages we have methods and self. See #1388.
  • Adding Elixir/Erlang guards: not really needed, use if, is_a? or type restrictions.

Please don't insist on these things because they will never, ever happen.

Why trailing while/until is not supported, unlike Ruby?

In Ruby a trailing while comes in two flavors:

# This one first checks <condition> and then executes <code>
<code> while <condition>

# This one first executes <code> and then checks <condition>
begin
  <code>
end while <condition>

We find this logic confusing, and even Matz regrets it.

We had four options:

  1. Keep the same semantic as Ruby.
  2. Unify the semantic of both constructs.
  3. Disallow the second construct (which Matz seems to regret)
  4. Disallow both constructs.

We didn't want Option 1 because that would be to keep a mistake.

Option 2 is the worst choice because it will be surprising for those who come from Ruby: their code will compile fine but behave in a different way.

Option 3 sounds good, but for someone learning Crystal without previous Ruby experience, we think it might be confusing. Imagine you don't know Ruby's semantic and you see this:

<code> if <condition>

Here, it doesn't make sense to execute <code> without first checking <condition>. However, if we change the if to a while:

<code> while <condition>

there are now two possibilities: execute <code> and then check the <condition> (in a loop), or check the <condition> and then execute <code> (in a loop). And <code> comes before <condition>, so you might consider that possibility.

So, to remove all ambiguity, so that programmers don't have to stop thinking about what happens first, we decided to go with Option 4.

You can always replace this:

# Ruby
<code> while <condition>

with this:

# Crystal and Ruby
while <condition>
  <code> 
end

and this:

# Ruby
begin
  <code>
end while <condition>

with this:

# Crystal and Ruby
loop do
  <code> 
  break unless <condition>
end

while is used much less frequently than if, so we think this is the correct choice.

As a bonus, the compiler's code and logic becomes simpler, because there's only one mode of operation for while, and there are less things to learn.

Why are parentheses mandatory for def arguments?

The main reason is that this:

def method arg : String
end

is ambiguous: is String an argument type restriction, or the method's return type? Even if we always associate to the left, it's confusing because one usually scans past the last colon to check the return type, and here you can't do that.

The second reason is that it makes defs have a single, unified style across a project, and between projects. In our experience leaving the parentheses off leads to some discussions between members of a project. These discussions disappear if there is only one way to do it.

Why are aliases discouraged?

Ruby has many aliases: length, size and count for Array, Hash, String and Enumerable. There's also map/collect, find/detect, select/find_all, etc. In our opinion, this is bad:

  • Having more than one way to do a single thing implies learning more: you have to know all of the aliases to potentially understand code, because someone else might use an alias you don't use.
  • If you want to implement a type similar to Array, you have to define all of the aliases for someone's code to work. In Crystal, where implementing efficient containers is possible (in Ruby too, but you probably have to do it in C), this is very important to make it easy to do this.
  • In a dynamically typed language, that alias definition must exist in memory for no real reason. In a statically typed language that alias must exist somewhere, slowing down (a bit) the semantic and codegen phase, and ending with a (slightly) bigger executable.
  • It opens up the door for useless discussion: should length be preferred over count?

Why are there no block comments?

Using single-line comments on every line is clearer. It's also much easier and faster to just select a region of code and apply the comment shortcut in your editor. Having to mark the beginning of the comment, and then the end of it is slower (issue comment).

There should be a shortcut in your editor to comment multiple lines at once.

Why my array literal doesn't work?

Crystal infers the type of arrays from its elements, to the most specific type. So for example:

class A; end
class B < A; end
class C < A; end

ary_a = [A.new] # Array(A)
ary_b = [B.new] # Array(B)

If you then try to assign ary_b to an instance variable that is of type Array(A), it won't work:

class AHolder
  def initialize(@ary : Array(A))
  end
end

AHolder.new(ary_a) # ok
AHolder.new(ary_b) # error

This gives an error, because in @ary you can insert any A, for example a C, but then you would be inserting a C inside ary_b too, which is an Array(B), and with that you break the type system.

The solution is:

ary_b = [B.new] of A # now this is Array(A)

# Or, if you already have an Array(B)...
ary_b = [B.new]
AHolder.new(ary_b.map(&.as(A))

Note that this works in a different way with type restrictions:

def foo(ary : Array(A))
end

foo(ary_b) # works

Type restrictions are less restrictive about this. It's only an error if the array effectively gets assigned to an instance or class variable.

This, of course, applies to any generic type, not only Array.

Why there are no increment (++) and decrement (--) operators?

See Why don't you add syntax for XYZ?

Is there an equivalent to Ruby's Array#pack/String#unpack

Actually, Crystal has a far superior tool for solving the problems that pack/unpack solve: IO. You can do almost the same things as with pack/unpack and it even offers better readable code, type safety, and more performance and flexibility. Example:

n = [65, 66, 67]

# Ruby
n.pack("ccc") #=> "ABC"

# Crystal
String.build do |io|
  n.each do |number|
    io.write_byte number.to_u8
  end
end #=> "ABC"

The Crystal implementation is a bit more verbose, but that only helps to understand what the code actually does instead of having to decipher the meaning of a cryptic pack format string.

While IO can be used to generate a String instance, there's even a better data type for generic byte sequences: Bytes (aka Slice(UInt8)).

numbers = [65, 66, 67]

io = IO::Memory.new
# Equivalent to `numbers.pack('e*')`:
numbers.each do |n|
  io.write_bytes n.to_f32, IO::ByteFormat::LittleEndian
end
bytes = io.to_slice # => Bytes[0, 0, 130, 66, 0, 0, 132, 66, 0, 0, 134, 66]
io.rewind
# Equivalent to `bytes.unpack('e*')
numbers = [] of Float32
loop do
  numbers << io.read_bytes Float32, IO::ByteFormat::LittleEndian
rescue IO::EOFError
  break
end
numbers # => [65.0, 66.0, 67.0]