Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Crystal

Introduction

Crystal Language

  • Crystal lang
  • Syntax similar to Ruby
  • LLVM under the hood
  • Ideas from Go, Erlang, Rust, Swift
  • Statically type checked
  • Built-in type inference
  • Types are non-nilable (compile time check for lack of assignment)
  • Meta-programming with Macro
  • Concurrency with green threads called fibers
  • C-bindings

Crystal Shards (3rd party libraries)

  • shards

  • The 3rd party modules directory

  • Shardbox

  • Crystal Shards

  • Use shards init to create a file called shard.yml and describe the dependencies there.

How to install 3rd party shards?

  • Create a file called shard.yml for your project listing the dependencies of this project
  • Run shards install
name: example
version: 0.1.0

# authors:
#   - name <email@example.com>

description: |
  Short description of the project

dependencies:
  ameba:
    github: crystal-ameba/ameba
  sqlite3:
    github: crystal-lang/crystal-sqlite3


#development_dependencies:
#  webmock:
#    github: manastech/webmock.cr

license: MIT

Install Crystal

Crystal in Docker on Linux

  • Docker Crystal
  • Install Docker
  • Create a file called crystal with the following content:
docker run --rm -it -w/opt -v$(pwd):/opt crystallang/crystal crystal $*
  • Make it executable by running chmod +x crystal
  • Run ./crystal examples/intro/hello_world.cr

Alternative:

Start docker container:

docker run --rm -it -w/opt -v$(pwd):/opt --name crystal -d crystallang/crystal tail -f /opt/Dockerfile

Execute in the running container:

docker exec crystal crystal examples/intro/hello_world.cr

Later you can stop the container:

docker -t0 stop crystal

Hello World (puts)

  • puts
puts "Hello World!"
  • puts stands for "put string"
  • Adds a newline at the end

Run Crystal source code

crystal hello_world.cr

Compile Crystal to executable

crystal build hello_world.cr -o hello
./hello

Speed of Crystal

$ time crystal hello_world.cr
Hello World!

real  0m1.108s
user  0m1.268s
sys   0m0.271s
$ time crystal build hello_world.cr -o hw

real   0m1.108s
user   0m1.323s
sys    0m0.238s
$ time ./hw
Hello World!

real   0m0.013s
user   0m0.001s
sys    0m0.011s

Hello World (print)

  • print

  • print does not add a newline

  • You can add one by including \n

print "Hello World!\n"

Hello Name (variables)

  • print will include an empty string between its parameters
  • puts will include a newline between its parameters
name = "Foo Bar"
print "Hello ", name, "!\n"
puts "Hello ", name, "!"
Hello Foo Bar!
Hello 
Foo Bar
!

Hello Name with interpolation

  • Interpolation is the embedding of variables in strings using #{}

name = "Foo Bar"
puts "Hello #{name}!"
Hello Foo Bar!

Interpolation

  • Interpolation using #{} for strings, integers, floating point numbers

  • and even expressions.

name = "Foo Bar"
pi = 3.14
radius = 3
yesno = true
puts "Hello #{name}"
puts "PI is #{pi}"
puts "The area of a circle with a radius of #{radius} is #{pi * radius**2}!"
puts "Boolean #{yesno}"
Hello Foo Bar
PI is 3.14
The area of a circle with a radius of 3 is 28.26!
Boolean true

Escaping - Alternative quote as delimiters

  • %
name = "Foo Bar"
puts "Hello '#{name}'!"
puts "Hello \"#{name}\"!"

puts %(Hello "#{name}"!) # alternative quotes
puts %{Hello "#{name}"!}
Hello 'Foo Bar'!
Hello "Foo Bar"!
Hello "Foo Bar"!
Hello "Foo Bar"!

Debugging print p!

  • p!

  • p! can be useful, especially for debugging prints as it includes the name of the variable.

  • Here we see a simple string printed and then a slightly more complex data-structure.

name = "Foo"
person = {
  name:    "Foo",
  number:  42,
  yesno:   true,
  fruits:  ["apple", "banana", "peach"],
  address: {
    "street"  => "Main str.",
    "city"    => "Capital",
    "country" => "Country",
  },
}
puts name
p name
p! name
pp! name

puts ""

puts person
p person
p! person
pp! person
Foo
"Foo"
name # => "Foo"
name # => "Foo"

{name: "Foo", number: 42, yesno: true, fruits: ["apple", "banana", "peach"], address: {"street" => "Main str.", "city" => "Capital", "country" => "Country"}}
{name: "Foo", number: 42, yesno: true, fruits: ["apple", "banana", "peach"], address: {"street" => "Main str.", "city" => "Capital", "country" => "Country"}}
person # => {name: "Foo", number: 42, yesno: true, fruits: ["apple", "banana", "peach"], address: {"street" => "Main str.", "city" => "Capital", "country" => "Country"}}
person # => {name: "Foo",
 number: 42,
 yesno: true,
 fruits: ["apple", "banana", "peach"],
 address: 
  {"street" => "Main str.", "city" => "Capital", "country" => "Country"}}

Comments

# Some commented out code:

# puts "hello"

puts "crystal" # another comment

Code formatting

  • 2-space indentation is the norm.

  • Use the Crystal tool to format your code:

crystal tool format
  • You can also use it in a CI system to verify code-formatting
  • (You can also make the CI format it for you and commit the changes back to the repository)
crystal tool format --check

Types - typeof

  • typeof

  • String

  • Int32

  • Float64

  • Bool

  • p!

  • Literals

name = "Foo Bar"
p! typeof(name) # typeof(name) => String

age = 42
p! typeof(age) # typeof(age) => Int32

pi = 3.14
p! typeof(pi) # typeof(pi) => Float64

yesno = true
p! typeof(yesno) # typeof(yesno) # => Bool

Compound Types - typeof

  • typeof
  • Array
  • Tuple
  • Hash
  • NamedTuple
x = ["Foo", 42]
p! typeof(x) # typeof(x) # => Array(Int32 | String)

a = {"Foo", 42}
p! typeof(a) # typeof(a) # => Tuple(String, Int32)

y = {
  "name" => "Foo Bar",
  "id"   => 42,
}
p! typeof(y) # typeof(y) # => Hash(String, Int32 | String)

z = {
  "name": "Foo Bar",
  "id":   42,
}
p! typeof(z) # typeof(z) # => NamedTuple(name: String, id: Int32)

Add numbers - concatenate strings

  • Interpolation works on numbers as well

  • The + operator is numerical addition or string concatenation

x = 23
y = 19
z = x + y
puts z                     # 42
puts "#{x} + #{y} is #{z}" # 23 + 19 is 42

a = "23"
b = "19"
c = a + b
puts c                     # 2319
puts "#{a} + #{b} is #{c}" # 23 + 19 is 2319

Add mixed strings and Integers

Error: no overload matches 'Int32#+' with type String

x = 23
y = "19"
z = x + y
puts z

Numeric Operators

a = 7
b = 2

puts a + b  # 9
puts a - b  # 5
puts a * b  # 14
puts a ** b # 49  (exponent)
puts a / b  # 3.5
puts a // b # 3   (floor division)
puts a % b  # 1   (modulus)

Methods of Int32

  • abs

  • round

  • even

  • gcd

  • Int32

answer = -42
p! answer.abs   # 42
p! answer.even? # true
p! answer.round # -42

puts 42.gcd(35) # 7  ( Greatest common divisor )

Methods of Float64

x = -5.2
p! x.abs   # 5.2
p! x.round # -5.0
# p! x.even? # Undefined method even? for Float64

Program name

  • PROGRAM_NAME
puts PROGRAM_NAME
  • When running with crystal examples/intro/program_name.cr:
/root/.cache/crystal/crystal-run-program_name.tmp

We can also compile it

crystal build examples/intro/program_name.cr
  • This will generate program_name (as that was the name of our source file)

  • We can rename it: mv program_name other

  • We can run it ./other and it will print the name of the executable file other.

  • See other top-level variables

Command line arguments - ARGV

  • ARGV

  • ARGV is an Array of Strings. Array(String)

puts ARGV
puts ARGV.size

ARGV.each_with_index { |arg, ix|
  puts "Argument #{ix} was #{arg}"
}

if ARGV.size > 0
  puts ARGV[0]
end

Early exit

  • exit
puts "before"
answer = 42
if answer == 42
  exit 3
end
puts "after"
  • Exit code defaults to 0
echo $?
echo %ERROR_LEVEL%

Rectangle

if ARGV.size != 2
  puts "Usage: #{PROGRAM_NAME} A B\nThe program needs two numbers, the two sides of the rectangle."
  exit 1
end

width, height = ARGV
puts width
puts height

area = width.to_f * height.to_f
circumference = 2 * (width.to_f + height.to_f)
puts "area: #{area}"
puts "circumference: #{circumference}"

True values

  • false, nil, and the null pointer are "falsy" everything else, including 0 and "" are true
values = [0, "0", "", 1, true, false, nil]
values.each { |val|
  if val
    puts "#{val} is true"
  else
    puts "#{val} is NOT true"
  end
}
0 is true
0 is true
 is true
1 is true
true is true
false is NOT true
 is NOT true

Math - PI

  • PI
  • Math
p! Math::PI
p! Math::TAU
p! Math::E
Math::PI # => 3.141592653589793
Math::TAU # => 6.283185307179586
Math::E # => 2.718281828459045

Read from STDIN

  • gets
print "What is your name? "
name = gets
puts "Hello, #{name}, how are you?"

Read number from STDIN

print "Give me a number! "
number = gets
puts number
puts number + 1

Interactive environments

Crystal one-liners

  • eval

  • crystal eval

Crystal and random numbers

puts Random.rand    # floating point
puts Random.rand(6) # Int
0.7619060657241036
1

Exercise: Hello World

  • Install Crystal.
  • Verify that you can run it on the command line and print the version number crystal --version.
  • Create a file called hello_world.cr that will print out "Hello World"

Exercise: Hello Name - STDIN

  • Create a file called hello_name_stdin.cr that will ask the user for their name.
  • Wait till the user types in their name.
  • print "Hello NAME" with their name.

Exercise: Hello Name - ARGV

  • Create a file called hello_name_argv.cr that expects a name on the command line:
  • crystal hello_name_argv.cr FooBar
  • print "Hello FooBar" using whatever name the user provided.

Exercise: Circle STDIN

  • Create a file called circle_stdin.cr that will ask the user for a number.
  • Then wait till the user types in a number (the radius of a circle).
  • Print the area and the circumference of the circle.

Exercise: Circle ARGV

  • Create a file called circle_argv.cr that will expect a number on the command line, the radius of a circle.
  • Print the area and the circumference of the circle.

Exercise: Calculator STDIN

  • Create a file called calculator_stdin.cr
  • Ask the user for two numbers and an operator (+, -, *, /)
  • Compute the result of the operation and print it out.

Exercise: Calculator ARGV

  • Create a file called calculator_argv.cr that the user can run with two numbers and an operator (+, -, *, /).
  • crystal calculator_argv.cr 3 + 7
  • Compute the result of the operation and print it out.

Exercise: Age limit

  • Create a script called age_limit_stdin.cr

  • Ask the user what is their age.

  • If it is above 18, tell them they can legally drink alcohol.

  • If is is above 21, tell them they can also legally drink in the USA.

  • Extra: ask the user for their age and the name of their country and tell them if they can legally drink alcohol.

  • See the Legal drinking age list.

  • Don't worry if this seems to be too difficult to solve in a nice way. We'll learn more tools to improve.

Strings

Strings intro

text = "The black cat climbed the green tree"
puts text
p! text

Length or size of a string

  • size
  • length
text = "The black cat climbed the green tree"
puts text.size

Locate substring (index, rindex)

  • index
  • rindex
text = "The black cat climbed the green tree"

puts text.index("cat")
puts text.index("dog")

puts text.index("c")
puts text.rindex("c")

Reverse a string

  • reverse

  • The original string stays intact

text = "The black cat climbed the green tree"
rev = text.reverse

puts rev
puts text
puts rev.reverse

Substring, range of characters

text = "The black cat climbed the green tree"

puts text[0]
puts text[4]
puts text[0, 4] # 4 characters (start, count)
puts text[0..4] # 5 characters (Range)
# count cannot be negative, but the Range ending can and it means, from the end of the string
puts text[0..-4]
T
b
The 
The b
The black cat climbed the green t

String includes another string

  • includes?
text = "The black cat climbed the green tree"

puts text.includes?("cat")
puts text.includes?("dog")

String starts with

  • starts_with
text = "The black cat climbed the green tree"
puts text.starts_with?("The")
puts text.starts_with?("the")

String ends with

  • ends_with
text = "The black cat climbed the green tree"
puts text.ends_with?("tree")

Replace part of a string (substitute)

  • sub

  • If there is nothing to replace, nothing happens

  • Only replaces the first occurrence of the string

text = "The black cat climbed the green tree"

new_text = text.sub("cat", "dog")
puts text
puts new_text

new_text = text.sub("dog", "elephant")
puts text
puts new_text

text = "Red cat, Blue cat"
new_text = text.sub("cat", "dog")
puts text
puts new_text

animal = "cat"
new_text = text.sub animal do |original|
  original.upcase
end
puts new_text
The black cat climbed the green tree
The black dog climbed the green tree
The black cat climbed the green tree
The black cat climbed the green tree
Red cat, Blue cat
Red dog, Blue cat
Red CAT, Blue cat

Is the string empty or blank?

  • size
  • empty?
  • blank?
empty = ""
puts empty.size
puts empty.empty?
puts empty.blank?

whitespaces = "  \t\n\n\t"
puts whitespaces.size
puts whitespaces.empty?
puts whitespaces.blank?

Iterate over characters of a string

  • each_char
text = "Crystal" # STring
text.each_char { |chr|
  puts chr      # Char
  puts chr.to_s # String
}

Type conversion from string to float, to int

  • to_f
  • to_i
value = "42.3"
p! value

puts value.to_f
p! value.to_f

value = "42"
puts value.to_i
p! value.to_i

Converting string to integer or float

  • to_i
  • to_i?
  • to_f
  • to_f?
values = ["42", "42.1", "abc", "0"]
values.each { |val|
  puts val
  puts val.to_i?
  if val.to_i?
    puts "Convertable to Int32"
  end

  puts val.to_f?
  if val.to_f?
    puts "Convertable to Floar64"
  end
  puts "---"
}
42
42
Convertable to Int32
42.0
Convertable to Floar64
---
42.1

42.1
Convertable to Floar64
---
abc


---
0
0
Convertable to Int32
0.0
Convertable to Floar64
---

Split String

  • split
text = "This is a  string"
puts text
pieces = text.split(" ")
puts pieces # ["This", "is", "a", "", "string"]

pieces = text.split(/ +/)
puts pieces # ["This", "is", "a", "string"]

text = "name:secret"
name, password = text.split(":")
puts name
puts password

String Transliteration

  • tr
puts "abcdeabcde".tr("ab", "xy")
xycdexycde

String Builder

str = String.build do |temp|
  temp << "hello "
  temp << 1
end
puts str # => "hello 1"

sprintf and %

name = "Foo"
number = 42

text = "The name is %s" % name
puts text

text = "The name is %s the number is %s" % {name, number} # Tuple
puts text

text = "The name is %s the number is %s" % [name, number] # Array
puts text

text = "The name is %{txt} the number is %{num}" % {txt: name, num: number} # NamedTuple
puts text

text = sprintf "The name is %s the number is %s", name, number
puts text
The name is Foo
The name is Foo the number is 42
The name is Foo the number is 42
The name is Foo the number is 42
The name is Foo the number is 42

Split to the same string length

text = "1234567890123"
width = 3
# (0..text.size).step(width) {|start|
#    puts text[start, width]
# }

res = ((0..text.size).step(width).map { |start| text[start, width] }).join " "
puts res

Split characters

  • split
  • chars
text = "black cat"
puts text.split("")
puts text.split("").sort

puts text.chars
["b", "l", "a", "c", "k", " ", "c", "a", "t"]
[" ", "a", "a", "b", "c", "c", "k", "l", "t"]
['b', 'l', 'a', 'c', 'k', ' ', 'c', 'a', 't']

printf

  • printf
# Padding and Alignment
number = 42
printf "'%-4s'\n", number # Padding, left align
printf "'%4s'\n", number  # Padding, right align
printf "'%04s'\n", number # Padding with 0s

# Rounding floating point
puts Math::PI
printf "'%d'\n", Math::PI
printf "'%f'\n", Math::PI
printf "'%.3f'\n", Math::PI
'42  '
'  42'
'0042'
3.141592653589793
'3'
'3.141593'
'3.142'

Here documents

  • <<-
name = "Crystal"

text = <<-ANYTHING
Hello World

Hello #{name}
ANYTHING

puts text

