learnxinyminutes-docs/crystal.md
2024-12-09 04:34:00 -07:00

12 KiB

name filename contributors
Crystal learncrystal.cr
Vitalii Elenhaupt
http://veelenga.com
Arnaud Fernandés
https://github.com/TechMagister/
Valentin Baca
https://github.com/valbaca/
# This is a comment

# Everything is an object
nil.class  #=> Nil
100.class  #=> Int32
true.class #=> Bool

# Falsey values are: nil, false and null pointers
!nil   #=> true  : Bool
!false #=> true  : Bool
!0     #=> false : Bool

# Integers

1.class #=> Int32

# Five signed integer types
1_i8.class   #=> Int8
1_i16.class  #=> Int16
1_i32.class  #=> Int32
1_i64.class  #=> Int64
1_i128.class #=> Int128

# Five unsigned integer types
1_u8.class   #=> UInt8
1_u16.class  #=> UInt16
1_u32.class  #=> UInt32
1_u64.class  #=> UInt64
1_u128.class #=> UInt128

2147483648.class          #=> Int64
9223372036854775808.class #=> UInt64

# Binary numbers
0b1101 #=> 13 : Int32

# Octal numbers
0o123 #=> 83 : Int32

# Hexadecimal numbers
0xFE012D #=> 16646445 : Int32
0xfe012d #=> 16646445 : Int32

# Floats

1.0.class #=> Float64

# There are two floating point types
1.0_f32.class #=> Float32
1_f32.class   #=> Float32

1e10.class    #=> Float64
1.5e10.class  #=> Float64
1.5e-7.class  #=> Float64

# Chars use 'a' pair of single quotes

'a'.class #=> Char

# Chars are 32-bit unicode
'あ' #=> 'あ' : Char

# Unicode codepoint
'\u0041' #=> 'A' : Char

# Strings use a "pair" of double quotes

"s".class #=> String

# Strings are immutable
s = "hello, "  #=> "hello, "        : String
s.object_id    #=> 134667712        : UInt64
s += "Crystal"
s              #=> "hello, Crystal" : String
s.object_id    #=> 142528472        : UInt64

# Supports interpolation
"sum = #{1 + 2}" #=> "sum = 3" : String

# Multiline string
"This is
   multiline string" #=> "This is\n   multiline string"


# String with double quotes
%(hello "world") #=> "hello \"world\""

# Symbols
# Immutable, reusable constants represented internally as Int32 integer value.
# They're often used instead of strings to efficiently convey specific,
# meaningful values

:symbol.class #=> Symbol

sentence = :question?     # :"question?" : Symbol

sentence == :question?    #=> true  : Bool
sentence == :exclamation! #=> false : Bool
sentence == "question?"   #=> false : Bool

# Arrays

[1, 2, 3].class         #=> Array(Int32)
[1, "hello", 'x'].class #=> Array(Char | Int32 | String)

# Empty arrays should specify a type
[]               # Syntax error: for empty arrays use '[] of ElementType'
[] of Int32      #=> [] : Array(Int32)
Array(Int32).new #=> [] : Array(Int32)

# Arrays can be indexed
array = [1, 2, 3, 4, 5] #=> [1, 2, 3, 4, 5] : Array(Int32)
array[0]                #=> 1               : Int32
array[10]               # raises IndexError
array[-6]               # raises IndexError
array[10]?              #=> nil             : (Int32 | Nil)
array[-6]?              #=> nil             : (Int32 | Nil)

# From the end
array[-1] #=> 5

# With a start index and size
array[2, 3] #=> [3, 4, 5]

# Or with range
array[1..3] #=> [2, 3, 4]

# Add to an array
array << 6  #=> [1, 2, 3, 4, 5, 6]

# Remove from the end of the array
array.pop #=> 6
array     #=> [1, 2, 3, 4, 5]

# Remove from the beginning of the array
array.shift #=> 1
array       #=> [2, 3, 4, 5]

# Check if an item exists in an array
array.includes? 3 #=> true

# Special syntax for an array of string and an array of symbols
%w(one two three) #=> ["one", "two", "three"] : Array(String)
%i(one two three) #=> [:one, :two, :three]    : Array(Symbol)

# There is a special array syntax with other types too, as long as
# they define a .new and a #<< method
set = Set{1, 2, 3} #=> Set{1, 2, 3}
set.class          #=> Set(Int32)

