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:
>> 2 < 3
=> true>> 1 == 2
=> falseComparisons are type checked:
>> 1 == "1"
=> falseTrip equals can be used to check if if an instance belongs to a class:
>> String === "abc"
=> trueIf, else, etc work as expected:
if name == "Zigor"
puts "#{name} is intelligent"
endHowever Ruby also allows interesting variations of this, such as putting the comparions behind the block to execute:
puts "#{name} is genius" if name == "Zigor"We can also use unless, which is a more natural way to
check for negated expressions:
p "You are a minor" unless age >= 18switch 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
endSince everything is an object, we can also use case
statements to check if instances are of a class:
a = "Zigor"
case a
when String
puts "Its a string"
when Fixnum
puts "Its a number"
endAs mentioned before, Ruby is a very flexible language. The case statement for example also allows to us to check regular expressions:
case string
when /Ruby/
puts "string contains Ruby"
else
puts "string does not contain Ruby"
endWe can even use Lambdas in case statements, making long
if ... else blocks unnecessary:
case num
when -> (n) { n % 2 == 0 }
puts "#{num} is even"
else
puts "#{num} is odd"
endAnd 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?"
endWe 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."
endRuby has the for loop that we are all used to, but also
more specialized constructs that allow for more expressive usecases:
for i in 0..10
p i
endFor example upto and downto methods:
10.downto 1 do |num|
p num
end17.upto 23 do |i|
print "#{i}, "
endOr the times method, which is much more readable:
7.times do
puts "I know something"
endwhile, until and the infinite
loop loops still exist however:
i=1
while i <= 10 do
print "#{i}, "
i+=1
endi=1
until i > 10 do
print "#{i}, "
i+=1
endloop do
puts "I Love Ruby"
endWe can also use break, next and
redo within a loop’s block:
1.upto 10 do |i|
break if i == 6
print "#{i}, "
end10.times do |num|
next if num == 6
puts num
end5.times do |num|
puts "num = #{num}"
puts "Do you want to redo? (y/n): "
option = gets.chop
redo if option == 'y'
endArrays in Ruby can contain multiple types and work as expected; there is no array vs collection divide:
my_array = ["Something", 123, Time.now]Instead of loops you can use the each method to
iterate:
my_array.each do |element|
puts element
endWe can use << to add things to an array:
>> countries << "India"
=> ["India"]
>> countries
=> ["India"]
>> countries.size
=> 1
>> countries.count
=> 1And access elements with [0]:
>> countries[0]
=> "India"Thanks to the .. syntax we can also access multiple
elements at once in a very simple way:
>> countries[4..9]
=> ["China", "Niger", "Uganda", "Ireland"]And use the includes? method (note the ?!)
to check if elements are present:
>> countries.include? "Somalia"
=> trueAnd delete to delete elements:
>> countries.delete "USA"
=> "USA"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:
>> volleyball | cricket
=> ["Ashok", "Chavan", "Karthik", "Jesus", "Budha", "Ragu", "Ram"]Finally, we can also use - to remove multiple elements
at once:
>> volleyball - cricket
=> ["Ashok", "Chavan", "Jesus"]For those who are familiar with MapReduce, Ruby provides all of it in
the language. For example map:
>> array = [1, 2, 3]
=> [1, 2, 3]
>> array.map{ |element| element * element }
=> [1, 4, 9]Note that this doesn’t modify the array; we can use map!
for that, which works for lots of Ruby methods:
>> array.collect!{ |element| element * element }
=> [1, 4, 9]
>> array
=> [1, 4, 9]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:
mark = {}
mark['English'] = 50
mark['Math'] = 70
mark['Science'] = 75And we can define a default value:
mark = {}
mark.default = 0
mark['English'] = 50
mark['Math'] = 70
mark['Science'] = 75The hash literal {} also allows us to create hashes with
pre-filled information:
marks = { 'English' => 50, 'Math' => 70, 'Science' => 75 }To loop over hashes, we can use the each method
again:
total = 0
mark.each { |key,value|
total += value
}
puts "Total marks = "+total.to_sA 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:
mark = {}
mark[:English] = 50
mark[:Math] = 70
mark[:Science] = 75We 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
=> 1441620e = :some_symbol
f = :some_symbol
>> e.object_id
=> 1097628
>> f.object_id
=> 1097628Just 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:
>> (1..5).each {|a| print "#{a}, " }
=> 1, 2, 3, 4, 5, => 1..5We can also use them on strings:
>> ("bad".."bag").each {|a| print "#{a}, " }
=> bad, bae, baf, bag, => "bad".."bag"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."
endIn 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') === letterWe can also use triple dots, which will remove the last value:
>> (1..5).to_a
=> [1, 2, 3, 4, 5]
>> (1...5).to_a
=> [1, 2, 3, 4]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"
endAs 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:
def print_line
puts '_' * 20
end
print_lineIt is also possible to define default arguments unlike in Java:
def print_line length = 20
puts '_'*length
end
print_line
print_line 40Arguments 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):
def addition x, y
x + y
end
addition 3, 5
=> 8We 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: 7Arguments can also be variadic:
def some_function a, *others
puts a
others.each do |x|
puts x
end
end
some_function 1,2,3,4,5A 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:
def double(num) = num * 2Besides the functional influence, Ruby is also a radically object-oriented language. As a result, it makes working with objects and classes very easy:
class Square
endThrough the attr_reader, attr_writer and
attr_accessor notation we can add instance variables to a
class:
class Square
attr_accessor :side_length
endThey 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 lengthMethods can be defined with def:
class Square
attr_accessor :side_length
def area
@side_length * @side_length
end
def perimeter
4 * @side_length
end
endNote 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
endVariables defined by attr_accessor as public; we can
make them private by ommiting their definition:
class Human
def set_name name
@name = name
end
def get_name
@name
end
endIn 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
endIn 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
endSimilarly so, we can define class constants like so:
class Something
Const = 25
def Const
Const
end
end
puts Something::ConstWhile 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
endWe can overwrite methods; interestingly it is possible to change a
child’s signature and use the super method in the
child:
class Square < Rectangle
def set_dimension side_length
super side_length, side_length
end
endI 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.shoutThis 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:
person = Class.new do
def say_hi
'Hi'
end
end.newTo 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&.nameWe can use the require function to import things from
files; this is very similar to how early NodeJS works:
# break_square.rb
class Square
attr_accessor :side_length
def perimeter
@side_length * 4
end
end# 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:
module Star
def line
puts '*' * 20
end
end
module Dollar
def line
puts '$' * 20
end
endIf 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:
>> Dollar::line
=> $$$$$$$$$$$$$$$$$$$$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:
class Sphere
include Constants
attr_accessor :radius
def volume
(4.0/3) * Pi * radius ** 3
end
endRuby 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 = 0If 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
endAs you can see, we’re now able to call a method that doesn’t exist, and provide the implementation ourselves:
=> Method: call_method with args: ["boo", 5] does not exist
=> JakeInstead 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 oldIt 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_accessors 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.joinAnd 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:
require 'sinatra'
get '/' do
'Hello, world!'
endHandling 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']
endBy using ERB, we can render templates very easily:
require 'sinatra'
get '/' do
@name = params['name']
erb :index
end<!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