Conditionals

Comparison Operators

  • ==

  • <

  • <=

  • =

  • ==, <, > <=, >=

Spaceship operator

  • <=>

  • Spaceship operator <=> returns -1, 0, or 1

puts 3 <=> 3 # 0
puts 3 <=> 4 # -1
puts 4 <=> 3 # 1

puts "a" <=> "a" # 0
puts "a" <=> "b" # -1
puts "b" <=> "a" # 1

if statement

  • if
  • else
  • end
age = 22

if age < 21
  puts "Sorry you cannot dring alcohol in the US."
else
  puts "Can drink alcohol in the US as well."
end

elsif

  • elsif
age = 22

if age < 21
  puts "Sorry you cannot dring alcohol in the US."
else
  if 21 < age
    puts "Can drink alcohol in the US as well."
  else
    puts "Congratulations on your birthday"
  end
end
age = 21

if age < 21
  puts "Sorry you cannot dring alcohol in the US."
elsif 21 < age
  puts "Can drink alcohol in the US as well."
else
  puts "Congratulations on your birthday"
end

unless statement

  • unless
age = 22

unless age >= 21
  puts "Sorry you cannot dring alcohol in the US."
end
  • ameba: [C] Style/UnlessElse: Favour if over unless with else

Suffix if

  • if
x = 23

puts "OK" if x > 20
puts "OK" if x != 20
begin
  x = x + 1; puts x
end if x < 30

Suffix unless

  • unless
x = 23
puts "not 17" unless x == 17
puts "not 17" if !(x == 17)
puts "not 17" if x != 17

Logical operators

&&
||
!

Truth-table

puts true && true   # true
puts true && false  # false
puts false && true  # false
puts false && false # false

puts true || true   # true
puts true || false  # true
puts false || true  # true
puts false || false # false

puts !true  # false
puts !false # true

case / when

  • case
  • switch
  • when
if ARGV.size != 1
  puts "Usage: case.cr DIRECTION"
  exit -1
end
direction = ARGV[0]

case direction
when "forward"
  puts "go on"
when "bacward"
  puts "go back"
when "left", "right"
  puts "go #{direction}"
else
  puts "We don't know how to go '#{direction}'"
  exit 0
end
  • You cannot have the same value in when twice (Crystal protects you from such mistake)

case of types

if Random.rand < 0.5
  x = 23
else
  x = "hello"
end

puts x

# puts x.abs # Error: undefined method 'abs' for String (compile-time type is (Int32 | String))
# puts x.size # Error: undefined method 'size' for Int32 (compile-time type is (Int32 | String))

case x
when String
  puts "string"
  puts x.size
when Int32
  puts "int32"
  puts x.abs
end

Ternary operator and or to set default value

  • ||
  • ?:
name = nil

ternary_name = name ? name : "default"
puts ternary_name

or_name = name || "default"
puts or_name

Exercise: Number Guessing game - level 0

Level 0

  • Create a file called number_guessing_game_0.cr
  • Using the random module the computer "thinks" about a whole number between 1 and 20.
  • The user has to guess the number. After the user types in the guess the computer tells if this was bigger or smaller than the number it generated, or if it was the same.
  • The game ends after just one guess.

Level 1-

  • Other levels in the next chapter.

Solution: Number Guessing game - level 0

LIMIT = 20

hidden = Random.rand(LIMIT) + 1
puts "For debugging: #{hidden}"
print "Guess a number between 1 and #{LIMIT}: "
guess = gets.not_nil!.to_i

if guess == hidden
  puts "Matched!"
elsif guess < hidden
  puts "Too small"
else
  puts "Too big"
end

Loops

loop

  • loop
  • break
cnt = 0
loop do
  cnt += 1
  puts cnt
  break if cnt >= 10
end

loop controls (next, break)

  • next

  • continue

  • next (continue)

  • break (last)

while

  • while
cnt = 0
while cnt < 10
  cnt += 1
  puts cnt
end

until

  • until
cnt = 0
until cnt >= 10
  cnt += 1
  puts cnt
end

Exercise: Number guessing game

Level 0

  • Create a file called number_guessing_game_0.cr
  • Using the random module the computer "thinks" about a whole number between 1 and 20.
  • The user has to guess the number. After the user types in the guess the computer tells if this was bigger or smaller than the number it generated, or if was the same.
  • The game ends after just one guess.

Level 1

  • Create a file called number_guessing_game_1.cr
  • The user can guess several times. The game ends when the user guessed the right number.

Level 2

  • Create a file called number_guessing_game_2.cr
  • If the user hits 'x', we leave the game without guessing the number.