# The above is equivalent to
set = Set(typeof(1, 2, 3)).new #=> Set{} : Set(Int32)
set << 1                       #=> Set{1} : Set(Int32)
set << 2                       #=> Set{1, 2} : Set(Int32)
set << 3                       #=> Set{1, 2, 3} : Set(Int32)

# Hashes

{1 => 2, 3 => 4}.class   #=> Hash(Int32, Int32)
{1 => 2, 'a' => 3}.class #=> Hash(Char| Int32, Int32)

# Empty hashes must specify a type
{}                     # Syntax Error: for empty hashes use '{} of KeyType => ValueType'
{} of Int32 => Int32   # {} : Hash(Int32, Int32)
Hash(Int32, Int32).new # {} : Hash(Int32, Int32)

# Hashes can be quickly looked up by key
hash = {"color" => "green", "number" => 5}
hash["color"]        #=> "green"
hash["no_such_key"]  #=> Missing hash key: "no_such_key" (KeyError)
hash["no_such_key"]? #=> nil

# The type of the returned value is based on all key types
hash["number"] #=> 5 : (Int32 | String)

# Check existence of keys hash
hash.has_key? "color" #=> true

# Special notation for symbol and string keys
{key1: 'a', key2: 'b'}     # {:key1 => 'a', :key2 => 'b'}
{"key1": 'a', "key2": 'b'} # {"key1" => 'a', "key2" => 'b'}

# Special hash literal syntax with other types too, as long as
# they define a .new and a #[]= methods
class MyType
  def []=(key, value)
    puts "do stuff"
  end
end

MyType{"foo" => "bar"}

# The above is equivalent to
tmp = MyType.new
tmp["foo"] = "bar"
tmp

# Ranges

1..10                  #=> Range(Int32, Int32)
Range.new(1, 10).class #=> Range(Int32, Int32)

# Can be inclusive or exclusive
(3..5).to_a  #=> [3, 4, 5]
(3...5).to_a #=> [3, 4]

# Check whether range includes the given value or not
(1..8).includes? 2 #=> true

# Tuples are a fixed-size, immutable, stack-allocated sequence of values of
# possibly different types.
{1, "hello", 'x'}.class #=> Tuple(Int32, String, Char)

# Access tuple's value by its index
tuple = {:key1, :key2}
tuple[1] #=> :key2
tuple[2] #=> Error: index out of bounds for Tuple(Symbol, Symbol) (2 not in -2..1)

# Can be expanded into multiple variables
a, b, c = {:a, 'b', "c"}
a #=> :a
b #=> 'b'
c #=> "c"

# Procs represent a function pointer with an optional context (the closure data)
# It is typically created with a proc literal
proc = ->(x : Int32) { x.to_s }
proc.class # Proc(Int32, String)
# Or using the new method
Proc(Int32, String).new { |x| x.to_s }

# Invoke proc with call method
proc.call 10 #=> "10"

# Control statements

if true
  "if statement"
elsif false
  "else-if, optional"
else
  "else, also optional"
end 

puts "if as a suffix" if true

# If as an expression
a = if 2 > 1
      3
    else
      4
    end

a #=> 3

# Ternary if
a = 1 > 2 ? 3 : 4 #=> 4

# Case statement
cmd = "move"

action = case cmd
  when "create"
    "Creating..."
  when "copy"
    "Copying..."
  when "move"
    "Moving..."
  when "delete"
    "Deleting..."
end

action #=> "Moving..."

# Loops
index = 0
while index <= 3
  puts "Index: #{index}"
  index += 1
end
# Index: 0
# Index: 1
# Index: 2
# Index: 3

index = 0
until index > 3
  puts "Index: #{index}"
  index += 1
end
# Index: 0
# Index: 1
# Index: 2
# Index: 3

# But the preferable way is to use each
(1..3).each do |index|
  puts "Index: #{index}"
end
# Index: 1
# Index: 2
# Index: 3

# Variable's type depends on the type of the expression
# in control statements
if a < 3
  a = "hello"
else
  a = true
end
typeof(a) #=> (Bool | String)

if a && b
  # here both a and b are guaranteed not to be Nil
end

if a.is_a? String
  a.class #=> String
end

# Functions

def double(x)
  x * 2
end

# Functions (and all blocks) implicitly return the value of the last statement
double(2) #=> 4

# Parentheses are optional where the call is unambiguous
double 3 #=> 6

double double 3 #=> 12

def sum(x, y)
  x + y
end

# Method arguments are separated by a comma
sum 3, 4 #=> 7

