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
-
Use
shards init
to create a file calledshard.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 parametersputs
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
name = "Foo Bar"
puts "Hello #{name}!"
Hello Foo Bar!
Interpolation
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!
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
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
-
abs
-
round
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 fileother
.
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 "" aretrue
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
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
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 = 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 acceptingnil
as well. -
Int32?
is the same asInt32 | ::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 havingyield
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 everyyield
.
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
-
JSON::Serializable
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
-
include
-
JSON::PullParser
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
-
to_json
-
from_json
-
JSON::Serializable
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
-
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
-
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
-
7 levels of logging, default is
info
and higher to theSTDOUT
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
-
Time
-
Time::Span
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
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)
-
option_parser
-
OptionParser
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)
-
`
-
$?
-
success?
result = `ls -l`
if $?.success?
puts result
else
puts "Failure"
end
Execute external program (Process)
-
exit
-
exit_code
-
exit_status
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 asString | 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 retunnil
if we press Ctrl-Dgets.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
-
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
-
Crystal Programming by Derek Banas from 2018
-
Crystal: Fast as C, Slick as Ruby by Tom Richards from Delegator form 2017
-
Crystal Weekly by Serdar Doğruyol
-
Friends of Crystal by Serdar Doğruyol
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?
- module counts
- 3rd party registry
- Listing of shards crystalshards.xyz (thin wrapper around the API of GitHub)
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
- Developing an application
- Prepare slides for a training course
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
- Spectator (similar to RSpec of Ruby)
Code coverage
CI - GitHub Actions
- GitHub Actions configurator
- Smoking Crystal and the shards
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 likep
, 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?
-
Are there any frameworks?
-
Which framework to use?
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
-
Kemal
-
Crinja (templates)
-
PostgreSQL
Future
-
Contribute to Shardbox
-
Contribute to shards. (tests, CI)
-
Pair programming sessions on Crystal projects
-
Crystal book (based on the course)
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