Level 3

  • Create a file called ``number_guessing_game_3.cr`
  • If the user presses 's', show the hidden value (cheat)

Level 4

  • Create a file called number_guessing_game_4.cr
  • Soon we'll have a level in which the hidden value changes after each guess. In order to make that mode easier to track and debug, first we would like to have a "debug mode".
  • If the user presses 'd' the game gets into "debug mode": the system starts to show the current number to guess every time, just before asking the user for new input.
  • Pressing 'd' again turns off debug mode. (It is a toggle each press on "d" changes the value to to the other possible value.)

Level 5

  • Create a file called number_guessing_game_5.cr
  • The 'm' button is another toggle. It is called 'move mode'. When it is 'on', the hidden number changes a little bit after every step (+/-2). Pressing 'm' again will turn this feature off.

Level 6

  • Create a file called number_guessing_game_6.cr
  • Let the user play several games.
  • Pressing 'n' will skip this game and start a new one. Generates a new number to guess.

Solution: Number guessing game

LIMIT = 20

hidden = Random.rand(LIMIT) + 1
puts "For debugging: #{hidden}"
loop do
  print "Guess a number between 1 and #{LIMIT}: "
  guess = gets.not_nil!.to_i

  if guess == hidden
    puts "Matched!"
    break
  elsif guess < hidden
    puts "Too small"
  else
    puts "Too big"
  end
end
LIMIT = 20

hidden = Random.rand(LIMIT) + 1
puts "For debugging: #{hidden}"
loop do
  print "Guess a number between 1 and #{LIMIT}: "
  guess_str = gets.not_nil!
  if guess_str == "x"
    puts "Good bye"
    break
  end

  guess = guess_str.to_i

  if guess == hidden
    puts "Matched!"
    break
  elsif guess < hidden
    puts "Too small"
  else
    puts "Too big"
  end
end

Arrays

Arrays intro

  • [Array API](https://crystal-lang.org/api/Array.html" %}
  • [Array reference](https://crystal-lang.org/reference/syntax_and_semantics/literals/array.html" %}
planets = ["Mars", "Jupyter", "Saturn", "Earth"]
p! planets           # ["Mars", "Jupyter", "Saturn", "Earth"]
puts typeof(planets) # Array(String)
puts planets.size    # 4

integers = [3, 8, -2]
puts typeof(integers) # Array(Int32)

floats = [3.14, 2.1]
puts typeof(floats) # Array(Float64)

mixed = [1, 3.14, "PI"]
puts typeof(mixed) # Array(Float64 | Int32 | String)

Array elements - indexing

planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
puts planets[0]  # Mercury
puts planets[1]  # Venus
puts planets[-1] # Jupiter

puts planets[0, 3]  # ["Mercury", "Venus", "Earth"]
puts planets[0..3]  # ["Mercury", "Venus", "Earth", "Mars"]
puts planets[0...3] # ["Mercury", "Venus", "Earth"]

Array iterate over (each, each_with_index)

  • each
  • each_with_index
planets = ["Mars", "Jupyter", "Saturn", "Earth"]

planets.each { |planet| puts planet }

# enumerate
planets.each_with_index { |planet, idx|
  puts "#{idx}: #{planet}"
}

Array push, append, <<

  • push
  • append
  • <<
planets = ["Mars", "Jupyter", "Saturn"]

# append / push / <<
planets << "Pluto"
puts planets # ["Mars", "Jupyter", "Saturn", "Pluto"]

planets.push("Venus")
puts planets # ["Mars", "Jupyter", "Saturn", "Pluto", "Venus"]

Empty array

  • Empty array must come with a type definition
# empty = []       # Syntax error

empty = [] of Int32
puts empty.size   # 0
puts empty.empty? # true

empty << 23
puts empty.size   # 1
puts empty.empty? # false

# empty << 3.14  # Error: no overload matches 'Array(Int32)#<<' with type Float64

other = Array(Int32).new
puts typeof(other) # Array(Int32)
puts other.size    # 0

Count digits

text = "123456789112777"
count = [0] * 10

text.each_char { |chr|
  count[chr.to_i] += 1
}

count.each_with_index { |value, idx|
  puts "#{idx} #{value}"
}

Operations on arrays

  • add +
  • repeat *

Add arrays

x = [2, 3, 4]
y = [5, 6, 7]
p! x
p! y
z = x + y
p! z
x # => [2, 3, 4]
y # => [5, 6, 7]
z # => [2, 3, 4, 5, 6, 7]

Repeat arrays

x = [1, 2] * 3
y = [0] * 10
p! x
p! y
x # => [1, 2, 1, 2, 1, 2]
y # => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Select (filter, grep)

  • select
  • select!
  • filter
  • grep
numbers = [10, 20, 7, 21, 5]
puts numbers
small = numbers.select do |num|
  num > 10
end
puts small
puts numbers.select { |num| num > 10 }
puts numbers
puts ""

big = numbers.select! do |num|
  num < 10
end
puts big
puts numbers
[10, 20, 7, 21, 5]
[20, 21]
[20, 21]
[10, 20, 7, 21, 5]

[7, 5]
[7, 5]

Reject (negative filter, grep)

  • reject

  • reject!

  • filter

  • grep

  • the reject! with the exclamation mark will modify the array

  • the reject without the exclamation mark will only return the filtered array

numbers = [10, 20, 7, 21, 5]
puts numbers
small = numbers.reject do |num|
  num > 10
end
puts small
puts numbers.reject { |num| num > 10 }
puts numbers
puts ""

big = numbers.reject! do |num|
  num < 10
end
puts big
puts numbers
[10, 20, 7, 21, 5]
[10, 7, 5]
[10, 7, 5]
[10, 20, 7, 21, 5]

[10, 20, 21]
[10, 20, 21]

Transform with map

  • map
  • map!
numbers = [2, -3, 4, -5]
puts numbers
puts ""

doubles = numbers.map do |num|
  num*2
end
puts doubles
puts numbers
puts ""

triples = numbers.map { |num| num*3 }
puts triples
puts numbers
puts ""

abs = numbers.map &.abs
puts abs
puts numbers
puts ""

abs = numbers.map! &.abs
puts abs
puts numbers
[2, -3, 4, -5]

[4, -6, 8, -10]
[2, -3, 4, -5]

[6, -9, 12, -15]
[2, -3, 4, -5]

[2, 3, 4, 5]
[2, -3, 4, -5]

[2, 3, 4, 5]
[2, 3, 4, 5]

Sample from array

  • sample

  • Using Random behind the scenes.

planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
puts planets
puts planets.sample
puts planets.sample
puts planets.sample
puts planets.sample
["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
Jupiter
Mars
Venus
Jupiter

Shuffle array

  • shuffle

  • Using Random behind the scenes.

planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
puts planets
puts planets.shuffle
puts planets
["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
["Venus", "Earth", "Mercury", "Mars", "Jupiter"]
["Mercury", "Venus", "Earth", "Mars", "Jupiter"]

join

names = ["Foo", "Bar", "Baz"]
puts names.join "-" # Foo-Bar-Baz

Remove nil elements

  • compact
numbers = [nil, 2, 3, nil, nil, 7, nil]
puts numbers
puts numbers.compact
puts numbers

puts numbers.compact!
puts numbers

Create Array with nil

  • 0_i64
n = 5
nums = (0_i64.as(Int64 | Nil)..n).to_a
puts typeof(nums) # Array(Int64 | Nil)

other = [nil] + (1..n).to_a
puts typeof(other) # Array(Int32 | Nil)

Does array include a value

  • includes?
planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
puts planets.includes?("Venus")
puts planets.includes?("Saturn")

Delete element from array by value

  • delete
planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
puts planets.delete("Earth")
puts planets

res = planets.delete("Pluto")
puts res.nil?
puts planets

names = ["Foo", "Bar", "Foo"]
names.delete("Foo")
puts names
Earth
["Mercury", "Venus", "Mars", "Jupiter"]
true
["Mercury", "Venus", "Mars", "Jupiter"]
["Bar"]

Delete element from array by location

  • delete_at
planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
puts planets.delete_at(1)
puts planets

puts planets.delete_at(1, 3)
puts planets
Venus
["Mercury", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
["Earth", "Mars", "Jupiter"]
["Mercury", "Saturn", "Uranus", "Neptune"]

Insert element into array at location

  • insert
numbers = ["One", "Two", "Three"]
puts numbers.insert(1, "Four")
puts numbers

# puts numbers.insert(1, ["1", "2"])
["One", "Four", "Two", "Three"]
["One", "Four", "Two", "Three"]

Arrays shift - remove first element

  • shift
planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
puts planets.shift
puts planets
Mercury
["Venus", "Earth", "Mars", "Jupiter"]

Arrays uniq elements

  • uniq
  • uniq!
names = ["Foo", "Bar", "Foo"]
puts names.uniq
puts names
puts names.uniq!
puts names
["Foo", "Bar"]
["Foo", "Bar", "Foo"]
["Foo", "Bar"]
["Foo", "Bar"]

First element of the array

  • first
  • first?
names = ["Foo", "Bar"]
puts names.first # Foo
puts names[0]    # Foo

names = [] of String
# puts names.first
# Unhandled exception: Empty enumerable (Enumerable::EmptyError)

# puts names[0]
# Unhandled exception: Index out of bounds (IndexError)

first = names.first?
puts first.nil? # true
first = names[0]?
puts first.nil? # true

Permutations

  • permutations
numbers = [1, 2, 3]
numbers.permutations.each { |num|
  puts num
}
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

Hash

Hash intro

  • has_key

  • has_value

  • each_key

  • size

  • keys

  • Hash

planets = {
  "Mars"    => 1,
  "Jupyter" => 2,
  "Saturn"  => 3,
  "Earth"   => 4,
}
puts planets
puts planets.size
puts planets.keys

planets["Pluto"] = 7

puts planets.each_key { |name|
  puts "#{name}: #{planets[name]}"
}

puts planets.has_key?("Pluto") # true
puts planets.has_key?("Moon")  # false

puts planets.has_value?(3) # true
puts planets.has_value?(8) # false

Count words

  • each
words = ["cat", "dog", "snake", "cat", "bug", "ant", "cat", "dog"]
count = {} of String => Int32

words.each { |word|
  if !count.has_key?(word)
    count[word] = 0
  end
  count[word] += 1
}

count.each { |word, cnt|
  puts "#{word} #{cnt}"
}

Create empty hash

  • empty?
phone_of = {} of String => String
puts phone_of.empty?

phone_of["Jane"] = "123"
phone_of["Jack"] = "456"

# phone_of["Narnia"] = 42
# Error: no overload matches 'Hash(String, String)#[]=' with types String, Int32

puts phone_of
puts phone_of.empty?
phone_of = {} of String => String | Int32
phone_of["Jane"] = "123"
phone_of["Jack"] = "456"

phone_of["Narnia"] = 42

# phone_of["is_it_true?"] = true
# Error: no overload matches 'Hash(String, Int32 | String)#[]=' with types String, Bool

puts phone_of
phone_of = {} of String => String | Int32 | Bool
phone_of["Jane"] = "123"
phone_of["Jack"] = "456"

phone_of["Narnia"] = 42

phone_of["is_it_true?"] = true

puts phone_of

Hash and types

person = {
  "name"   => "Foo Bar",
  "number" => 42,
}
p! person
puts typeof(person) # Hash(String, Int32 | String)
person # => {"name" => "Foo Bar", "number" => 42}
Hash(String, Int32 | String)

Hash get value, get default value

  • Get value of a key
  • Get value or nil if the key does not exist
  • Get value of a default value if the key does not exist
person = {
  "name"   => "Jane",
  "number" => 42,
}
puts person

# Unhandled exception: Missing hash key: "email" (KeyError)
# email = person["email"]
email = person["email"]?
puts email.nil? # true

name = person["name"]?
puts name # Jane

email = person["email"]? || "default@example.com"
puts email.nil? # false
puts email      # default@example.com

Merge hashes

  • merge
  • merge!
planets = {
  "Mars"    => 1,
  "Jupyter" => 2,
  "Saturn"  => 3,
  "Earth"   => 4,
}
puts planets
puts planets.merge({"Venus" => 5, "Mars" => 10})
puts planets

puts planets.merge!({"Pluto" => 5, "Mars" => 20})
puts planets
{"Mars" => 1, "Jupyter" => 2, "Saturn" => 3, "Earth" => 4}
{"Mars" => 10, "Jupyter" => 2, "Saturn" => 3, "Earth" => 4, "Venus" => 5}
{"Mars" => 1, "Jupyter" => 2, "Saturn" => 3, "Earth" => 4}
{"Mars" => 20, "Jupyter" => 2, "Saturn" => 3, "Earth" => 4, "Pluto" => 5}
{"Mars" => 20, "Jupyter" => 2, "Saturn" => 3, "Earth" => 4, "Pluto" => 5}

Delete - remove an element from a hash

  • delete

  • delete! does not exist.

planets = {
  "Mars"    => 1,
  "Jupyter" => 2,
  "Saturn"  => 3,
  "Earth"   => 4,
}
puts planets.delete("Mars")
puts planets
1
{"Jupyter" => 2, "Saturn" => 3, "Earth" => 4}

Reject - remove an element from a hash

  • reject
  • reject!
planets = {
  "Mars"    => 1,
  "Jupyter" => 2,
  "Saturn"  => 3,
  "Earth"   => 4,
}
puts planets.reject("Mars")
puts planets

puts planets.reject!("Mars")
puts planets
{"Jupyter" => 2, "Saturn" => 3, "Earth" => 4}
{"Mars" => 1, "Jupyter" => 2, "Saturn" => 3, "Earth" => 4}
{"Jupyter" => 2, "Saturn" => 3, "Earth" => 4}
{"Jupyter" => 2, "Saturn" => 3, "Earth" => 4}

Clear - empty a hash

  • clear
planets = {
  "Mars"    => 1,
  "Jupyter" => 2,
  "Saturn"  => 3,
  "Earth"   => 4,
}
puts planets.clear # {}
puts planets       # {}

Select - keep certain key-value pairs

  • select
  • select!
planets = {
  "Mars"    => 1,
  "Jupyter" => 2,
  "Saturn"  => 3,
  "Earth"   => 4,
}

puts planets.select { |name, number| number > 2 && name.includes?("r") }
puts planets

puts planets.select! { |name, number| number > 2 && name.includes?("r") }
puts planets
{"Saturn" => 3, "Earth" => 4}
{"Mars" => 1, "Jupyter" => 2, "Saturn" => 3, "Earth" => 4}
{"Saturn" => 3, "Earth" => 4}
{"Saturn" => 3, "Earth" => 4}

Multi-dimensional hash

planets = {
  "Mars" => {
    "color" => "Red, brown and tan.",
  },
  "Jupyter" => {
    "color" => "Brown, orange and tan, with white cloud stripes.",
  },
  "Saturn" => {
    "color" => "Golden, brown, and blue-grey.",
  },
  "Earth" => {
    "color" => "Blue, brown green and white.",
  },
}

puts planets["Mars"]["color"]

# puts planets["Mercury"]
# Unhandled exception: Missing hash key: "Mercury" (KeyError)

# puts planets["Mars"]["rover"]
# Unhandled exception: Missing hash key: "rover" (KeyError)

Dig a hash

  • dig
  • dig?
planets = {
  "Mars" => {
    "Traveler" => "Elon",
    "People"   => "Green",
  },
  "Jupyter" => 2,
  "Saturn"  => 3,
  "Earth"   => 4,
}
# puts planets["Mars"]["People"]
# Error: undefined method '[]' for Int32 (compile-time type is (Hash(String, String) | Int32))

puts planets.dig "Mars", "People" # Green

# puts planets.dig "Mars", "Date"
# Unhandled exception: Missing hash key: "Date" (KeyError)

date = puts planets.dig? "Mars", "Date" # nil
puts date.nil?                          # true

Files

Files intro

Read from file (slurp)

  • read

  • slurp

  • Read the content of the whole file

  • Raise exception File::NotFoundError if file does not exist

if ARGV.size != 1
  puts "Need a filename on the command line"
  exit 1
end
filename = ARGV[0]

content = File.read(filename)

puts content

Read lines into array

  • read_lines
if ARGV.size != 1
  puts "Need a filename on the command line"
  exit 1
end
filename = ARGV[0]

content = File.read_lines(filename)

puts content

Read file line-by-line

  • each_line
if ARGV.size != 1
  puts "Need a filename on the command line"
  exit 1
end
filename = ARGV[0]

File.each_line(filename) { |line|
  puts line
}

Write to file

  • write

  • Write the content to the file

  • Raise exception if cannot open file for writing

  • e.g. raise File::NotFoundError if parent directory does not exist

if ARGV.size != 2
  puts "Need a filename and the content to write to it on the command line"
  exit 1
end
filename, content = ARGV

File.write(filename, content)

Append to file

  • append
if ARGV.size != 2
  puts "Need a filename and the content to write to it on the command line"
  exit 1
end
filename, content = ARGV

File.write(filename, content, mode: "a")
  • This is a bug in my code, but it still creates the file with some strange rights ---xr-Sr--
File.write("out.txt", "content", mode = "a")
  • The ameba linter will catch this error.

Does file exist?

  • exists?
if ARGV.size != 1
  puts "Need a filename on the command line"
  exit 1
end
filename = ARGV[0]

if File.exists?(filename)
  puts "Exists"
else
  puts "Does NOT exist"
end

Size of file?

  • empty?
  • size
if ARGV.size != 1
  puts "Need a filename on the command line"
  exit 1
end
filename = ARGV[0]

if File.exists?(filename)
  puts File.empty?(filename)
  puts File.size(filename)
else
  puts "File #{filename} does not exist"
end

Last Modified date of file?

  • modification_time
if ARGV.size != 1
  puts "Need a filename on the command line"
  exit 1
end
filename = ARGV[0]

puts File.info(filename).modification_time

Counter

# if file exists read the content

filename = "counter.txt"

counter = 0
if File.exists?(filename)
  content = File.read(filename)
  counter = content.to_i
end

counter += 1
puts counter

File.write(filename, counter)

Multi Counter JSON

  • json
  • from_json
  • to_json
require "json"

filename = "counter.json"

# if file exists read the content
counters = {} of String => Int32
if File.exists?(filename)
  content = File.new(filename)
  counters = Hash(String, Int32).from_json(content)
end

if ARGV.size == 1
  name = ARGV[0]
  if !counters.has_key?(name)
    counters[name] = 0
  end
  counters[name] += 1
  puts counters[name]
  File.write(filename, counters.to_json)
else
  counters.each { |arg, i|
    puts "#{i}: #{arg}"
  }
end

Multi Counter YAML

  • yaml

  • from_yaml

  • to_yaml

  • YAML

require "yaml"

filename = "counter.yaml"

counters = {} of String => Int32
if File.exists?(filename)
  content = File.new(filename)
  counters = Hash(String, Int32).from_yaml(content)
end

if ARGV.size == 1
  name = ARGV[0]
  if !counters.has_key?(name)
    counters[name] = 0
  end
  counters[name] += 1
  puts counters[name]
  File.write(filename, counters.to_yaml)
else
  counters.each { |arg, i|
    puts "#{i}: #{arg}"
  }
end

Directories

List directory content

  • Dir
if ARGV.size != 1
  puts "Needs path to directory"
  exit 1
end

path = ARGV[0]

dr = Dir.new(path)
dr.children.each { |thing|
  puts thing
}

List directory tree

  • Dir
  • glob
if ARGV.size != 1
  puts "Needs path to directory"
  exit 1
end

path = ARGV[0]

dr = Dir.glob("#{path}/**/*")
dr.each { |name| puts name }

Get Current working directory (cwd, pwd)

puts Dir.current

Temporary directory

  • tempname
  • tempdir
  • file_utils
  • FileUtils
  • rm_rf
  • cd
require "file_utils"

tempdir = File.tempname
FileUtils.mkdir(tempdir)
original = Dir.current
puts tempdir
FileUtils.cd(tempdir)

File.write("welcome.txt", "Hello World")

FileUtils.cd(original)
FileUtils.rm_rf(tempdir)

Tempdir function

require "file_utils"

tempdir(cleanup: true) do |tmp_dir|
  puts tmp_dir
  path = Path.new(tmp_dir, "welcome.txt")
  File.write(path, "Hello World")
end

def tempdir(cleanup = true)
  tmp_dir = File.tempname
  begin
    FileUtils.mkdir(tmp_dir)
    yield tmp_dir
  ensure
    if cleanup
      FileUtils.rm_rf(tmp_dir)
    end
  end
end

Join / concatenate file system path

path = Path.new("one", "two", "welcome.txt")
puts path # one/two/welcome.txt

Join / concatenate file system path

  • Path
  • /
home = Path.home
puts home
other = home / "other" / "file.txt"
puts other

Path from string

path = Path["/home/foobar"]
puts path

Expand Path

  • expand
  • ~
some_path = "~/work"
path = Path[some_path].expand(home: true)
puts path

puts Path["~/other"].expand(home: true)
puts Path["~/other"].expand
/home/gabor/work
/home/gabor/other
/home/gabor/work/slides/crystal/~/other

Sets

Create empty set

names = Set(String).new
puts names.empty?

names.add("Joe")
puts names
puts names.empty?

Set examples

animals = Set{"snake", "mouse", "giraf"}
long = Set{"cable", "snake"}

puts animals
puts animals.includes?("snake")
puts animals.includes?("table")
long.add("journey")
puts long

Functions

Functions and methods

Function return value

  • return
def welcome
  return "Hello World!"
end

puts welcome
text = welcome
puts text
Hello World!
Hello World!

Function parameter passing

def welcome(name)
  return "Hello #{name}!"
end

puts welcome("Foo")
Hello Foo!

Function parameter default value

  • default
def welcome(name = "World")
  return "Hello #{name}!"
end

puts welcome
puts welcome("Foo")
Hello World!
Hello Foo!
  • Type definition for parameters
def welcome(name : String)
  return "Hello #{name}"
end

puts welcome "Foo"

# puts welcome 42
# Error: no overload matches 'welcome' with type Int32

def add(x, y : Int32)
  puts "#{x} #{y}"
end

add(2, 3)

# add(2, 3.1)
# Error: no overload matches 'add' with types Int32, Float64

add(2.1, 3) # this works

Wrong number of arguments

  • We have a function that expect two integers. Can we instead pass an array of two integers?
  • Normally no, but there are at least 3 solutions
def f(x, y)
  return x + y
end

puts f(2, 3)
values = [3, 4]
puts values

puts f(values[0], values[1])

# puts f(values)
# Error: wrong number of arguments for 'f' (given 1, expected 2)

Any number of arguments (splat, *)

def sum(*numbers : Int32)
  puts typeof(numbers) # Tuple(Int32, Int32, Int32)
  numbers.sum
end

res = sum(2, 3, 4)
puts res # 9

Manually separate

def f(x, y)
  return x + y
end

puts f(2, 3)
values = [3, 4]
puts values

puts f(values[0], values[1])

Tuple from

def f(x, y)
  return x + y
end

puts f(2, 3)
values = [3, 4]
puts values

puts f(*{Int32, Int32}.from(values))

Array overload

def f(x, y)
  return x + y
end

def f(args : Array(Int32))
  return f(args[0], args[1])
end

puts f(2, 3)
values = [3, 4]
puts values
puts f(values)

Multiple dispatch

  • Multi-dispatch functions with same name but different signature
def welcome(name : String)
  puts "welcome string"
end

def welcome(number : Int32)
  puts "welcome integer"
end

welcome("Foo")
welcome(42)
  • Integers are also accepted when we are expecting floats
def add(x : Int32, y : Int32)
  puts "handling two integers"
end

def add(x : Float64, y : Float64)
  puts "handling two floats"
end

add(2, 3)
add(2.1, 3.1)
add(2.1, 3)

Implicit return value

If there is no explicit return statement then the result of the last statement executed in the function will be returned from the function.

def add(x, y)
  z = x + y
end

res = add(2, 3)
puts res # 5
def cond(x)
  if x > 5
    return x
  end
end

[3, 6].each { |value|
  puts value
  res = cond(value)
  puts res
  puts typeof(res)

  # puts res+1
  # Error: undefined method '+' for Nil (compile-time type is (Int32 | Nil))

  if !res.nil?
    puts res + 1
  end
}
3

(Int32 | Nil)
6
6
(Int32 | Nil)
7

Return Type definition

  • The compile-time error only happens if we actually call the incorrect function.
def is_odd(n : Int32) : Bool
  return n % 2 == 1
end

def is_even(n : Int32) : Bool
  return n
end

x = is_odd(2)
puts x
y = is_even(2)
puts y

#  5 | def is_even(n : Int32) : Bool
# Error: method top-level is_even must return Bool but it is returning Int32

Type or Nil

  • ?

  • Nil

  • A question mark ? after the type in the function declaration is the same as accepting nil as well.

  • Int32? is the same as Int32 | ::Nil

def func(number : Int32?)
  puts number
end

func(23)
func(nil)

# func()
# Error: wrong number of arguments for 'func' (given 0, expected 1)

# func(2.3)
# Error: no overload matches 'func' with type Float64

def show(number : Int32)
  puts number
end

show(23)

# show(nil)
# Error: no overload matches 'show' with type Nil

def other(number : Int32 | ::Nil)
  puts number
end

other(23)
other(nil)

Yield

def run
  puts "before"
  yield
  puts "after"
end

run {
  puts "in block"
}

run do
  puts "in do-end"
end
  • Optionally you can add a &anything if you think that makes the code more readable, but the important part is having yield in the code.
def run(&block)
  puts "before"
  yield
  puts "after"
end

run {
  puts "in block"
}
  • You can call yield more than once inside the function and the block will be executed for every yield.
def run
  puts "before"
  yield
  puts "middle"
  yield
  puts "after"
end

run {
  puts "in block"
}
puts "----"
run do
  puts "in do-end"
end

Yield with parameters

def run
  puts "before"
  yield 42
  puts "after"
end

run { |value|
  puts "in block #{value}"
}
puts "----"

run do |value|
  puts "in do-end #{value}"
end
puts "----"

run {
  puts "in block"
}
puts "----"

run do
  puts "in do-end"
end
  • This example based on the example on the Crystal web site does not work:
def foo
  puts "foo before"
  yield
  puts "foo after"
end

def bar
  puts "bar before"
  yield
  puts "bar after"
end

foo bar {
  puts "in block"
}

puts "----"

foo bar do
  puts "in do-end"
end

Block and parameters

def run(number, &block)
  puts "before"
  yield number
  puts "after"
end

run(10) { |value|
  puts "in block #{value}"
}
puts "----"

run(20) do |value|
  puts "in do-end #{value}"
end

Tuples and Named Tuples

Create Tuple

a = {"Foo", "Bar", 42, true}
puts a
puts typeof(a)

b = Tuple.new("Foo", "Bar", 42, true)
puts b
puts typeof(b)
{"Foo", "Bar", 42, true}
Tuple(String, String, Int32, Bool)
{"Foo", "Bar", 42, true}
Tuple(String, String, Int32, Bool)

Create Named Tuple

a = {fname: "Foo", "lname": "Bar"}
puts a
puts typeof(a)

b = NamedTuple.new(fname: "Foo", "lname": "Bar")
puts b
puts typeof(b)

Access fields of Named Tuple

tpl = {fname: "Foo", "lname": "Bar"}
puts tpl
puts typeof(tpl)

puts tpl[:fname]
puts tpl["fname"]
puts tpl[:lname]
puts tpl["lname"]

# tpl["fname"] = "Jane"
# tpl["lname"] = "Bar"

Named Tuple with non-alphanumeric fields

macro t(name)
  print "%s %s\n" % {typeof({{name}}), {{name}}}
end

h = {a: 1, "a b-23": 2}
t h
h.each { |x, y|
  t x
  t y
}

Named Tuple with optional field

require "json"

alias Person = NamedTuple(
  name: String,
  email: String?,
)

p1_str = %[{"name": "Foo", "email": "foo@bar.com"}]
p1 = Person.from_json(p1_str)
p p1

p2_str = %[{"name": "Bar"}]
p2 = Person.from_json(p2_str)
# Unhandled exception: Missing json attribute: email
p p2

p3_str = %[{"name": "Bar", "email": null}]
p3 = Person.from_json(p3_str)
p p3

# {name: "Foo", email: "foo@bar.com"}
# {name: "Bar", email: nil}
# {name: "Bar", email: nil}

Structs

Empty struct

  • struct

Structs are very powerful constructs in Crystal. Very similar to classes, but they are usually faster.

In the first example we create an empty struct. It does not give as a lot, but we have to start somewhere.

struct MyConfig
end

cfg = MyConfig.new
p! cfg
p! typeof(cfg)
cfg # => MyConfig()
typeof(cfg) # => MyConfig

Initialize immutable struct

  • initialize

A more realistic example is a struct that has a method called initialize that can be used to set the attributes of the struct. Each variable with a single @ sign in-front of it is an attribute.

We can initialize some of the attributes by values received from the user and some attributes by generating the value ourselves. e.g. by using a Time object, a Random value or generating or computing it in any other way.

We can print the content of the Struct, but we have no way to access the attributes and no way to change them. Hence this struct is immutable.

  • There is no way to change this struct
  • There is no way to access the individual attributes as there are no getters
struct Person
  def initialize(name : String, email : String)
    @name = name
    @email = email
    @time = Time.utc
  end
end

foo = Person.new("Foo", "me@foo.bar")
p! foo
# p! foo.name
# Error: undefined method 'name' for Person
foo # => Person(@name="Foo", @email="me@foo.bar", @time=2021-07-11 06:22:05.981166766 UTC)

Initialize immutable struct - shorthand

Writing each attribute name 3 times is quite annoying, luckily Crystal provides a shorthand writing mode.

struct Person
  def initialize(@name : String, @email : String)
    @time = Time.utc
  end
end

foo = Person.new("Foo", "me@foo.bar")
p! foo
foo # => Person(@name="Foo", @email="me@foo.bar", @time=2021-07-11 06:25:04.796465667 UTC)

Immutable struct with getters

We can defined methods in the struct to become the getters of the attributes, but this too is boring.

struct Person
  def initialize(@name : String, @email : String)
  end

  def name
    @name
  end

  def email
    @email
  end
end

foo = Person.new("Foo", "me@foo.bar")
p! foo
p! foo.name
p! foo.email
foo # => Person(@name="Foo", @email="me@foo.bar")
foo.name # => "Foo"
foo.email # => "me@foo.bar"

Immutable struct with getter macro

We can use the getter macro to create getters to all of the attributes.

struct Person
  getter name, email

  def initialize(@name : String, @email : String)
  end
end

foo = Person.new("Foo", "me@foo.bar")
p! foo
p! foo.name
p! foo.email
foo # => Person(@name="Foo", @email="me@foo.bar")
foo.name # => "Foo"
foo.email # => "me@foo.bar"

Mutable Struct with setter

struct Person
  def initialize(@name : String, @email : String)
  end

  def name
    @name
  end

  def email
    @email
  end

  def name(value)
    @name = value
  end

  def email=(value)
    @email = value
  end
end

prs = Person.new("Foo", "me@foo.bar")
p! prs
p! prs.name
p! prs.email

prs.name("Bar")
p! prs.name

prs.email=("bar@foo.bar")
p! prs.email

prs.email = "new@foo.bar"
p! prs.email
prs # => Person(@name="Foo", @email="me@foo.bar")
prs.name # => "Foo"
prs.email # => "me@foo.bar"
prs.name # => "Bar"
prs.email # => "bar@foo.bar"
prs.email # => "new@foo.bar"

Mutable Struct with property macro

struct Person
  property name : String
  property email : String

  def initialize(@name, @email)
  end
end

foo = Person.new("Foo", "me@foo.bar")
p! foo
p! foo.name
p! foo.email

foo.email = "new@foo.bar"
p! foo.email
foo # => Person(@name="Foo", @email="me@foo.bar")
foo.name # => "Foo"
foo.email # => "me@foo.bar"
foo.email # => "new@foo.bar"

Struct with optional attributes

struct Person
  property name : String
  property email : String?

  def initialize(@name)
  end

  def initialize(@name, @email)
  end
end

prs = Person.new("Foo")
p! prs
p! prs.name
p! prs.email

prs.email = "foo@bar.com"
p! prs
p! prs.email
prs # => Person(@name="Foo", @email=nil)
prs.name # => "Foo"
prs.email # => nil
prs # => Person(@name="Foo", @email="foo@bar.com")
prs.email # => "foo@bar.com"

Struct with default value

struct Person
  property name : String = ""
  property email : String?
end

prs = Person.new
p! prs
p! prs.name
p! prs.email

prs.name = "Foo"
prs.email = "new@foo.bar"
p! prs.name
p! prs.email
prs # => Person(@name="", @email=nil)
prs.name # => ""
prs.email # => nil
prs.name # => "Foo"
prs.email # => "new@foo.bar"

Struct pass-by-value

For immutable structs this is not relevant but when the struct is mutable you have to remember that it is passed by value to functions. That is, the function receives a copy of the external struct. Any changes made to the struct inside the function will be lost when you leave the function.

struct Person
  property name : String
  property email : String

  def initialize(@name, @email)
  end
end

def set_email(pers : Person)
  pers.email = "fake@address.com"
  p! pers
end

prs = Person.new("Foo", "me@foo.bar")
p! prs
p! prs.name
p! prs.email

puts ""

set_email(prs)

puts ""
p! prs
p! prs.name
p! prs.email
prs # => Person(@name="Foo", @email="me@foo.bar")
prs.name # => "Foo"
prs.email # => "me@foo.bar"

pers # => Person(@name="Foo", @email="fake@address.com")

prs # => Person(@name="Foo", @email="me@foo.bar")
prs.name # => "Foo"
prs.email # => "me@foo.bar"

Struct from JSON

require "json"

struct Person
  include JSON::Serializable

  getter name : String
  getter email : String
end

json_str = %{{"name": "Bar", "email": "bar@foobar.com"}}
prs = Person.from_json(json_str)
p! prs
p! prs.name
p! prs.email
prs # => Person(@name="Bar", @email="bar@foobar.com")
prs.name # => "Bar"
prs.email # => "bar@foobar.com"

Struct from JSON - upper case

  • We cannot have attributes starting with upper case so we have to convert the field names to lowercase:
  • Using attribute annotation
require "json"

struct Person
  include JSON::Serializable

  getter name : String

  @[JSON::Field(key: "Email")]
  getter email : String
end

json_str = %{{"name": "Bar", "Email": "bar@foobar.com"}}
prs = Person.from_json(json_str)
p! prs
p! prs.name
p! prs.email
prs # => Person(@name="Bar", @email="bar@foobar.com")
prs.name # => "Bar"
prs.email # => "bar@foobar.com"

Struct both from JSON and initialize

  • initialize
require "json"

struct Person
  include JSON::Serializable

  getter name : String
  getter email : String

  def initialize(@name, @email)
  end
end

prs1 = Person.new("Foo", "me@foo.bar")
p! prs1
p! prs1.name
p! prs1.email

json_str = %{{"name": "Bar", "email": "bar@foobar.com"}}
prs2 = Person.from_json(json_str)
p! prs2
p! prs2.name
p! prs2.email
prs1 # => Person(@name="Foo", @email="me@foo.bar")
prs1.name # => "Foo"
prs1.email # => "me@foo.bar"
prs2 # => Person(@name="Bar", @email="bar@foobar.com")
prs2.name # => "Bar"
prs2.email # => "bar@foobar.com"

Struct from JSON - manual parsing

require "json"

struct Person
  getter name : String
  getter email : String

  def initialize(pull) # JSON::PullParser
    @name = ""
    @email = ""
    pull.read_object do |key|
      case key
      when "name"
        @name = pull.read_string
      when "email"
        @email = pull.read_string
      end
    end
  end
end

json_str = %{{"name": "Bar", "email": "bar@foobar.com"}}
prs = Person.from_json(json_str)
p! prs
p! prs.name
p! prs.email
prs # => Person(@name="Bar", @email="bar@foobar.com")
prs.name # => "Bar"
prs.email # => "bar@foobar.com"

Multi-level struct manually

require "json"

struct Address
  getter street : String
  getter city : String
  getter country : String

  def initialize(@street, @city, @country)
  end
end

struct Person
  getter name : String
  getter email : String
  getter address : Address

  def initialize(@name, @email, @address)
  end
end

adr = Address.new("Main str. 3", "Capital", "Big")
pp! adr

prs = Person.new("Foo", "me@foo.bar", adr)
pp! prs
adr # => Address(@city="Capital", @country="Big", @street="Main str. 3")
prs # => Person(
 @address=Address(@city="Capital", @country="Big", @street="Main str. 3"),
 @email="me@foo.bar",
 @name="Foo")

Multi-level struct from JSON

require "json"

struct Address
  include JSON::Serializable

  getter street : String
  getter city : String
  getter country : String
end

struct Person
  include JSON::Serializable

  getter name : String
  getter email : String
  getter address : Address
end

json_str = %[{
  "name": "Bar",
  "email": "bar@foobar.com",
  "address" : {
    "street": "Broadway",
    "city": "New York",
    "country": "USA"
   }
}]
prs = Person.from_json(json_str)
pp! prs

p! prs.name
p! prs.address
p! prs.address.street
prs # => Person(
 @address=Address(@city="New York", @country="USA", @street="Broadway"),
 @email="bar@foobar.com",
 @name="Bar")
prs.name # => "Bar"
prs.address # => Address(@street="Broadway", @city="New York", @country="USA")
prs.address.street # => "Broadway"

Struct from JSON with extra data

require "json"

struct Person
  include JSON::Serializable

  getter name : String
  getter email : String
end

json_str = %[{
  "name": "Bar",
  "email": "bar@foobar.com",
  "address" : "Somewhere"
}]
prs = Person.from_json(json_str)
pp! prs

p! prs.name
prs # => Person(@email="bar@foobar.com", @name="Bar")
prs.name # => "Bar"

Struct from JSON missing data (optional fields)

require "json"

struct Person
  include JSON::Serializable

  getter name : String
  getter email : String
  getter address : String?
end

json_str = %[{
  "name": "Bar",
  "email": "bar@foobar.com",
  "address" : "my address"
}]
prs = Person.from_json(json_str)
pp! prs
p! prs.name
p! prs.address
puts ""

json_str = %[{
  "name": "Bar",
  "email": "bar@foobar.com"
}]
prs = Person.from_json(json_str)
pp! prs
p! prs.name
p! prs.address
prs # => Person(@address="my address", @email="bar@foobar.com", @name="Bar")
prs.name # => "Bar"
prs.address # => "my address"

prs # => Person(@address=nil, @email="bar@foobar.com", @name="Bar")
prs.name # => "Bar"
prs.address # => nil

Extend struct

struct MyConfig
end

cfg = MyConfig.new
p! cfg
p! typeof(cfg)

struct MyConfig
  property name : String?
end

cfg.name = "Foo"
p! cfg
cfg # => MyConfig(@name=nil)
typeof(cfg) # => MyConfig
cfg # => MyConfig(@name="Foo")

Extend other structs

struct Int
  def prime?
    (2..(self ** 0.5).to_i).each { |num|
      return false if self % num == 0
    }
    return true
  end
end

puts 23.odd?
puts 23.even?
puts 23.prime?
puts 20.prime?

Classes

Empty Class definition

  • class
class Person
end

prs = Person.new
puts prs # #<Person:0x7f8eb5bb3eb0>
p! prs   # prs # => #<Person:0x7f8eb5bb3eb0>

Class with attributes

  • initialize
  • @
class Person
  def initialize(name : String, height : Float64)
    @name = name
    @height = height
  end
end

# prs = Person.new
# Error: wrong number of arguments for 'Person.new' (given 0, expected 2)

prs = Person.new(name: "Joe", height: 180)
puts prs # #<Person:0x7f3e27f68e40>
p! prs   # prs # => #<Person:0x7f3e27f68e40 @name="Joe", @height=180.0>

# puts prs.name
# Error: undefined method 'name' for Person

Class with getters

class Person
  def initialize(name : String, height : Float64)
    @name = name
    @height = height
  end

  def name
    @name
  end
end

prs = Person.new(name: "Joe", height: 180)
puts prs      # #<Person:0x7f1678dd0e40>
p! prs        # prs # => #<Person:0x7f1678dd0e40 @name="Joe", @height=180.0>
puts prs.name # Joe

# prs.name = "Jane"
# Error: undefined method 'name=' for Person

Class with setter

class Person
  def initialize(name : String, height : Float64)
    @name = name
    @height = height
  end

  def name
    @name
  end

  def name(value)
    @name = value
  end
end

prs = Person.new(name: "Joe", height: 180)
puts prs      # #<Person:0x7f1678dd0e40>
p! prs        # prs # => #<Person:0x7f1678dd0e40 @name="Joe", @height=180.0>
puts prs.name # Joe

prs.name("Jane")

p! prs        # prs # => #<Person:0x7f1678dd0e40 @name="Jane", @height=180.0>
puts prs.name # Jane

Class with getters and setter (property)

  • property
  • getter
class Person
  property name : String

  def initialize(@name = "George")
  end
end

prs = Person.new(name: "Joe")
p! prs        # prs # => #<Person:0x7f8d0f266e80 @name="Joe">
puts prs.name # Joe
prs.name = "Jane"
puts prs.name # Jane

defaulty = Person.new
puts defaulty.name # George

Class with property with default value

  • property
class Person
  property name : String

  def initialize
    @name = "Default name"
  end
end

prs = Person.new
p! prs        # prs # => #<Person:0x7fa3aaae2eb0>
puts prs.name # Default name
prs.name = "Joe"
puts prs.name # joe

Class with declared getter and default value

  • getter
class Person
  getter name : String

  def initialize
    @name = "Default name"
  end
end

prs = Person.new
p! prs        # prs # => #<Person:0x7f804672fe80 @name="Default name">
puts prs.name # Default name

# prs.name = "Joe"
# Error: undefined method 'name=' for Person

Class with declared getter

  • getter
class Person
  getter name : String

  def initialize(name)
    @name = name
  end
end

prs = Person.new(name: "Joe")
p! prs        # prs # => #<Person:0x7f804672fe80 @name="Joe">
puts prs.name # Joe

# prs.name = "Jane"
# Error: undefined method 'name=' for Person

Serialize Crystal-lang class to/from JSON from_json to_json

require "json"

class Person
  include JSON::Serializable

  property name : String
  property height : Float64

  def initialize(@name, @height)
  end
end

prs = Person.new(name: "Jane", height: 173.1)
p! prs
p! prs.to_json

george_str = %{{"name": "George", "height": 171.19}}
# details = JSON.parse(george_str)
# puts details

prs = Person.from_json(george_str)
p! prs
puts prs.name
puts prs.height
puts typeof(prs.name)
puts typeof(prs.height)

people_str = %{[{"name": "George", "height": 171.19}, {"name": "Jane", "height": 168.23}]}
# details = JSON.parse(people_str)
people = Array(Person).from_json(people_str)
p! people

Compare objects for equality

Normally using == between two instances will only return true if they are the exact same objects in the memory. If "only" all the attributes are the same then == will be false.

To be able to compare two objects based on their attributes only we can used the def_equals macro.

class Person
  def_equals @name, @height
  property name : String
  property height : Float64

  def initialize(@name, @height)
  end
end

prs1 = Person.new(name: "Jane", height: 173.1)
prs2 = prs1.dup
prs3 = Person.new(name: "Jane", height: 173.1)
prs4 = prs1
prs5 = Person.new(name: "Jane", height: 173.2)
p! prs1
p! prs2
p! prs3
puts prs1 == prs2 # true
puts prs1 == prs3 # true

puts prs1 == prs4 # true
puts prs1 == prs5 # false

Singleton using class properties

  • class_property
class Options
  class_property repetition : Int32 | Nil
  class_property url = String
  class_property verbose = Bool
end

puts Options.repetition
Options.repetition = 3
puts Options.repetition
if !Options.repetition.nil?
  puts Options.repetition + 1
end

Singleton using class properties with default values

  • class_property
class Options
  class_property repetition = 0
  class_property url = ""
  class_property verbose = false
end

puts Options.repetition
Options.repetition = 3
puts Options.repetition
puts Options.repetition + 1

Class monkey-path add method

class Person
  def initialize(name : String, height : Float64)
    @name = name
    @height = height
  end
end
require "./person"

class Person
  def name
    return @name
  end
end

prs = Person.new(name: "Joe", height: 180)
puts prs # #<Person:0x7f3e27f68e40>
p! prs   # prs # => #<Person:0x7f3e27f68e40 @name="Joe", @height=180.0>

puts prs.name
require "./person"

class Person
  property email : String | ::Nil
end

prs = Person.new(name: "Joe", height: 180)
puts prs # #<Person:0x7f3e27f68e40>
p! prs   # prs # => #<Person:0x7f3e27f68e40 @name="Joe", @email=nil, @height=180.0>

prs.email = "foo@bar.com"
p! prs # prs # => #<Person:0x7f3e27f68e40 @name="Joe", @email="foo@bar.com", @height=180.0>

Stringification to_s

  • to_s
class Point
  property x : Float64
  property y : Float64

  def initialize(@x, @y)
  end

  def to_s
    return "(#{@x}, #{@y})"
  end
end

p = Point.new(2.1, 3.4)
puts p
puts p.to_s
#<Point:0x7f037505efc0>
(2.1, 3.4)

Regexes

Alternate delimiter matching slashes

  • %r
text = "some/path"
match = /\/(.*)/.match(text)
if match
  puts match.[1]
end

match = %r{/(.*)}.match(text)
if match
  puts match.[1]
end

Substitute

  • sub

  • gsub

  • gsub is the global substituted or /g in other languages.

text = "ab  text. and $ and ^ also"
puts text
puts text.sub(/\W/, "")
puts text.gsub(/\W/, "")
ab  text. and $ and ^ also
ab text. and $ and ^ also
abtextandandalso

gsub

puts "The cat is here".gsub("cat", "dog")
puts "The cat is here".gsub("cat") { "dog" }

# puts "The cat is here".gsub("cat") { |match| match.reverse }
# ameba: Use short block notation instead
puts "The cat is here".gsub("cat", &.reverse)

# puts "The cat is here".gsub(/c../) { |match| match.reverse }
# ameba: Use short block notation instead
puts "The cat is here".gsub(/c../, &.reverse)

Testing

Testing 1

def add(x, y)
  return x * y
end
require "./mymath.cr"
puts add(2, 2)
require "spec"
require "./mymath.cr"

describe "add" do
  it "correctly adds two numbers" do
    add(2, 2).should eq 4
  end
end

describe "add" do
  it "correctly adds two numbers" do
    add(2, 3).should eq 5
  end
end

# describe Array do
#   describe "#size" do
#     it "correctly reports the number of elements in the Array" do
#       [1, 2, 3].size.should eq 3
#     end
#   end

#   describe "#empty?" do
#     it "is true when no elements are in the array" do
#       ([] of Int32).empty?.should be_true
#     end

#     it "is false if there are elements in the array" do
#       [1].empty?.should be_false
#     end
#   end
# end
crystal spec .

Testing with Spec

require "spec"
require "./spec_helper"

describe "demo test" do
  it "some free text here" do
    result = 23 + 19 # this might be calling the real application
    result.should eq 42
  end
end
def add(x, y)
  return x*y
end

describe "test cases" do
  it "good" do
    add(2, 2).should eq 4
  end

  pending "add" do
    add(2, 3).should eq 5
  end
  pending "add" do
    add(2, 4).should eq 6
  end
end
crystal spec

Modules

Namespace

module MyProject
  class Device
  end
end

x = MyProject::Device.new
p! x

Extend class - include module

module Tools
  def screwdriver
    puts "Screwdriver"
  end
end

class Device
  include Tools
end

x = Device.new
p! x
x.screwdriver

JSON

JSON (to_json, parse)

  • json

  • to_json

  • parse

  • Round trip with JSON

  • Have to require "json" for the method to be added.

require "json"

data = {
  "Mars"    => 1,
  "Jupyter" => 2,
  "Saturn"  => 3,
  "Earth"   => 4,
  "moons"   => ["Our", "Jupyters"],
  "missing" => nil,
}
puts data # Hash(String, Array(String) | Int32 | Nil)
json_string = data.to_json
puts json_string

parsed = JSON.parse(json_string)
puts parsed # JSON::Any

other = Hash(String, Array(String) | Int32 | Nil).from_json(json_string)
puts other.keys

JSON to NamedTuple

  • NamedTuple
  • from_json
  • JSON
require "json"

alias Thing = NamedTuple(name: String, number: Int32)

thing_json = %{{"name": "table", "number": 3}}
puts thing_json

tg = Thing.from_json(thing_json)
puts tg

Reading a JSON file

  • NamedTuple
{
    "status": 200,
    "status_text": "OK",
    "count": 3,
    "otzzz": 23,
    "results": [
        {
            "name": "pi",
            "value": 3.14
        },
        {
            "name": "e",
            "value": 2.71
        },
        {
            "name": "Square root of 2",
            "value": 1.41
        }
    ]
}
require "json"

alias Constant = NamedTuple(name: String, value: Float64)
alias MathResponseType = NamedTuple(
  status: Int32,
  status_text: String,
  count: Int32,
  results: Array(Constant))
main

def main
  if ARGV.size != 1
    puts "Needs filename eg. math.json"
    exit 1
  end
  filename = ARGV[0]
  content = File.read(filename)
  data = MathResponseType.from_json(content)
  puts data
  puts data["results"][0].keys
end

# This will complain if field is missing or the value of a field is of incorrect type,
# but it will silently ignore any extra fields

JSON to Array

  • JSON
  • from_json
things_str = %{["table", "chair"]}
data = JSON.parse(things_str)
p! data

# data.each {|thing|
#     puts thing
# }
# Error: undefined method 'each' for JSON::Any

things = Array(String).from_json(things_str)
p! things

things.each { |thing|
  puts thing
}

Range

Range using dots

  • ..
  • ...
(1...3).each { |this|
  puts this
}
(1..3).each { |this|
  puts this
}

Range using a class

  • Range
r = Range.new(1, 3, exclusive: true)
r.each { |this|
  puts this
}
r = Range.new(1, 3)
r.each { |this|
  puts this
}

Range - sum

  • An example using a range and sum together in a one-line statement.
res = (0..3).sum { |ix| ix * ix }
puts res

Range - count

res = (1..10).count { |ix| sprintf("%b", ix) == sprintf("%b", ix).reverse }
puts res

# puts (1..10).map{|ix| sprintf("%b", ix) ==  sprintf("%b", ix).reverse; sprintf("%b", ix) }

Range and loop controls

  • next
  • break
  • continue
(1..10).each { |ix|
  next if ix % 2 == 0
  break if ix == 7
  puts ix
}
1
3
5

Range with step

  • step
(1..10).step(3) { |ix|
  puts ix
}
1
4
7
10

Range to Array

  • to_a
numbers = (2..7).to_a
p! typeof(numbers)
p! numbers
typeof(numbers) # => Array(Int32)
numbers # => [2, 3, 4, 5, 6, 7]

Random

rand

  • rand

  • Random

  • Pseudo Random floating point number between 0 and 1

puts Random.rand    # floating point
puts Random.rand(6) # Int
0.9567402323460531
0

Random in other parts of Crystal

Random as a class

  • next_float
  • next_int
  • next_bool
  • hex
  • base64
rnd = Random.new
puts rnd.next_float
puts rnd.next_int
puts rnd.next_bool
puts rnd.hex(3) # 6 characters from hex values
puts rnd.base64(2)
0.17048389661361377
-258843089
false
ba753d
Fd4=

Random - seed

  • seed

  • Fixed random-looking numbers.

rnd = Random.new(23)
puts rnd.next_float
puts rnd.next_float
puts rnd.next_float
0.6834386634915893
0.8454063470402874
0.4425350078089257

YAML

Parse YAML

---
language:
  name: Crystal
  features:
    - Syntax
    - Type System
    - Null Reference Checks
  first_year: 2014
      
require "yaml"

data = File.open("examples/yaml/crystal.yml") do |file|
  YAML.parse(file)
end
puts data
puts typeof(data) # YAML::Any
puts data["language"]
puts data["language"]["first_year"]
puts typeof(data["language"]["first_year"].to_s.to_i)
# [YAML::Any](https://crystal-lang.org/api/YAML/Any.html)
puts data.as_h.keys
data.as_h.keys.each { |main_key|
  data[main_key].as_h.keys.each { |sub_key|
    puts data[main_key][sub_key]
  }
}

Exception Handling in Crystal-lang

Catch exception - begin, rescue

  • begin
  • rescue
filenames = ["README.md", "Other file", "crystal.json"]
filenames.each { |file|
  begin
    result = process(file)
    puts result
  rescue err
    puts "Rescued! #{err.message}"
    puts err.class # File::NotFoundError
  end
}

def process(filename)
  content = File.read(filename)
  return content.size
end

Raise exception

puts fibonacci(2)
puts fibonacci(-1)
puts fibonacci(3)

def fibonacci(num)
  if num < 0
    raise "Invalid number"
  end
  # ...
  return num
end

Logging

Crystal logging

  • Log

  • 7 levels of logging, default is info and higher to the STDOUT

require "log"

Log.setup(:trace) # set the level of logging

Log.trace { "Trace level" }
Log.debug { "Debug level" }
Log.info { "Info level" }
Log.notice { "Notice level" }
Log.warn { "Warn level" }
Log.error { "Error level" }
Log.fatal { "Fatal level" }

SQLite

Try SQLite

  • SQLite

  • You need to install the development package for libsqlite3

  • On Ubuntu

sudo apt-get install libsqlite3-dev
require "sqlite3"

# Based on the example in the documentation
db_file = "data.db"
if File.exists?(db_file)
  puts "File #{db_file} already exists. Aborting"
  exit(1)
end
DB.open "sqlite3://#{db_file}" do |db|
  db.exec "CREATE TABLE contacts (name TEXT, age INTEGER)"
  db.exec "INSERT INTO contacts VALUES (?, ?)", "John Doe", 30

  args = [] of DB::Any
  args << "Sarah"
  args << 33
  db.exec "INSERT INTO contacts VALUES (?, ?)", args: args

  puts "max age:"
  puts db.scalar "SELECT max(age) FROM contacts" # => 33

  puts "contacts:"
  db.query "SELECT name, age FROM contacts ORDER BY age DESC" do |rs|
    puts "#{rs.column_name(0)} (#{rs.column_name(1)})"
    # => name (age)
    rs.each do
      puts "#{rs.read(String)} (#{rs.read(Int32)})"
      # => Sarah (33)
      # => John Doe (30)
    end
  end
end

Multi-counter with SQLite

  • SQLite
require "sqlite3"

db_file = "counter.db"
if !File.exists?(db_file)
  DB.open "sqlite3://#{db_file}" do |db|
    db.exec "CREATE TABLE counters (name TEXT, count INTEGER, UNIQUE(name))"
  end
end

DB.open "sqlite3://#{db_file}" do |db|
  if ARGV.size == 1
    name = ARGV[0]
    count = 0
    db.query "SELECT count FROM counters WHERE name=?", name do |rs|
      rs.each do
        count = rs.read(Int32)
      end
    end

    count += 1
    puts count

    if count == 1
      db.exec "INSERT INTO counters VALUES (?, ?)", name, count
    else
      db.exec "UPDATE counters SET count=? WHERE name=?", count, name
    end
  else
    db.query "SELECT name, count FROM counters ORDER BY name DESC" do |rs|
      puts "#{rs.column_name(0)} (#{rs.column_name(1)})"
      rs.each do
        puts "#{rs.read(String)} (#{rs.read(Int32)})"
      end
    end
  end
end

Unhandled exception issue: https://github.com/crystal-lang/crystal-sqlite3/issues/52

SQLite last_id last_insert_id

  • last_id
  • last_insert_id
require "sqlite3"

db_file = "data.db"
if File.exists?(db_file)
  puts "File #{db_file} already exists. Aborting"
  exit(1)
end

DB.open "sqlite3://#{db_file}" do |db|
  db.exec "CREATE TABLE contacts (
        id INTEGER PRIMARY KEY,
        name TEXT
    )"
  res = db.exec "INSERT INTO contacts (name) VALUES (?)", "John Doe"
  puts res.rows_affected
  puts res.last_insert_id

  res = db.exec "INSERT INTO contacts (name) VALUES (?)", "Jane Doe"
  puts res.rows_affected
  puts res.last_index_id

  puts "contacts:"
  db.query "SELECT id, name FROM contacts" do |rs|
    puts "#{rs.column_name(0)} (#{rs.column_name(1)})"
    rs.each do
      puts "#{rs.read(Int64)} #{rs.read(String)}"
    end
  end
end

SQLite UPDATE row_affected

require "sqlite3"

db_file = "data.db"
if File.exists?(db_file)
  puts "File #{db_file} already exists. Aborting"
  exit(1)
end

begin
  DB.open "sqlite3://#{db_file}" do |db|
    db.exec "CREATE TABLE contacts (
            id INTEGER PRIMARY KEY,
            name TEXT,
            email TEXT,
            UNIQUE (name)
        )"
    res = db.exec "INSERT INTO contacts (name, email) VALUES (?, ?)", "John Doe", "john@example.com"
    puts res.rows_affected # 1
    res = db.exec "INSERT INTO contacts (name, email) VALUES (?, ?)", "Other Person", "other@example.com"
    puts res.rows_affected # 1

    res = db.exec "UPDATE contacts SET email=? WHERE name=?", "john.doe@example.com", "John Doe"
    puts res.rows_affected # 1

    res = db.exec "UPDATE contacts SET email=? WHERE name=?", "john.doe@example.com", "No such user"
    puts res.rows_affected # 0

    res = db.exec "UPDATE contacts SET email=?", "shared@example.com"
    puts res.rows_affected # 2

    puts "contacts:"
    db.query "SELECT id, name, email FROM contacts" do |rs|
      puts "#{rs.column_name(0)} (#{rs.column_name(1)}) (#{rs.column_name(2)})"
      rs.each do
        puts "#{rs.read(Int64)} #{rs.read(String)} (#{rs.read(String)})"
      end
    end
  end
rescue err
  puts "Exception #{err}"
  # The external begin/rescue is needed because of this bug:
  # https://github.com/crystal-lang/crystal-sqlite3/issues/52
end

SQLite exception handling (during INSERT)

  • The internal exception handling should be enough, but apparently it is not
  • See this report
require "sqlite3"

db_file = "data.db"
if File.exists?(db_file)
  puts "File #{db_file} already exists. Aborting"
  exit(1)
end

begin
  DB.open "sqlite3://#{db_file}" do |db|
    db.exec "CREATE TABLE contacts (
            id INTEGER PRIMARY KEY,
            name TEXT,
            email TEXT,
            UNIQUE (name)
        )"
    res = db.exec "INSERT INTO contacts (name, email) VALUES (?, ?)", "John Doe", "john@example.com"
    puts typeof(res)       # DB::ExecResult
    puts res.rows_affected # 1

    begin
      res = db.exec "INSERT INTO contacts (name, email) VALUES (?, ?)", "John Doe", "john@example.com"
      puts res.rows_affected
    rescue err
      puts "Exception #{err}" # SQLite3::Exception  UNIQUE constraint failed: contacts.name
    end

    puts "contacts:"
    db.query "SELECT id, name, email FROM contacts" do |rs|
      puts "#{rs.column_name(0)} (#{rs.column_name(1)}) (#{rs.column_name(2)})"
      rs.each do
        puts "#{rs.read(Int64)} #{rs.read(String)} (#{rs.read(String)})"
      end
    end
  end
rescue err
  puts "Exception #{err}"
  # The external begin/rescue is needed because of this bug:
  # https://github.com/crystal-lang/crystal-sqlite3/issues/52
end

SQLite all

require "sqlite3"
db_file = "data.db"

create_db(db_file)
insert_data(db_file, "Foo", true, 19)

def create_db(db_file)
  if File.exists?(db_file)
    return
  end
  begin
    DB.open "sqlite3://#{db_file}" do |db|
      db.exec "CREATE TABLE information (
            id INTEGER PRIMARY KEY,
            name TEXT,
            isit BOOLEAN,
            number INTEGER
        )"
    end
  rescue err
    puts "Exception #{err}"
  end
end

def insert_data(db_file, name, isit, number)
  args = [name, isit, number]
  begin
    DB.open "sqlite3://#{db_file}" do |db|
      db.exec "INSERT INTO information
                 (name, isit, number)
                 VALUES (?, ?, ?)", args: args
    end
  rescue err
    puts "Exception #{err}"
  end
end

def get_max
  return db.scalar "SELECT max(number) FROM information"
end

def get_data(db_file)
  DB.open "sqlite3://#{db_file}" do |db|
    db.exec "SELECT * FROM information"
  end
rescue err
  puts "Exception #{err}"
end

SQLite in memory

require "sqlite3"

DB.open "sqlite3://%3Amemory%3A" do |db|
  db.exec "CREATE TABLE data (
        id INTEGER PRIMARY KEY,
        name TEXT,
        yesno BOOLEAN,
        number INTEGER,
        start DATETIME
    )"
  db.exec "INSERT INTO data (name, yesno, number, start) VALUES (?, ?, ?, ?)",
    "Foo", true, 42, Time.utc
  name = db.scalar "SELECT name FROM data"
  puts name
  puts typeof(name)
  puts name.to_s + " and Bar"

  number = db.scalar "SELECT number FROM data"
  puts number
  puts typeof(number)
  puts number.to_s.to_i + 1

  db.exec "INSERT INTO data (name, yesno, number, start) VALUES (?, ?, ?, ?)",
    "Bar", false, 23, Time.utc
  name = db.scalar "SELECT name FROM data"
  puts name

  count = db.scalar "SELECT COUNT(*) FROM data"
  puts count
  puts typeof(count)

  puts db.scalar "SELECT name FROM sqlite_schema"
  db.query_all "SELECT name, type FROM sqlite_schema" do |line|
    puts line
    puts line.read
    puts line.read
  end
end

Time

Dates and Time

now = Time.utc
puts now
puts now.year
2021-06-11 10:06:26 UTC
2021

Sleep

  • sleep

  • Sleep in seconds.

  • It can get either and integer or a floating point number.

puts Time.monotonic
sleep 1.5
puts Time.monotonic
27.01:06:47.337685122
27.01:06:48.841641088

Time difference or Time::Span

time1 = Time.utc(2016, 2, 15, 10, 20, 30)
time2 = Time.utc(2016, 2, 16, 10, 20, 30)
elapsed = time2 - time1 # Time::Span
puts elapsed.days
puts elapsed.total_seconds
1
86400.0

Elapsed time

  • monotonic

  • seconds

  • total_seconds

  • total_milliseconds

  • microseconds

  • total_microseconds

  • sleep

  • Time::Span

t0 = Time.monotonic
sleep 1
t1 = Time.monotonic

elapsed = t1 - t0
puts elapsed
puts elapsed.seconds
puts elapsed.total_seconds
puts elapsed.total_milliseconds
puts elapsed.microseconds
puts elapsed.total_microseconds
00:00:01.004479308
1
1.004479308
1004.479308
4479
1004479.308

Timestamp formatting

  • to_s
time = Time.utc(2016, 2, 15, 10, 20, 30)
puts time.to_s("%Y-%m-%d %H:%M:%S %:z")
puts time.to_s("%Y-%m-%d")
2016-02-15 10:20:30 +00:00
2016-02-15

Add timespan

  • days
  • hours
  • minutes
  • seconds
now = Time.utc
tomorrow = now + Time::Span.new(days: 1)
puts now
puts tomorrow

puts now + 1.days
puts now + 24.hours
puts now + 1440.minutes
puts now + 86_400.seconds
2021-06-29 14:31:14 UTC
2021-06-30 14:31:14 UTC
2021-06-30 14:31:14 UTC
2021-06-30 14:31:14 UTC
2021-06-30 14:31:14 UTC
2021-06-30 14:31:14 UTC
  • See these methods at Int

Time Types

now = Time.utc
puts typeof(now)
puts now

monotonic = Time.monotonic
puts typeof(monotonic)
puts monotonic

timespan = Time::Span.new(days: 1)
puts typeof(timespan)
puts timespan
Time
2021-06-11 10:11:22 UTC
Time::Span
27.01:20:51.929165992
Time::Span
1.00:00:00

CLI

ARGV

  • ARGV

  • The raw values from the command line can be found in the ARGV array.

puts ARGV
puts ARGV.size
ARGV.each { |arg|
  puts arg
}

Usage statement and exit

if ARGV.size != 1
  puts "Usage: some_program FILENAME"
  exit 1
end

filename = ARGV[0]
puts filename

Command line Option Parser (argparse, GetOpts)

require "option_parser"

verbose = false
destination = "127.0.0.1"

OptionParser.parse do |parser|
  parser.banner = "Usage: cli_parser.cr [arguments]"
  parser.on("-v", "--verbose", "Verbose mode") { verbose = true }
  parser.on("-d DESTINATION", "--destinaton=DESTINATION", "Where shall we go?") { |name| destination = name }
  parser.on("-h", "--help", "Show this help") do
    puts parser
    exit
  end
  parser.invalid_option do |flag|
    STDERR.puts "ERROR: #{flag} is not a valid option."
    STDERR.puts parser
    exit(1)
  end
  parser.missing_option do |flag|
    STDERR.puts "ERROR: #{flag} requires a value"
    STDERR.puts parser
    exit(1)
  end
end

if verbose
  puts "Verbose mode"
end
puts "Destination: #{destination}"
  • cli_parser.cr -d code-maven.com -v
  • cli_parser.cr -d
  • cli_parser.cr -x

Pass parameters to OptionParser

require "option_parser"

def get_params(arguments)
  verbose = false
  destination = "127.0.0.1"

  OptionParser.parse(arguments) do |parser|
    parser.banner = "Usage: cli_parser.cr [arguments]"
    parser.on("-v", "--verbose", "Verbose mode") { verbose = true }
    parser.on("-d DESTINATION", "--destinaton=DESTINATION", "Where shall we go?") { |name| destination = name }
    parser.on("-h", "--help", "Show this help") do
      puts parser
      exit
    end
    parser.invalid_option do |flag|
      STDERR.puts "ERROR: #{flag} is not a valid option."
      STDERR.puts parser
      exit(1)
    end
    parser.missing_option do |flag|
      STDERR.puts "ERROR: #{flag} requires a value"
      STDERR.puts parser
      exit(1)
    end
  end
  return {verbose, destination}
end

verbose, destination = get_params([] of String)
puts verbose
puts destination

verbose, destination = get_params(["-v"])
puts verbose
puts destination

verbose, destination = get_params(["-v", "-d", "10.0.0.1"])
puts verbose
puts destination
false
127.0.0.1
true
127.0.0.1
true
10.0.0.1

Order of parsing

require "option_parser"

def get_params(arguments)
  OptionParser.parse(arguments) do |parser|
    parser.banner = "Usage: cli_parser.cr [arguments]"
    parser.on("-v", "--verbose", "Verbose mode") { puts "verbose" }
    parser.on("-d DESTINATION", "--destinaton=DESTINATION", "Where shall we go?") { puts "destination" }
    parser.on("-h", "--help", "Show this help") { puts "help" }
  end
  puts "---"
end

get_params([] of String)
get_params(["-v"])
get_params(["-v", "-d", "10.0.0.1"])
get_params(["-d", "10.0.0.1", "-v"])
---
verbose
---
verbose
destination
---
destination
verbose
---

HTTP Client

HTTP Client example

  • http/client
  • HTTP::Client
require "http/client"

url = "https://httpbin.org/get"
response = HTTP::Client.get(url)
puts response.status_code
puts response.body

# Setting some values in the header
url = "https://httpbin.org/get"
response = HTTP::Client.get(url, headers: HTTP::Headers{"Key" => "Value"})
puts response.status_code
puts response.body

Parse URL (URI)

  • URI
require "uri"

url = "https://github.com:443/szabgab/crystal-mine.cr?name=Foo"
uri = URI.parse url
puts uri.scheme
puts uri.host
puts uri.port
puts uri.path
puts uri.query

HTTP::Request GET

require "http"

req = HTTP::Request.new("GET", "https://code-maven.com/page?name=Foo&email=foo@bar.com")
p! req
p req.resource
p req.query_params
p req.body

HTTP::Request POST

require "http"

req = HTTP::Request.new("POST",
  "https://code-maven.com/page",
  body: "name=Foo&email=foo@bar.com",
  headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded"})
p! req
p req.body # IO::Memory

Process

Execute external program (system)

  • system
res = system("ls -l")
puts res

Execute external program (backtick)

result = `ls -l`
if $?.success?
  puts result
else
  puts "Failure"
end

Execute external program (Process)

process = Process.new("ls", ["-l", "-a"],
  output: Process::Redirect::Pipe,
  error: Process::Redirect::Pipe,
)

output = process.output.gets_to_end
error = process.error.gets_to_end

res = process.wait
if res.success?
  puts output
else
  puts error
end
puts res.exit_status # exit code

Execute external program (capture)

output, error, exit_code = capture("ls", ["-l", "-a"])
if exit_code == 0
  puts output
else
  puts error
end
puts exit_code

def capture(cmd, params)
  process = Process.new(cmd, params,
    output: Process::Redirect::Pipe,
    error: Process::Redirect::Pipe,
  )

  output = process.output.gets_to_end
  error = process.error.gets_to_end

  res = process.wait

  return output, error, res.exit_status
end

Execute external program (capture)

output, error, exit_code = capture("ls", ["-l", "-a"])
if exit_code == 0
  puts output
else
  puts error
end
puts exit_code

def capture(cmd, params)
  stdout = IO::Memory.new
  stderr = IO::Memory.new
  res = Process.new(cmd, params, output: stdout, error: stderr).wait

  return stdout.to_s, stderr.to_s, res.exit_status
end

Abort

  • abort

abort is the combination of printing to the STDERR and calling exit with an exit code.

abort("Something bad happened")
abort("Something bad happened", 3)
  • abort prints a message to STDERR and exit with exit code 1 (see $? or %ERROR_LEVEL%)
  • Optionally we can supply the exit code as well.

Concurrency

Send and receive

  • Channel
  • send
  • receive
  • spawn
puts "before"
ch = Channel(Int32).new

puts "before spawn"
spawn do
  puts "in spawn before send"
  ch.send 42
  puts "in spawn after send"
end

puts "before receive"
res = ch.receive
puts "received #{res}"

Concurrent HTTP request

require "http/client"
puts "before"
ch = Channel(HTTP::Client::Response).new

puts "before spawn"
spawn do
  puts "in spawn before send"
  res = HTTP::Client.get "https://code-maven.com/"
  ch.send res
  puts "in spawn after send"
end

puts "before receive"
res = ch.receive
puts "received #{res.body.size} bytes including this row: #{res.body.lines.select(/<title>/)}"

Kemal

About Kemal

  • Kemal
  • Created by Serdar Dogruyol

Kemal Install

Create a directory and create the following file in it:

{% embed include file="src/examples/kemal/shard.yml)

Run the following command in the directory

shards install

If it fails with Failed to resolve dependencies, try updating incompatible shards or use --ignore-crystal-version as a workaround if no update is available.

then try this:

shards install --ignore-crystal-version

Hello World

require "kemal"

get "/" do
  "Welcome to Kemal"
end

Kemal.run
crystal src/hello_world.cr
http://localhost:3000/

Testing Hello World

ENV["KEMAL_ENV"] = "test"
require "spec-kemal"
require "../src/hello_world"

describe "Web Application" do
  it "renders /" do
    get "/"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain("Welcome to Kemal")
  end
end
crystal spec/hello_world_spec.cr

Kemal Autorestart (autoreload)

crystal build --release lib/sentry/src/sentry_cli.cr -o ./bin/sentry
./bin/sentry -b "crystal build src/webapp.cr -o bin/webapp" -r bin/webapp
require "kemal"

get "/" do
  "Hello Changing World"
end

Kemal.run

Kemal GET parameters

  • GET
require "kemal"

get "/" do |env|
  response = ""
  text = env.params.query["text"]?
  if !text.nil?
    response = "You typed in <b>#{text}</b>"
  end
  %{
    <form method="GET" action="/">
    <input name="text">
    <input type="submit" value="Echo">
    </form>
    #{response}
  }
end

Kemal.run
ENV["KEMAL_ENV"] = "test"
require "spec-kemal"
require "../src/get_params"

describe "Web Application" do
  it "renders /" do
    get "/"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{<form method="GET" action="/">})
    response.body.should_not contain(%{You typed in <b>})
  end

  it "renders /?text=Foo Bar" do
    get "/?text=Foo Bar"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{<form method="GET" action="/">})
    response.body.should contain(%{You typed in <b>Foo Bar</b>})
  end
end
crystal spec/get_params_spec.cr

Kemal POST parameters

  • POST
require "kemal"

get "/" do
  form("")
end

post "/" do |env|
  response = ""
  text = env.params.body["text"]?
  if !text.nil?
    response = "You typed in <b>#{text}</b>"
  end
  form(response)
end

def form(response)
  %{
    <form method="POST" action="/">
    <input name="text">
    <input type="submit" value="Echo">
    </form>
    #{response}
  }
end

Kemal.run
ENV["KEMAL_ENV"] = "test"
require "spec-kemal"
require "../src/post_params"

describe "Web Application" do
  it "renders /" do
    get "/"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{<form method="POST" action="/">})
    response.body.should_not contain(%{You typed in <b>})
  end

  it "renders / POST text=Foo Bar" do
    post "/", body: "text=Foo Bar", headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded"}
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{<form method="POST" action="/">})
    response.body.should contain(%{You typed in <b>Foo Bar</b>})
  end
end
crystal spec/post_params_spec.cr

Kemal Route parameters

require "kemal"

get "/" do
  %{
    <a href="/user/foo">/user/foo</a><br>
    <a href="/user/bar/bados">/user/bar/bados</a><br>
    <a href="/user/a/b/c">/user/a/b/c</a><br>
    <a href="/path/a/b/c/d">/path/a/b/c/d</a></br>
  }
end

get "/user/:fname" do |env|
  fname = env.params.url["fname"]
  "received fname: #{fname}"
end

get "/user/:fname/:lname" do |env|
  fname = env.params.url["fname"]
  lname = env.params.url["lname"]
  "received fname: #{fname} and lname: #{lname}"
end

get "/path/*all" do |env|
  all = env.params.url["all"]
  "received #{all}"
end

Kemal.run
ENV["KEMAL_ENV"] = "test"
require "spec-kemal"
require "../src/route_params"

describe "Web Application" do
  it "renders /" do
    get "/"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{<a href="/user/foo">/user/foo</a><br>})
    response.body.should_not contain(%{received})
  end

  it "renders /user/one" do
    get "/user/one"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should_not contain(%{<a})
    response.body.should contain(%{received fname: one})
  end

  it "renders /user/one/two" do
    get "/user/one/two"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should_not contain(%{<a})
    response.body.should contain(%{received fname: one and lname: two})
  end

  it "renders /user/one/two/three" do
    get "/user/one/two/three"
    response.status_code.should eq 200 # TODO should be 404
    response.headers["Content-Type"].should eq "text/html"
    response.body.should eq "" # TODO: where is the page?
  end

  it "renders /path/1/2/3/4/5" do
    get "/path/1/2/3/4/5"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should eq "received 1/2/3/4/5"
  end
end
crystal spec/post_params_spec.cr

Kemal ECR Templates

require "kemal"

get "/" do |env|
  response = ""
  text = env.params.query["text"]?
  if !text.nil?
    response = text
  end
  render "src/views/page.ecr", "src/views/layouts/layout.ecr"
end

Kemal.run
<form method="GET" action="/">
<input name="text">
<input type="submit" value="Echo">
</form>
<% if response %>
   You typed in <b><%= response %></b>
<% end %>

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
  <title><%= title %></title>
</head>
<body>
  <%= content %>
</body>
</html>

Kemal with Jinja templates

require "kemal"
require "crinja"

get "/" do
  crinja = Crinja.new
  crinja.loader = Crinja::Loader::FileSystemLoader.new("src/views/")

  template = crinja.get_template("home.html.j2")
  template.render({
    "title"   => "With CRinja",
    "planets" => ["Mercury", "Venus", "Earth", "Mars"],
    "cond"    => Random.new.next_bool,
  })
end

Kemal.run
<title>{{ title }}</title>

<h1>{{ title }}</h1>

<ul>
{% for planet in planets %}
  <li>{{ planet }}</li>
{% endfor %}
</ul>

{% if cond %}
Condition is true
{% else %}
condition is false
{% endif %}

Kemal Elapsed time

require "kemal"

class HTTP::Server::Context
  getter start_time : Time::Span = Time.monotonic
end

get "/" do |env|
  # spend some time
  x = 0
  loop do
    x += 1
    break if x > 10_000_000
  end
  text = "This is some text"
  render "src/views/time.ecr", "src/views/layouts/layout_with_elapsed_time.ecr"
end

Kemal.run
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
  <title></title>
</head>
<body>
  <%= content %>
<hr>
<div>
Elapsed time <%= (Time.monotonic - env.start_time).total_milliseconds %> ms
</div>
</body>
</html>

<%= text %>

Accept GET, POST, and route parameter in the same POST route

require "kemal"

get "/" do
  form()
end

post "/user/:name" do |env|
  form(env.params.url["name"], env.params.query["email"]?, env.params.body["text"]?)
end

def form(route = "", get = "", post = "")
  %{
    <form method="POST" action="/user/foobar?email=foo@bar.com">
    <input name="text">
    <input type="submit" value="Echo">
    </form>
    <div>route: #{route}</div>
    <div>get: #{get}</div>
    <div>post: #{post}</div>
  }
end

Kemal.run
ENV["KEMAL_ENV"] = "test"
require "spec-kemal"
require "../src/params"

describe "Web Application" do
  it "renders /" do
    get "/"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{<form method="POST" action="/user/foobar?email=foo@bar.com">})
  end

  it "renders mixed POST request" do
    post "/user/foobar?email=foo@bar.com", body: "text=Foo Bar", headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded"}
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{<form method="POST" action="/user/foobar?email=foo@bar.com">})
    response.body.should contain(%{<div>route: foobar</div>})
    response.body.should contain(%{<div>get: foo@bar.com</div>})
    response.body.should contain(%{<div>post: Foo Bar</div>})
  end
end

Kemal indicate 404

require "kemal"

get "/" do
  %{
    <a href="/user/foo">/user/foo</a><br>
    <a href="/user/bar">/user/bar</a><br>
    <a href="/user/zorg">/user/zorg</a><br>
  }
end

DATABASE = Set{"foo", "bar"}

get "/user/:fname" do |env|
  fname = env.params.url["fname"]
  if !DATABASE.includes?(fname)
    halt env, status_code: 404, response: "We don't have this user <b>#{fname}</b>"
  end
  "received fname: #{fname}"
end

Kemal.run
ENV["KEMAL_ENV"] = "test"
require "spec-kemal"
require "../src/send_404"

describe "Web Application" do
  it "renders /" do
    get "/"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{<a href="/user/foo">/user/foo</a><br>})
    response.body.should_not contain(%{received})
  end

  it "renders /user/foo" do
    get "/user/foo"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{received fname: foo})
  end

  it "renders /user/bar" do
    get "/user/bar"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{received fname: bar})
  end

  it "renders /user/other" do
    get "/user/other"
    response.status_code.should eq 404
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{We don't have this user <b>other</b>})
  end
end

Kemal Styling 404 pages

require "kemal"

error 404 do
  "This is a customized 404 page."
end

get "/" do
  "Hello World"
end

Kemal.run
ENV["KEMAL_ENV"] = "test"
require "spec-kemal"
require "../src/customize_404"

describe "Web Application" do
  it "renders /" do
    get "/"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{Hello World})
  end

  it "renders /other" do
    get "/other"
    response.status_code.should eq 404
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{This is a customized 404 page.})
  end
end

Kemal set headers (change content-type)

require "kemal"

base_url = "https://code-maven.com"

get "/" do
  "Hello Kemal"
end

# source
get "/sitemap.xml" do |env|
  now = Time.utc
  now_str = now.to_s("%Y-%m-%d")
  env.response.content_type = "application/xml"
  xml = %{
     <?xml version="1.0" encoding="UTF-8"?>
     <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
     <url>
       <loc>#{base_url}/</loc>
       <lastmod>#{now_str}</lastmod>
     </url>
     </urlset>
  }
  xml
end

Kemal.run
ENV["KEMAL_ENV"] = "test"
require "spec-kemal"
require "../src/set_header"

describe "Web Application" do
  it "renders /" do
    get "/"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "text/html"
    response.body.should contain(%{Hello Kemal})
  end

  it "renders /sitemap.xml" do
    get "/sitemap.xml"
    response.status_code.should eq 200
    response.headers["Content-Type"].should eq "application/xml"
    response.body.should contain(%{<?xml version="1.0" encoding="UTF-8"?>})
  end
end

Kemal redirect

Kemal in Docker

Other

Sequence

def seq
  val = 0
  loop do
    val += 1
    yield val
  end
end

seq { |num|
  puts num
  break if num > 10
}
puts "after"

Check if variable is nil?

  • nil
  • nil?
x = "hello"
puts x.nil?
y = nil
puts y.nil?

Single quotes vs double quotes

  • Single quotes are for characters
  • Double quotes are for strings

No type-checking?

x = "one"
p! x
p! typeof(x)

x = 1
p! x
p! typeof(x)

{% embed include file="src/examples/other/assign_to_variable.out)

Divide by zero is Infinity

  • Infinity
puts divide(8, 2)
puts divide(8, 0)
puts divide(8, 4)

def divide(x, y)
  return x/y
end

Require other files

def hi(name)
  puts "Hello #{name}"
end
require "./welcome.cr"
hi("Foo")

List Methods

class Object
  macro methods
        {{ @type.methods.map &.name.stringify }}
    end
end

p! Bool.methods
p! String.methods
p! Int32.methods
p! Array.methods
p! Hash.methods
Bool.methods # => ["|", "&", "^", "hash", "to_unsafe", "to_s", "to_s", "clone", "==", "!="]
String.methods # => ["bytesize", "to_i", "to_i", "to_i?", "to_i8", "to_i8", "to_i8?", "to_u8", "to_u8", "to_u8?", "to_i16", "to_i16", "to_i16?", "to_u16", "to_u16", "to_u16?", "to_i32", "to_i32", "to_i32?", "to_u32", "to_u32", "to_u32?", "to_i64", "to_i64", "to_i64?", "to_u64", "to_u64", "to_u64?", "to_u64_info", "to_f", "to_f64", "to_f?", "to_f64?", "to_f32", "to_f32?", "to_f_impl", "[]", "[]", "[]", "[]", "[]", "[]", "[]?", "[]?", "[]?", "[]?", "[]?", "[]?", "char_at", "char_at", "delete_at", "delete_at", "delete_at", "byte_delete_at", "unicode_delete_at", "find_start_end_and_index", "byte_slice", "byte_slice", "byte_slice?", "codepoint_at", "byte_at", "byte_at", "byte_at?", "unsafe_byte_at", "downcase", "downcase", "upcase", "upcase", "capitalize", "capitalize", "titleize", "titleize", "chomp", "chomp", "chomp", "lchop", "lchop", "lchop?", "lchop?", "rchop", "rchop", "rchop?", "rchop?", "encode", "hexbytes", "hexbytes?", "insert", "insert", "insert_impl", "strip", "strip", "strip", "strip", "rstrip", "rstrip", "rstrip", "rstrip", "lstrip", "lstrip", "lstrip", "lstrip", "calc_excess_right", "calc_excess_right", "calc_excess_right", "calc_excess_right", "calc_excess_left", "calc_excess_left", "calc_excess_left", "calc_excess_left", "remove_excess", "remove_excess_right", "remove_excess_left", "tr", "sub", "sub", "sub", "sub", "sub", "sub", "sub", "sub", "sub", "sub", "sub", "sub", "sub_append", "sub_index", "sub_range", "has_back_references?", "scan_backreferences", "gsub", "gsub", "gsub", "gsub", "gsub", "gsub", "gsub", "gsub", "gsub", "gsub_ascii_char", "gsub_append", "count", "count", "count", "delete", "delete", "delete", "squeeze", "squeeze", "squeeze", "squeeze", "empty?", "blank?", "presence", "==", "<=>", "compare", "=~", "=~", "+", "+", "*", "index", "index", "index", "rindex", "rindex", "rindex", "partition", "partition", "rpartition", "rpartition", "byte_index", "byte_index", "char_index_to_byte_index", "byte_index_to_char_index", "includes?", "split", "split", "split", "split", "split", "split", "split", "split", "split_single_byte", "split_by_empty_separator", "lines", "each_line", "each_line", "underscore", "underscore", "camelcase", "camelcase", "reverse", "ljust", "ljust", "rjust", "rjust", "center", "center", "just", "succ", "match", "matches?", "scan", "scan", "scan", "scan", "each_char", "each_char", "each_char_with_index", "chars", "each_codepoint", "each_codepoint", "codepoints", "each_byte", "each_byte", "bytes", "pretty_print", "inspect", "inspect", "inspect_unquoted", "inspect_unquoted", "dump", "dump", "dump_unquoted", "dump_unquoted", "dump_or_inspect", "dump_or_inspect_unquoted", "inspect_char", "dump_char", "dump_or_inspect_char", "dump_hex", "dump_unicode", "starts_with?", "starts_with?", "starts_with?", "ends_with?", "ends_with?", "ends_with?", "%", "hash", "size", "ascii_only?", "single_byte_optimizable?", "valid_encoding?", "scrub", "char_bytesize_at", "size_known?", "each_byte_index_and_char_index", "clone", "dup", "to_s", "to_s", "to_slice", "to_unsafe", "unsafe_byte_slice", "unsafe_byte_slice", "unsafe_byte_slice_string", "unsafe_byte_slice_string", "check_no_null_byte", "to_utf16"]
Int32.methods # => ["/", "/", "/", "/", "/", "/", "/", "/", "/", "/", "/", "/", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "popcount", "leading_zeros_count", "trailing_zeros_count", "clone", "to_i", "to_i!", "to_u", "to_u!", "to_f", "to_f!", "to_i8", "to_i8!", "to_i16", "to_i16!", "to_i32", "to_i32!", "to_i64", "to_i64!", "to_i128", "to_i128!", "to_u8", "to_u8!", "to_u16", "to_u16!", "to_u32", "to_u32!", "to_u64", "to_u64!", "to_u128", "to_u128!", "to_f32", "to_f32!", "to_f64", "to_f64!", "==", "==", "==", "==", "==", "==", "==", "==", "==", "==", "==", "==", "!=", "!=", "!=", "!=", "!=", "!=", "!=", "!=", "!=", "!=", "!=", "!=", "<", "<", "<", "<", "<", "<", "<", "<", "<", "<", "<", "<", "<=", "<=", "<=", "<=", "<=", "<=", "<=", "<=", "<=", "<=", "<=", "<=", ">", ">", ">", ">", ">", ">", ">", ">", ">", ">", ">", ">", ">=", ">=", ">=", ">=", ">=", ">=", ">=", ">=", ">=", ">=", ">=", ">=", "unsafe_chr", "+", "+", "+", "+", "+", "+", "+", "+", "+", "+", "+", "+", "&+", "&+", "&+", "&+", "&+", "&+", "&+", "&+", "&+", "&+", "&-", "&-", "&-", "&-", "&-", "&-", "&-", "&-", "&-", "&-", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "&*", "&*", "&*", "&*", "&*", "&*", "&*", "&*", "&*", "&*", "|", "|", "|", "|", "|", "|", "|", "|", "|", "|", "&", "&", "&", "&", "&", "&", "&", "&", "&", "&", "^", "^", "^", "^", "^", "^", "^", "^", "^", "^", "unsafe_shl", "unsafe_shl", "unsafe_shl", "unsafe_shl", "unsafe_shl", "unsafe_shl", "unsafe_shl", "unsafe_shl", "unsafe_shl", "unsafe_shl", "unsafe_shr", "unsafe_shr", "unsafe_shr", "unsafe_shr", "unsafe_shr", "unsafe_shr", "unsafe_shr", "unsafe_shr", "unsafe_shr", "unsafe_shr", "unsafe_div", "unsafe_div", "unsafe_div", "unsafe_div", "unsafe_div", "unsafe_div", "unsafe_div", "unsafe_div", "unsafe_div", "unsafe_div", "unsafe_mod", "unsafe_mod", "unsafe_mod", "unsafe_mod", "unsafe_mod", "unsafe_mod", "unsafe_mod", "unsafe_mod", "unsafe_mod", "unsafe_mod"]
Array.methods # => ["initialize", "initialize", "initialize", "size", "==", "==", "<=>", "&", "|", "+", "-", "*", "<<", "[]=", "[]=", "[]=", "[]=", "[]=", "[]", "[]", "[]?", "[]?", "unsafe_fetch", "clear", "clone", "compact", "compact!", "concat", "concat", "delete", "delete_at", "delete_at", "delete_at", "dup", "fill", "fill", "fill", "fill", "fill", "fill", "fill", "fill", "first", "insert", "inspect", "last", "size=", "map", "map!", "select!", "select!", "reject!", "reject!", "internal_delete", "map_with_index", "map_with_index!", "skip", "flatten", "repeated_permutations", "each_repeated_permutation", "pop", "pop", "pop", "pop?", "product", "product", "push", "push", "replace", "reverse", "reverse!", "rotate!", "rotate", "shift", "shift", "shift", "shift_when_not_empty", "shift?", "shuffle", "shuffle!", "sort", "sort", "sort!", "sort!", "sort_by", "sort_by!", "swap", "to_a", "to_s", "pretty_print", "to_unsafe", "transpose", "uniq", "uniq", "uniq!", "uniq!", "unshift", "unshift", "update", "check_needs_resize", "needs_resize?", "remaining_capacity", "double_capacity", "resize_to_capacity", "double_capacity_for_unshift", "resize_to_capacity_for_unshift", "resize_if_cant_insert", "root_buffer", "shift_buffer_by", "reset_buffer_to_root_buffer", "to_lookup_hash", "to_lookup_hash", "index"]
Hash.methods # => ["initialize", "initialize", "upsert", "update_linear_scan", "delete_impl", "delete_linear_scan", "find_entry", "find_entry_linear_scan", "resize", "do_compaction", "double_indices_size", "clear_impl", "initialize_dup", "initialize_clone", "initialize_compare_by_identity", "initialize_dup_entries", "initialize_clone_entries", "initialize_copy_non_entries_vars", "get_index", "set_index", "indices_size", "compute_indices_bytesize", "malloc_indices", "indices_malloc_size", "realloc_indices", "clear_indices", "get_entry", "set_entry", "add_entry_and_increment_size", "delete_entry", "delete_entry_and_update_counts", "entries_full?", "each_entry_with_index", "malloc_entries", "realloc_entries", "clear_entries", "next_index", "fit_in_indices", "first_entry?", "last_entry?", "entries", "entries_size", "entries_capacity", "key_hash", "entry_matches?", "entry_matches?", "size", "compare_by_identity", "compare_by_identity?", "[]=", "put", "[]", "[]?", "dig?", "dig?", "dig", "dig", "has_key?", "has_value?", "fetch", "fetch", "values_at", "key_for", "key_for", "key_for?", "delete", "delete", "empty?", "each", "each", "each_key", "each_key", "each_value", "each_value", "keys", "values", "merge", "merge", "merge!", "merge!", "select", "select", "select", "select!", "select!", "select!", "reject", "reject", "reject!", "reject!", "reject!", "compact", "compact!", "transform_keys", "transform_values", "transform_values!", "first_key", "first_key?", "first_value", "first_value?", "last_key", "last_key?", "last_value", "last_value?", "shift", "shift", "shift?", "clear", "==", "hash", "dup", "clone", "inspect", "to_s", "pretty_print", "to_a", "to_a_impl", "to_h", "rehash", "invert"]

Checking the slides

require "option_parser"

# Extract the list of imported files
# Verify that all the files are indeed imported

# Optionally verify that the same file is not imported more than once.

class Options
  class_property verbose = false
end

def main
  get_options()
  md_files = get_md_files(Dir.current)
  if Options.verbose
    puts "MD files:\n"
    md_files.each { |path|
      puts path
    }
  end
  imported_files, errors = get_imported_files(md_files)
  # imported_files.each {|file|
  #     puts file
  # }
  files = get_files(Dir.current)
  # files.each {|name|
  #     puts name
  # }
  imported_files.each { |name|
    if files.includes?(name)
      files.delete name
    else
      puts "ERROR #{name} is imported but does not exist"
      errors = true
    end
  }
  files.each { |name|
    puts "ERROR #{name} is not imported"
    errors = true
  }

  if errors
    exit(1)
  end
end

def get_options
  OptionParser.parse do |parser|
    parser.banner = "Usage: check_slides.cr [arguments]"
    parser.on("-v", "--verbose", "Verbose mode") { Options.verbose = true }
    parser.on("-h", "--help", "Show this help") do
      puts parser
      exit
    end
    parser.invalid_option do |flag|
      STDERR.puts "ERROR: #{flag} is not a valid option."
      STDERR.puts parser
      exit(1)
    end
    parser.missing_option do |flag|
      STDERR.puts "ERROR: #{flag} requires a value"
      STDERR.puts parser
      exit(1)
    end
  end
end

def get_imported_files(md_files)
  imported_files = Set(String).new
  errors = false
  md_files.each { |path|
    lines = File.read_lines(path)
    lines.each { |line|
      match = /^!\[\]\((.*)\)\s*$/.match(line)
      if match
        if !imported_files.add?(match.[1])
          puts "WARN #{match.[1]} was imported twice"
          errors = true
        end
      end
    }
  }
  return imported_files, errors
end

def get_files(root)
  skip = Set{"Dockerfile", "shard.lock", "crystal.json", "examples/kemal/shard.lock"}
  size = root.size
  files = [] of String
  all_files = Dir.glob("#{root}/**/*")
  all_files.each { |path|
    if !File.file?(path)
      next
    end
    short_name = path[size + 1..]
    if short_name.starts_with?("bin/") || short_name.starts_with?("lib/")
      next
    end
    if short_name.ends_with?(".md")
      next
    end
    if skip.any?(short_name)
      next
    end
    files.push(short_name)
  }
  files.push(".ameba.yml")

  return files
end

def get_md_files(root)
  all_md_files = Dir.glob("#{root}/*.md")
  md_files = [] of String

  all_md_files.each { |path|
    # TODO use some filter?
    if path.ends_with?("README.md")
      next
    end
    md_files.push(path)
  }
  return md_files
end

# path = ARGV[0]

# dr = Dir.glob("#{path}/**/*")
# dr.each {|name| puts name}

main()

Crystal mine

# CI systems: Travis-CI / GitHub Actions
require "http/client"
require "json"

main

def main
  token = read_config
  # get_page(token)
  search(token)
end

def read_config
  config_file = "config.txt"
  line = File.read_lines(config_file).first
  # puts line
  return line
end

def get_page(token)
  url = "https://api.github.com/users/szabgab"
  response = HTTP::Client.get(url, headers: HTTP::Headers{"AUthentication" => "token #{token}"})

  puts response.status_code
  puts response.body
end

def search(token)
  per_page = 3 # max is 100
  query = "language:crystal"
  page = 1
  sort = "updated" # stars, forks, help-wanted-issues, updated
  order = "desc"
  url = "https://api.github.com/search/repositories?q=#{query}&per_page=#{per_page}&page=#{page}&sort=#{sort}&order=#{order}"
  response = HTTP::Client.get(url, headers: HTTP::Headers{"AUthentication" => "token #{token}"})
  # puts typeof(response) # HTTP::Client::Response
  # puts typeof(response.body) # string
  # data = Hash(String, Any).from_json(response.body)
  json_text = response.body
  puts json_text
  # data = Hash(String, Bool | Array(Hash) | Int32 | Nil).from_json(json_text)
  # puts typeof(data)
  # puts data.keys

  # puts response.status_code
  # puts response.body
end

Ameba Linter

Style/RedundantReturn:
  Enabled: false
Layout/TrailingBlankLines:
  Enabled: false
Lint/UselessAssign:
  Excluded:
  - examples/files/append_to_file_bug.cr
  - examples/functions/implicit.cr
  - examples/kemal/src/elapsed_time.cr
  - examples/kemal/src/ecr_template.cr
Excluded:
  - examples/kemal/lib/
Lint/UnusedArgument:
  Excluded:
  - examples/kemal/src/elapsed_time.cr
shards install
./bin/ameba

Gravatar

  • Digest::MD5.hexdigest
require "digest/md5"

if ARGV.size != 1
  puts "Need EMAIL"
  exit 1
end

email = ARGV[0]
code = gravatar(email)
puts "https://www.gravatar.com/avatar/#{code}?s=100&d=blank"

def gravatar(email)
  return Digest::MD5.hexdigest(email)
end

Try Crystal

# content = 1
# {% if content == 1 %}
# def f
#   puts "one"
# end
# {% else %}
# def f
#   puts "else"
# end
# {% end %}

# joe = {} of String => Int32 | String
# joe["name"] = "Joe"
# joe["number"] = 23
# # joe["float"] = 2.3
# # compile time error
# # Error: no overload matches 'Hash(String, Int32 | String)#[]=' with types String, Float64
# puts typeof(joe)

# #alias Some = {} of String => Int32 | String

# numbers = [] of Int32
# numbers.push(23)
# # numbers.push("text")
# # Error: no overload matches 'Array(Int32)#push' with type String
# # numbers.push(nil)
# # Error: no overload matches 'Array(Int32)#push' with type Nil
# puts typeof(numbers)

# alias Int32orNil = Int32|Nil
# # nilnum = [] of Int32 | Nil
# nilnum = [] of Int32orNil
# nilnum.push(23)
# nilnum.push(nil)
# puts typeof(nilnum)
# #num = Int32
# #num = 23
# #num = nil

# # class Person
# #     name: {type: String}
# #     number: {type: Int32}
# # end

# #alias Person = {name: String, number: Int32}
# alias Person = NamedTuple(name: String, number: Int32)

# people = [] of Person
# puts typeof(people)
# people.push({
#     "name": "Foo Bar",
#     "number": 42
# })

# puts people[0]["name"]
# # people[0]["name"] = "New Name"
# # Error: undefined method '[]=' for NamedTuple(name: String, number: Int32)

STDERR, STDOUT

  • STDERR
  • STDOUT
puts "Goes to STDOUT"
STDERR.puts "Goes to STDERR"
  • Redirection on the command line using > out and 2> err

Return Boolean

def is_odd(n : Int32)
  return n % 2 == 1
end

puts is_odd(3)
puts is_odd(4)

def is_this_odd?(n : Int32)
  return n % 2 == 1
end

puts is_this_odd?(3)
puts is_this_odd?(4)

Symbols

# :pi = 3.14
values = {
  "foo" => 1,
  "bar" => 2,
  "baz" => 3,
}

# puts :pi
names = %i(foo bar baz)
puts names
puts names[0]
puts typeof(names)
puts typeof(names[0])

puts values
# names.each {|name|
#    puts name
#    values[name]
# }

h = {
  "fname" => "Foo",
}
puts h
puts typeof(h)
h["lname"] = "Bar"
puts h
# puts h[:lname]
# Missing hash key: :lname (KeyError)

# g and i are the same
i = {
  :fname => "Foo",
}
puts i
puts typeof(i)
# i["fname"]
puts i[:fname]

g = {
  fname: "Foo", # Symbol
}
puts g[:fname]

Docspec

name: Anagram
version: 0.0.1

authors:
   - Gabor Szabo <gabor@szabgab.com>

development_dependencies:
  docspec:
    github: skippi/docspec

license: MIT
require "spec"
require "../src/app"
describe "Try" do
  it "anagram" do
    anagram?("abc", "cba").should be_true
    anagram?("abc", "abd").should be_false
  end
end
require "docspec"

Docspec.doctest("../src/app.cr")
# ```
# >> anagram?("abc", "bca") # => true
# >> anagram?("abc", "abd") # => false
# ```
def anagram?(x, y)
  return x.split("").sort == y.split("").sort