sum sum(3, 4), 5 #=> 12

# yield
# All methods have an implicit, optional block parameter
# it can be called with the 'yield' keyword

def surround
  puts '{'
  yield
  puts '}'
end

surround { puts "hello world" }

# {
# hello world
# }


# You can pass a block to a function
# "&" marks a reference to a passed block
def guests(&block)
  block.call "some_argument"
end

# You can pass a list of arguments, which will be converted into an array
# That's what splat operator ("*") is for
def guests(*array)
  array.each { |guest| puts guest }
end

# If a method returns an array, you can use destructuring assignment
def foods
    ["pancake", "sandwich", "quesadilla"]
end
breakfast, lunch, dinner = foods
breakfast #=> "pancake"
dinner    #=> "quesadilla"

# By convention, all methods that return booleans end with a question mark
5.even? # false
5.odd?  # true

# Also by convention, if a method ends with an exclamation mark, it does 
# something destructive like mutate the receiver.
# Some methods have a ! version to make a change, and
# a non-! version to just return a new changed version
fruits = ["grapes", "apples", "bananas"]
fruits.sort  #=> ["apples", "bananas", "grapes"]
fruits       #=> ["grapes", "apples", "bananas"]
fruits.sort! #=> ["apples", "bananas", "grapes"]
fruits       #=> ["apples", "bananas", "grapes"]

# However, some mutating methods do not end in !
fruits.shift #=> "apples"
fruits       #=> ["bananas", "grapes"]

# Define a class with the class keyword
class Human

  # A class variable. It is shared by all instances of this class.
  @@species = "H. sapiens"

  # An instance variable. Type of name is String
  @name : String

  # Basic initializer
  # Assign the argument to the "name" instance variable for the instance
  # If no age given, we will fall back to the default in the arguments list.
  def initialize(@name, @age = 0)
  end

  # Basic setter method
  def name=(name)
    @name = name
  end

  # Basic getter method
  def name
    @name
  end

  # The above functionality can be encapsulated using the propery method as follows
  property :name

  # Getter/setter methods can also be created individually like this
  getter :name
  setter :name

  # A class method uses self to distinguish from instance methods.
  # It can only be called on the class, not an instance.
  def self.say(msg)
    puts msg
  end

  def species
    @@species
  end
end


# Instantiate a class
jim = Human.new("Jim Halpert")

dwight = Human.new("Dwight K. Schrute")

# Let's call a couple of methods
jim.species #=> "H. sapiens"
jim.name #=> "Jim Halpert"
jim.name = "Jim Halpert II" #=> "Jim Halpert II"
jim.name #=> "Jim Halpert II"
dwight.species #=> "H. sapiens"
dwight.name #=> "Dwight K. Schrute"

# Call the class method
Human.say("Hi") #=> print Hi and returns nil

# Variables that start with @ have instance scope
class TestClass
  @var = "I'm an instance var"
end

# Variables that start with @@ have class scope
class TestClass
  @@var = "I'm a class var"
end
# Variables that start with a capital letter are constants
Var = "I'm a constant"
Var = "can't be updated" # Error: already initialized constant Var

# Class is also an object in Crystal. So a class can have instance variables.
# Class variable is shared among the class and all of its descendants.

# base class
class Human
  @@foo = 0

  def self.foo
    @@foo
  end

  def self.foo=(value)
    @@foo = value
  end
end

# derived class
class Worker < Human
end

Human.foo   #=> 0
Worker.foo  #=> 0

Human.foo = 2 #=> 2
Worker.foo    #=> 0

Worker.foo = 3 #=> 3
Human.foo   #=> 2
Worker.foo  #=> 3

module ModuleExample
  def foo
    "foo"
  end
end

# Including modules binds their methods to the class instances
# Extending modules binds their methods to the class itself

class Person
  include ModuleExample
end

class Book
  extend ModuleExample
end

Person.foo     # => undefined method 'foo' for Person:Class
Person.new.foo # => 'foo'
Book.foo       # => 'foo'
Book.new.foo   # => undefined method 'foo' for Book


# Exception handling

# Define new exception
class MyException < Exception
end

# Define another exception
class MyAnotherException < Exception; end

ex = begin
   raise MyException.new
rescue ex1 : IndexError
  "ex1"
rescue ex2 : MyException | MyAnotherException
  "ex2"
rescue ex3 : Exception
  "ex3"
rescue ex4 # catch any kind of exception
  "ex4"
end

ex #=> "ex2"

Additional resources