Felicitas Pojtinger (fp036)
2022-10-24
These study materials are heavily based on professor Ihler’s “Aktuelle Programmiersprachen” lecture at HdM Stuttgart.
Found an error or have a suggestion? Please open an issue on GitHub (github.com/pojntfx/uni-programminglanguages-notes):
If you like the study materials, a GitHub star is always appreciated :)
Uni Programming Languages Notes (c) 2022 Felicitas Pojtinger and contributors
SPDX-License-Identifier: AGPL-3.0
5.times { print "We *love* Ruby -- it's outrageous!" }
)@
, globals with $
etc.)Typical logical operators:
Comparisons are type checked:
Trip equals can be used to check if if an instance belongs to a class:
If, else, etc work as expected:
However Ruby also allows interesting variations of this, such as putting the comparions behind the block to execute:
We can also use unless
, which is a more natural way to
check for negated expressions:
switch
statements are known as case
statements, but don’t fallthrough
by default like in
Java:
case a
when 1
spell = "one"
when 2
spell = "two"
when 3
spell = "three"
when 4
spell = "four"
when 5
spell = "five"
else
spell = nil
end
Since everything is an object, we can also use case
statements to check if instances are of a class:
As mentioned before, Ruby is a very flexible language. The case statement for example also allows to us to check regular expressions:
We can even use Lambdas in case statements, making long
if ... else
blocks unnecessary:
And the object orientation becomes very clear; we can even define our own matcher classes:
class Zigor
def self.===(string)
string.downcase == "zigor"
end
end
name = "Zigor"
case name
when Zigor
puts "Nice to meet you Zigor!!!"
else
puts "Who are you?"
end
We can also assign values from a case statement:
grade = case mark
when 80..100 : 'A'
when 60..79 : 'B'
when 40..59 : 'C'
when 0..39 : 'D'
else "Unable to determine grade. Try again."
end
Ruby has the for
loop that we are all used to, but also
more specialized constructs that allow for more expressive usecases:
For example upto
and downto
methods:
Or the times
method, which is much more readable:
while
, until
and the infinite
loop
loops still exist however:
We can also use break
, next
and
redo
within a loop’s block:
5.times do |num|
puts "num = #{num}"
puts "Do you want to redo? (y/n): "
option = gets.chop
redo if option == 'y'
end
Arrays in Ruby can contain multiple types and work as expected; there is no array vs collection divide:
Instead of loops you can use the each
method to
iterate:
We can use <<
to add things to an array:
>> countries << "India"
=> ["India"]
>> countries
=> ["India"]
>> countries.size
=> 1
>> countries.count
=> 1
And access elements with [0]
:
Thanks to the ..
syntax we can also access multiple
elements at once in a very simple way:
And use the includes?
method (note the ?
!)
to check if elements are present:
And delete
to delete elements:
If we have a nested array, using dig
fill allow us to
find deeply nested elements in a simple way:
>> array = [1, 5, [7, 9, 11, ["Treasure"], "Sigma"]]
=> [1, 5, [7, 9, 11, ["Treasure"], "Sigma"]]
>> array.dig(2, 3, 0)
=> "Treasure"
Another very useful set of features are set operations, allowing us
to modify arrays in a simple way, for example we can use the
&
operator to find elements that are in two arrays:
>> volleyball = ["Ashok", "Chavan", "Karthik", "Jesus", "Budha"]
=> ["Ashok", "Chavan", "Karthik", "Jesus", "Budha"]
>> cricket = ["Budha", "Karthik", "Ragu", "Ram"]
=> ["Budha", "Karthik", "Ragu", "Ram"]
>> volleyball & cricket
=> ["Karthik", "Budha"]
Or +
to merge them:
>> volleyball + cricket
=> ["Ashok", "Chavan", "Karthik", "Jesus", "Budha", "Budha", "Karthik", "Ragu", "Ram"]
Or use |
to merge both, but de-duplicating at the same
time:
Finally, we can also use -
to remove multiple elements
at once:
For those who are familiar with MapReduce, Ruby provides all of it in
the language. For example map
:
Note that this doesn’t modify the array; we can use map!
for that, which works for lots of Ruby methods:
The filter
method for example can be used in the same
way (named keep_if
, with the opposite
delete_if
also existing), and works like how you already
know if from JS:
>> array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>> array.keep_if{ |element| element % 2 == 0}
=> [2, 4, 6, 8, 10]
Hashes can be used to store mapped information:
And we can define a default value:
The hash literal {}
also allows us to create hashes with
pre-filled information:
To loop over hashes, we can use the each
method
again:
A very interesting feature to use in combination with hashes are symbols; they are much more efficient than strings as they are global and thus use less memory:
We can check this by getting their object_id
(a kind of
pointer):
c = "able was i ere i saw elba"
d = "able was i ere i saw elba"
>> c.object_id
=> 21472860
>> .object_id
=> 1441620
Just like accessing hash values is similar for arrays and hashes, we can use the same MapReduce functions on hashes:
>> hash = {a: 1, b: 2, c: 3}
=> {:a=>1, :b=>2, :c=>3}
>> hash.transform_values{ |value| value * value }
=> {:a=>1, :b=>4, :c=>9}
Ranges are a cool concept in Ruby that we’ve used before. We can use
them with the ..
notation:
We can also use them on strings:
They can be very useful in case
statements, where you
can replace lots of or
operators with them:
grade = case mark
when 80..100
'A'
when 60..79
'B'
when 40..59
'C'
when 0..39
'D'
else
"Unable to determine grade. Try again."
end
In addition to using them in case
statements as
described before, they can also serve as conditions:
print "Enter any letter: "
letter = gets.chop
puts "You have entered a lower case letter" if ('a'..'z') === letter
puts "You have entered a upper case letter" if ('A'..'Z') === letter
We can also use triple dots, which will remove the last value:
It is also possible to define endless ranges:
print "Enter your age: "
age = gets.to_i
case age
when 0..18
puts "You are a kid"
when (19..)
puts "You are grown up"
end
As mentioned before, Ruby draws a lot of inspiration from functional programming languages, and functions are a primary building block in the language as a result.
We can define functions with def
and call them without
parentheses:
It is also possible to define default arguments unlike in Java:
Arguments are always passed by reference:
def array_changer array
array << 6
end
some_array = [1, 2, 3, 4, 5]
p some_array
array_changer some_array
p some_array
=> [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5, 6]
There is no need for a return
statements as returns are
implicit (but optional for control flow support):
We can also define named arguments, with or without defaults:
def say_hello name: "Martin", age: 33
puts "Hello #{name} your age is #{age}"
end
say_hello name: "Joseph", age: 7
Arguments can also be variadic:
A very neat function is to use argument forwarding to call a function with all used parameters:
def print_something string
puts string
end
def decorate(...)
puts "#" * 50
print_something(...)
puts "#" * 50
end
decorate "Hello World!"
We can also define a function in more consise way:
Besides the functional influence, Ruby is also a radically object-oriented language. As a result, it makes working with objects and classes very easy:
Through the attr_reader
, attr_writer
and
attr_accessor
notation we can add instance variables to a
class:
They can be read and written with .
:
s1 = Square.new # creates a new square
s1.side_length = 5 # sets its side length
puts "Side length of s1 = #{s1.side_length}" # prints the side length
Methods can be defined with def
:
class Square
attr_accessor :side_length
def area
@side_length * @side_length
end
def perimeter
4 * @side_length
end
end
Note the use of @
to access instance variables.
Like many object-oriented languages, Ruby supports constructors (called initializers):
class Square
attr_accessor :side_length
def initialize side_length = 0
@side_length = side_length
end
def area
@side_length * @side_length
end
def perimeter
4 * @side_length
end
end
Variables defined by attr_accessor
as public; we can
make them private by ommiting their definition:
In a similar way, we can use private
and
protected
to change the visibility of methods:
class Human
attr_accessor :name, :age
def tell_about_you
puts "Hello I am #{@name}. I am #{@age} years old"
end
private def tell_a_secret
puts "I am not a human, I am a computer program. He! Hee!!"
end
end
In addition to instance variables, we can also create class variables
which work similar to static variables in Java using the @@
notation:
class Robot
def initialize
if defined?(@@robot_count)
@@robot_count += 1
else
@@robot_count = 1
end
end
def self.robots_created
@@robot_count
end
end
Similarly so, we can define class constants like so:
While inheritance is not the primary means of reusing code in Ruby,
there is support for it in the language using the <
notation:
class Rectangle
attr_accessor :length, :width
end
class Square < Rectangle
def initialize length
@width = @length = length
end
def side_length
@width
end
end
We can overwrite methods; interestingly it is possible to change a
child’s signature and use the super
method in the
child:
I won’t go into more details on these aspects as they are mostly
similar to Java; the same goes for Threads, Exception and more. One
thing uniquely powerful in Ruby is reflection; for example, you can get
the methods of a class as an array using .methods
:
>> "a".methods
=>
[:unicode_normalized?,
:encode!,
:unicode_normalize,
:ascii_only?,
:unicode_normalize!,
:to_r,
:encode,
:to_c,
:include?,
:%,
:*,
:+,
:unpack,
# ...
]
We can also get private methods using .private_methods
,
instance variables using .instance_variables
etc.
Another feature fairly unique to Ruby is method aliasing:
class Something
def make_noise
puts "AAAAAAAAAAAAAAHHHHHHHHHHHHHH"
end
alias :shout :make_noise
end
Something.new.shout
This makes it very easy to define multiple method names for things
that are frequently interchanged, such as .delete
and
.remove
, or .filter
and
.keep_if
.
Due to Ruby’s dynamic nature, we can also define classes dynamically and anonymously:
To deal with the complexities of such a dynamic language, Ruby has support for a safe navigation operator similar to Typescript:
class Robot
attr_accessor :name
end
robot = Robot.new
robot.name = "Zigor"
puts "The robots name is #{robot.name}" if robot&.name
We can use the require
function to import things from
files; this is very similar to how early NodeJS works:
# break_main.rb
require "./break_square.rb"
s = Square.new
s.side_length = 5
puts "The squares perimeter is #{s.perimeter}"
However this quickly leads to problems with code organization, for example when two functions with a different purpose are named the same way. Ruby solves this issue with modules:
If we include Star
and call line
, we will
print a line of starts, and if we do so with Dollar
,
calling line
again will print dollar signs. Without
including line
, the method will be undefined.
We can also call methods and access other objects in a module using
the ::
operator:
The include
keyword can be used to form Mixins, which
will expose reusable code only to a specific class, i.e. make the
Pi
constant only accessible from a single class:
Ruby is a very flexible langauge, and as such it allows
metaprogramming. For example, directly call a method using the
send
function by passing in the speak
symbol:
class Person
attr_accessor :name
def speak
"Hello I am #{@name}"
end
end
p = Person.new
p.name = "Karthik"
puts p.send(:speak)
This allows for very powerful, but dangerous things, such as calling arbitrary functions by passing in the method name as a string:
class Student
attr_accessor :name, :math, :science, :other
end
s = Student.new
s.name = "Zigor"
s.math = 100
s.science = 100
s.other = 0
If we want to give a user access to any of the properties using
send
, we can get their input using
gets.chop
:
print "Enter the subject who's mark you want to know: "
subject = gets.chop
puts "The mark in #{subject} is #{s.send(subject)}"
We can also catch a developer calling methods that don’t exist at
runtime and handle that usecase explicitly by implementing a
method_missing
method:
class Something
def initialize
@name = "Jake"
end
def method_missing method, *args, &block
puts "Method: #{method} with args: #{args} does not exist"
block.call @name
end
end
s = Something.new
s.call_method "boo", 5 do |x|
puts x
end
As you can see, we’re now able to call a method that doesn’t exist, and provide the implementation ourselves:
Instead of passing in an implementation in the form of a block ourselves, we can also do other things, such as matching the incoming method name against a regular expression and then manually calling the method:
class Person
attr_accessor :name, :age
def initialize name, age
@name, @age = name, age
end
def method_missing method_name
method_name.to_s.match(/get_(\w+)/)
send($1)
end
end
person = Person.new "Zigor", "67893"
puts "#{person.get_name} is #{person.get_age} years old"
=> Zigor is 67893 years old
It is also possible to use define_method
to dynamically
define a method at runtime:
class Person
def initialize name, age
@name, @age = name, age
end
end
Person.define_method(:get_name) do
@name
end
person = Person.new "Zigor", "67893"
>> person.get_name
=> "Zigor"
We can also define class methods etc. using
define_singleton_method
or class_eval
and
instance_eval
etc. to add arbitrary things such ass
attr_accessor
s to classes or even instances.
Recommended:
Not Recommended:
While not recommended in modern applications (see professor Kriha’s “Distributed Systems” course), dRuby is an excellent example of an idiomatic Ruby way of creating servers and clients, specifically distributed objects. We can define a server like so:
require 'drb/drb'
URI = 'druby://localhost:8787'
class PersonServer
attr_accessor :name
def initialize(name)
@name = name
end
def local_time
Time.now
end
end
DRb.start_service URI, PersonServer.new('Sheepy')
puts "Listening on to URI #{URI}"
DRb.thread.join
And interact with the objects on the server like so:
require 'drb/drb'
URI = 'druby://localhost:8787'
DRb.start_service
puts "Connecting to URI #{URI}"
person = DRbObject.new_with_uri URI
puts "#{person.name} #{person.local_time}"
person.name = 'Noir'
puts "#{person.name} #{person.local_time}"
As we can see, with very little code we can get a lot of functionality.
Demo: Write such a service and expose it to the internet with
ssh -R
, then consume it
Aside from Ruby on Rails, Sinatra is a very neat web framework. You can define a web server in just three lines of code:
Handling POST requests and parsing data is also very simple:
before do
next unless request.post?
request.body.rewind
@request_payload = JSON.parse request.body.read
end
post '/' do
@request_payload['name']
end
By using ERB, we can render templates very easily:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ERB Learning</title>
</head>
<body>
<h1>Hello, <%= @name %>!</h1>
</body>
</html>
Demo: Add a webserver with a dRuby interface for setting the data