end

require

require - imports are global. This makes it hard to see in a file where some objects might come from as they might have been required by some other file.

Similarly requiring a bunch of files in a directory is easy to do, but might make it a bit harder for someone without an IDE to find the source of each object.

require "./directory/file"  # relative path to the cr file
require "./directory/*"     # all the cr files in the directory
require "./directory/**"    # all the cr files in the directory - recursively
require "some_name"         # Find it somewhere (standard library, src directory)
  • You can put the require statements anywhere but you might want to be consistent in your project.
  • Make sure you don't have circular requires.

Constants

  • Variable names that start with upper-case letter are constants
PI = 3.14

puts PI
PI = 3.145 # Error: already initialized constant PI

Multiple assignment

a, b = 2, 3
puts a # 2
puts b # 3

a, b = b, a
puts a # 3
puts b # 2

Chained assignment

a = b = 42
puts a # 42
puts b # 42

Int methods: Times

3.times do |i|
  puts i
end

Int64 Zero

zero = 0_i64

p! zero
p! typeof(zero)

Question mark: ?

  • ?

  • meaning "or nil" in type definitions String? is the same as String | Nil

  • Methods ending with ? usually return a boolean (true, false) - there is no enforcement of this in Crystal

  • If a construct might raise an exception adding a question mark can convert that into returning nil

  • It is also part of the conditional operator ?:

