Search code examples
rubystringcrystal-lang

Why is the .to_s method needed in Crystal when converting user input to an integer?


I'm getting started with Crystal, and I've run into something I don't understand. I've written a simple program to demonstrate, which takes a number from the console and adds one.

Ruby

# Add one program.
puts "Enter a number."

number = gets

number = number.to_i

puts "You entered #{number}. #{number} + 1 = #{number + 1}"

Crystal

# Add one program.
puts "Enter a number."

number = gets

number = number.to_s.to_i # Why is to_s needed?

puts "You entered #{number}. #{number} + 1 = #{number + 1}"

As you can see, the programs are nearly identical, however, in crystal I must take the input from the console and convert it to a string before it can be converted into an integer.

What I want to know is:

  • What is being returned by gets in crystal?
  • Is there another way to do this without chaining methods?

This may seem like a basic question, but it's still early days for crystal, and documentation is sparse.

Error

Error in example.cr:6: undefined method 'to_i' for Nil (compile-time type is (String | Nil)) (did you mean 'to_s'?)

number = number.to_i # Why is to_s needed?
                ^~~~

================================================================================

Nil trace:

  example.cr:4

    number = gets
    ^~~~~~

  example.cr:4

    number = gets
             ^~~~

  /usr/share/crystal/src/kernel.cr:105

    def gets(*args, **options)


  /usr/share/crystal/src/kernel.cr:105

    def gets(*args, **options)
              ^

  /usr/share/crystal/src/kernel.cr:106

      STDIN.gets(*args, **options)
            ^~~~

  /usr/share/crystal/src/io.cr:574

      def gets(chomp = true) : String?


  /usr/share/crystal/src/io.cr:574

      def gets(chomp = true) : String?
               ^

  /usr/share/crystal/src/io.cr:574

      def gets(chomp = true) : String?


  /usr/share/crystal/src/io.cr:574

      def gets(chomp = true) : String?
          ^~~~

  /usr/share/crystal/src/io.cr:575

        gets '\n', chomp: chomp
        ^~~~

  /usr/share/crystal/src/io.cr:604

      def gets(delimiter : Char, chomp = false) : String?
          ^~~~

  /usr/share/crystal/src/io.cr:605

        gets delimiter, Int32::MAX, chomp: chomp
        ^~~~

  /usr/share/crystal/src/io.cr:618

      def gets(delimiter : Char, limit : Int, chomp = false) : String?
          ^~~~

  /usr/share/crystal/src/io.cr:619

        raise ArgumentError.new "Negative limit" if limit < 0
        ^

  /usr/share/crystal/src/io.cr:632

        if ascii && !decoder && (peek = self.peek)
        ^

  /usr/share/crystal/src/io.cr:633

          if peek.empty?
          ^

  /usr/share/crystal/src/io.cr:634

            nil
            ^

Solution

  • In most cases gets will return a String, but it is possible that it returns nil too.

    This isn't an issue in Ruby, because in your example you will ever have nil returned at runtime and even if you had, there is NilClass#to_i in Ruby which always returns 0.

    But the Crystal compiler checks object types upfront and therefore makes sure that your code can handle all possible return types. Unfortunately, in Crystal, there is no to_i method on Nil yet and therefore you get the compiler error:

    undefined method 'to_i' for Nil (compile-time type is (String | Nil))