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:

  1. value is a regular positional parameter, caller provides a value.
  2. * indicates all the following parameters will have to be called by their name.
  3. by num represent the same parameter, by is inteded for the external use, num is 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.