[0, 23, nil].each { |value|
  puts value.nil?
  z = value || 42
  puts z
}

names = ["Foo", "Bar"]
puts names[1]

# puts names[2]
# Unhandled exception: Index out of bounds (IndexError)

z = names[2]? # nil
puts z.nil?   # true

person = {
  "name"  => "Foo",
  "email" => "foo@bar.com",
}
puts person
puts person["name"]
# puts person["age"]
# Unhandled exception: Missing hash key: "age" (KeyError)
z = person["age"]?
puts z.nil? # true

Exclamation mark: !

  • !

  • Methods ending with ! usually modify the underlying object.

  • Logical not (before an expression)

Ampersand: &

STDIN don't accept nil

  • gets will retun nil if we press Ctrl-D
  • gets.not_nil! will raise an exception
# x = gets
# puts x.nil?

x = gets.not_nil!
puts x.nil?

Math

puts Math.sqrt(4) # square root
puts Math.cbrt(8) # cube root
puts 4 ** 0.5
puts 8 ** (1/3)
puts Math.hypot(3, 4) # hypotenuse (Pythagoras)

puts Math.max(3, 10) # for only 2 values
puts Math.min(3, 10) # for only 2 values

Proc

square = ->(x : Int32) { x * x }
puts typeof(square) # Proc(Int32, Int32)
puts square.call(3) # 9

