Methods
Methods (functions) are an important building block. Some methods are available on
the top-level scope, few common examples are typeof, puts, gets. Whereas other
methods are available on certain objects, such as the size method which is available
on the String object.
A simple method in Crystal looks like this:
def double(num)
num * 2
end
p double 6 # => 12
Note that, methods return their last expression implicitly. Though you can use return
keyword explicitly.
What happens if we call our double method on a String?
p double "66" # => "6666"
Because the multiplication operator works on the String type as well, we get "6666" as a result.
That's where static typing and explicitly restricting types come into play:
def triple(num: Int32)
num * 3
end
p triple 2 # => 6
p triple "2" # => Error: No overload matches triple with type string
It's possible to be flexible and overload a method to accept another type parameter:
def triple(str : String)
str * 3
p "You just tripled a string. Weird."
end
Default values
You can define default values for parameters of your methods:
def random_number(base, max =10)
base + rand(0..max)
end
p random_number 5 # => Random number between 5 to 15
p random_number 5, 5 # => Random number between 5 to 10
Note that the parameter that has a default value has become an optional argument for the caller site.
Named parameters
You can explicitly use named parameters on the caller site:
p random_number 5, max: 5
p random_number base: 5, max: 5
It is also possible to force the use of named parameters on the caller site. In the following example, all parameters on the right side of * will have to be named parameters:
def greeter(*, first_name, last_name, emphatic = false)
if emphatic
p "Greetings #{first_name} #{last_name}!!!!"
else
p "Greetings #{first_name} #{last_name}."
end
end
greeter first_name: "John", last_name: "Doe" # => Greetings, John Doe
greeter first_name: "John", last_name: "Doe", emphatic: true # => Greetings, John Doe!!!!
Naming parameters externally/internally
See the following example:
def multiply(value, *, by num)
value * num
end
p multiply(3, by: 5)
Let's inspect each parameter of the method multiply:
valueis a regular positional parameter, caller provides a value.*indicates all the following parameters will have to be called by their name.by numrepresent the same parameter,byis inteded for the external use,numis intended for internal use.
Passing blocks to methods
You can pass blocks of code to a method, and these blocks can be invoked within the
method with the use of the yield keyword.
See the example:
def perform_op
p "We're in the method perform_op"
yield
p "We've executed the passed code block"
yield
p "We've executed the passed code block again"
p "Leaving perform_op method scope"
end
# You can pass a code block in two ways:
perform_op do
puts "this is the passed code block"
end
perform_op {
puts "this is the passed code block"
}
Passing blocks with context (arguments and return values)
def perform_upcase(str)
str = str.upcase
yield str
end
def perform_exclaim(str)
str += "!!!!!"
end
sample = "hello john"
result = perform_upcase sample do |s|
perform_exclaim s
end
Using next keyword within a passed code block
You can use the next keyword to stop the execution of the passed
code block, and return a value to the yield statement that invoked it.
If a value is passed to the next keyword, yield will receive it.
def greeting_generator
person_one = yield "John"
person_two = yield "Jane"
person_three = yield "Paige"
"#{person_one}, #{person_two}, #{person_three}"
end
# Let's pass a code block to get a different type of greeting for John
result = greeting_generator do |prs|
if prs == "John"
next "Greetings #{prs}"
end
"Hi #{prs}"
end
p result # => Greetings John, Hi Jane, Hi Paige
Using the break keyword within a passed code block
You can use the break keyword to stop the method that is invoking
the inner block, similar to encountering a return.
def greeting_generator
person_one = yield "John"
person_two = yield "Jane"
person_three = yield "Paige"
"#{person_one} , #{person_two}, #{person_three}"
end
result = greeting_generator do |prs|
if prs == "John"
# This will be returned on break, and the inner method will stop.
break "Greetings #{prs}"
end
"Hi #{prs}"
end
p result # => Greetings John
Using a return statement within a passed code block
The return statement will stop the execution of the passed code block similar to break,
however it will also return from the method where the passed code block was written.
Tip: Return always finalizes the execution of the method that it resides in.
See the following example:
def greeting_generator
person_one = yield "John"
person_two = yield "Jane"
person_three = yield "Paige"
"#{person_one} , #{person_two}, #{person_three}"
end
def run
greeting_generator do |prs|
if prs == "John"
return "Greetings #{prs}" # => The method `run` will also exit at this point.
end
"Hi #{prs}"
end
# -> if there was a statement here, this would not be executed due to return.
end
p run # => "Greetings John"
Using Splat parameters
It's possible to define a method that accepts an arbitrary number of
arguments by prefixing the argument with the * symbol:
def greet(*people)
p "We will greet #{people}" # => We will greet {{\"John\", \"Jane\", \"Paige\", \"Herbert\"}}"
p typeof(people) # => Tuple(String, String, String, String)
people.each do |prs|
p "Hi, #{prs}!"
end
end
greet("John", "Jane", "Paige", "Herbert")
# =>
# "Hi, John"
# "Hi, Jane"
# "Hi, Paige"
# "Hi, Herbert"
As you can see in the type check of the splat parameters, it's a tuple. Splat parameters are alwoys referred to as a Tuple of zero or more arguments.