Enums

  • enum
enum Direction : UInt8
  Left
  Right
  Up
  Down

  def left?
    self == Direction::Left
  end
end

puts Direction::Left
puts Direction::Left.value
puts Direction::Right
dir = Direction::Left
puts dir.left?

dir = Direction::Right
puts dir.left?

Type of array elements

values = [1, 2, "three", "four"]
puts typeof(values)
puts typeof(values[0])
puts typeof(values[2])
puts ""

values.each { |val|
  puts typeof(val)
  # puts val.size
  # Error: undefined method 'size' for Int32 (compile-time type is (Int32 | String))

  if val.is_a?(String)
    puts val.size
  end
}

All the elements of an array "inherit" the type from the array

Environment variables

  • ENV
ENV.keys.sort!.each { |key|
  puts "%-25s %s" % {key, ENV[key]}
}

Int32 or Nil

text = "Black cat"
idx = text.index("cat")
puts typeof(idx) # (Int32 | Nil)

# puts text[idx]
# Error: no overload matches 'String#[]' with type (Int32 | Nil)

if idx.is_a?(Int32)
  puts typeof(idx) # Int32
  puts text[idx]
end

Resources

struct Person
  getter first : String
  getter family : String?

  def initialize(@first)
  end

  def initialize(@first, @family)
  end
end

if Random.rand < 0.5
  prs = Person.new("Foo")
else
  prs = Person.new("Foo", "Bar")
end

family = prs.family
if family.nil?
  puts "nil"
else
  puts "not nil"
  puts family.size
end

if family = prs.family
  puts "not nil"
  puts family.size
else
  puts "nil"
end
if Random.rand < 0.5
  x = "abc"
else
  x = nil
end

if x.nil?
  puts "nil"
else
  puts x
  puts x.size
end
if Random.rand < 0.5
  prs = {
    "name"  => "Foo",
    "title" => "Manager",
  }
else
  prs = {
    "name"  => "Foo",
    "title" => nil,
  }
end

p! prs

title = prs["title"]
if title.nil?
  puts "nil"
else
  puts "not nil"
  puts title.size
end

if title = prs["title"]
  puts "not nil"
  puts title.size
else
  puts "nil"
end
struct Person
  getter first : String
  getter family : String?

  def initialize(@first)
  end

  def initialize(@first, @family)
  end
end

if Random.rand < 0.5
  prs = Person.new("Foo")
else
  prs = Person.new("Foo", "Bar")
end

if prs.family.nil?
  puts "nil"
else
  puts "not nil"
  puts prs.family.size
end
struct Dog
  getter name : String
  getter owner : String

  def initialize(@name, @owner)
  end
end

struct Cat
  getter name : String
  getter staff : String

  def initialize(@name, @staff)
  end
end

if Random.rand < 0.5
  animal = Dog.new("Gin", "Foo")
else
  animal = Cat.new("Tonic", "Bar")
end

p! animal
p! animal.class

# if animal.class == Cat
#  p! animal.staff
# else
#  p! animal.owner
# end

if animal.is_a?(Cat)
  p! animal.staff
else
  p! animal.owner
end

case animal
when Cat
  puts "cat"
  p! animal.staff
when Dog
  puts "dog"
  p! animal.owner
else
  puts "other"
end
if Random.rand < 0.5
  prs = {
    "name"  => "Foo",
    "title" => "Manager",
  }
else
  prs = {
    "name"  => "Foo",
    "title" => nil,
  }
end

p! prs

if prs["title"].nil?
  puts "nil"
else
  puts "not nil"
  puts prs["title"].size
end
puts "Hello Crystal!"

class Hello
  puts "Hello from class"
end

struct My
  puts "Hello from struct"
end

Crystal from 0 to Web site

Crystal from 0 to Web site

  • Crystal 1.0 Conference
  • 2021.07.08
  • by Gábor Szabó
  • https://szabgab.com/
  • https://code-maven.com/crystal
  • @szabgab

Background

  • Programming 40 years.

  • Perl 25 years.

  • Python 10 years.

  • Crystal 30 days.

  • Teaching programming for 20 years.

  • Test automation / CI / DevOps

Actually it is already almost 60 days

Why?

This site listing the shards are very plain, only a thin wrapper around GitHub API I can do a much better one analyzing shards and displaying information.

I can do better

How to learn

Lots of small examples, questions arise, make me learn more.

When

  • Sat May 15 13:48:30 2021 +0300 - first commit to slides

  • Wed May 19 07:29:28 2021 +0300 - first commit to Crystal Mine

  • Wed Jun 9 03:03:02 2021 +0300 - posted on the Crystal forum

Shardbox

  • 4 hours later Johannes Müller pointed to Shardbox

What did I learn?

  • Crystal
  • Web development with Crystal

Crystal is fun

  • Nice syntax
  • "Make the easy things easy, hards things possible"
  • Tim Toady - TMTOWTDI - "There's more than one way to do it"
  • Lots of methods
  • Not a small language

Shards

  • Easy to install (shards install).
  • Few of them. Many common libraries (shards) are missing - harder to develop and many opportunities.
  • Discover-ability (There is no shards search, there is no central database of shards).
  • "Failed to resolve dependencies, try updating incompatible shards or use --ignore-crystal-version as a workaround if no update is available."

Code formatter

crystal tool format

Ameba - Linter

Spectator - testing framework

Code coverage

CI - GitHub Actions

The language

What do ? and ! mean?!

  • What does ? mean
  • What does ! mean
  • What if they are both: !var.empty?

What to ? and ! mean at the end of the functions and sometimes at the end of various statements? What does !something.empty? and why don't you write ¡empty! anyway? That would at least make sense...

Emojis and Unicode characters 💎

  • Why are we not using ¡ and ¿

Meaning of ?

  • var.nil? - true/false

  • array[42]? - nil instead of exception

  • String? - String | Nil

  • But answer? can return 42

Meaning of !

  • There is p.

  • There is p! that is like p, but more so.

  • Hash#reject - returns a hash without some elements

  • Hash#reject! - does it in-place

  • Hash#delete - removes an element in-place

It seems there is some inconsistency here.

Web framework?

Amber Framework

  • Complex installation (starts by installing Crystal?)
curl -L https://github.com/amberframework/amber/archive/stable.tar.gz | tar xz
cd amber-stable
shards install
make install
  • It wants to install in /usr/local/bin/amber why?
  • There is some talk about docker-compose...

Lucky Framework

  • Installation is complex
  • Needs PostgreSQL on my development machine?
  • A docker-compose up would be great.

Kemal

  • The simplest to get started

  • Might not be as powerful as the others

  • Most popular of the 3

  • The need to recompile the whole thing before I run it and the fact that compilation is slow makes life of a (web) developer hard.

name: Kemal Demo
version: 0.0.1

authors:
   - Gabor Szabo <gabor@szabgab.com>

description: |
  Showing some code with Kemal

dependencies:
  kemal:
    github: kemalcr/kemal
  spec-kemal:
    github: kemalcr/spec-kemal
  crinja:
    github: straight-shoota/crinja

development_dependencies:
  ameba:
    github: crystal-ameba/ameba
  sentry:
    github: samueleaton/sentry

license: MIT

Crinja

  • The Jinja Template system of Johannes Müller
  • No need for compilation - makes the HTML development much faster.

Shardbox

  • Shardbox

  • Kemal

  • Crinja (templates)

  • PostgreSQL

Future

Thank you - QA ?!?!

  • Thank you - QA
  • https://szabgab.com/
  • https://code-maven.com/crystal
  • Discord
  • @szabgab

Macros

Macro - increment x by 1

  • macro
macro add_one_to_x
  x += 1
end

x = 1
puts x
add_one_to_x
add_one_to_x
add_one_to_x
puts x

Macro - with parameter and placeholder

macro add_one_to(name)
  {{name}} += 1
end

x = 1
y = 1
puts x
puts y
add_one_to(x)
add_one_to(x)
add_one_to(x)
add_one_to(y)
puts x
puts y

Macro Swap

macro swap(a, b)
  temp = {{a}}
  {{a}} = {{b}}
  {{b}} = temp
end

temp = "z"
x = "x"
y = "y"
swap(x, y)
p! x
p! y
p! temp
x # => "y"
y # => "x"
temp # => "x"

Macro - internal variables - Swap fixed

macro swap(a, b)
  %temp = {{a}}
  {{a}} = {{b}}
  {{b}} = %temp
end

temp = "z"
x = "x"
y = "y"
swap(x, y)
p! x
p! y
p! temp
x # => "y"
y # => "x"
temp # => "z"

Macros function

macro define_method(name, content)
  puts "before"
  def {{name}}
    puts {{content}}
  end
  puts "after"
end

define_method foo, 1

puts "before foo"
foo # => 1
puts "after foo"

Macro to print type and content

macro t(name)
  print "%s %s\n" % {typeof({{name}}), {{name}}}
end

h = {"a" => 1, "b-1" => 2}
t h