Uni Programming Languages Notes

Felicitas Pojtinger (fp036)

2022-10-24

1.1 Introduction

1.1.1 Contributing

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):

QR code to source repository

If you like the study materials, a GitHub star is always appreciated :)

1.1.2 License

AGPL-3.0 license badge

Uni Programming Languages Notes (c) 2022 Felicitas Pojtinger and contributors

SPDX-License-Identifier: AGPL-3.0

1.2 Overview

1.2.1 General Design

1.2.2 Implementation Details

1.2.3 Users

1.2.4 Timeline

1.3 Syntax

1.3.1 Logic

Typical logical operators:

>> 2 < 3
=> true
>> 1 == 2
=> false

Comparisons are type checked:

>> 1 == "1"
=> false

Trip equals can be used to check if if an instance belongs to a class:

>> String === "abc"
=> true

If, else, etc work as expected:

if name == "Zigor"
  puts "#{name} is intelligent"
end

However 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 >= 18

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:

a = "Zigor"
case a
when String
  puts "Its a string"
when Fixnum
  puts "Its a number"
end

As 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"
end

We 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"
end

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

1.3.2 Loops

Ruby 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
end

For example upto and downto methods:

10.downto 1 do |num|
  p num
end
17.upto 23 do |i|
  print "#{i}, "
end

Or the times method, which is much more readable:

7.times do
  puts "I know something"
end

while, until and the infinite loop loops still exist however:

i=1
while i <= 10 do
  print "#{i}, "
  i+=1
end
i=1
until i > 10 do
  print "#{i}, "
  i+=1
end
loop do
  puts "I Love Ruby"
end

We can also use break, next and redo within a loop’s block:

1.upto 10 do |i|
  break if i == 6
  print "#{i}, "
end
10.times do |num|
  next if num == 6
  puts num
end
5.times do |num|
  puts "num = #{num}"
  puts "Do you want to redo? (y/n): "
  option = gets.chop
  redo if option == 'y'
end

1.3.3 Arrays

Arrays 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
end

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]:

>> 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"
=> true

And 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]

1.3.4 Hashes

Hashes can be used to store mapped information:

mark = {}
mark['English'] = 50
mark['Math'] = 70
mark['Science'] = 75

And we can define a default value:

mark = {}
mark.default = 0
mark['English'] = 50
mark['Math'] = 70
mark['Science'] = 75

The 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_s

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:

mark = {}
mark[] = 50
mark[] = 70
mark[] = 75

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
e = 
f = 
>> e.object_id
=> 1097628
>> f.object_id
=> 1097628

Just like accessing hash values is similar for arrays and hashes, we can use the same MapReduce functions on hashes:

>> hash = {1, 2, 3}
=> {>1, >2, >3}
>> hash.transform_values{ |value| value * value }
=> {>1, >4, >9}

1.3.5 Ranges

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..5

We 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."
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:

>> (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"
end

1.3.6 Functions

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:

def print_line
  puts '_' * 20
end

print_line

It is also possible to define default arguments unlike in Java:

def print_line length = 20
  puts '_'*length
end

print_line
print_line 40

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):

def addition x, y
  x + y
end

addition 3, 5

=> 8

We can also define named arguments, with or without defaults:

def say_hello "Martin", 33
  puts "Hello #{name} your age is #{age}"
end

say_hello "Joseph", 7

Arguments 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,5

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:

def double(num) = num * 2

1.3.7 Classes

Besides 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
end

Through the attr_reader, attr_writer and attr_accessor notation we can add instance variables to a class:

class Square
  attr_accessor 
end

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 

  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 

  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:

class Human
  def set_name name
    @name = name
  end

  def get_name
    @name
  end
end

In a similar way, we can use private and protected to change the visibility of methods:

class Human
  attr_accessor , 

  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:

class Something
  Const = 25

  def Const
    Const
  end
end

puts Something::Const

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 , 
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:

class Square < Rectangle
  def set_dimension side_length
    super side_length, side_length
  end
end

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
=>
[,
 ,
 ,
 ,
 ,
 ,
 ,
 ,
 ,
 :%,
 :*,
 :+,
 ,
 # ...
]

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  
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:

person = Class.new do
  def say_hi
    'Hi'
  end
end.new

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 
end

robot = Robot.new
robot.name = "Zigor"
puts "The robots name is #{robot.name}" if robot&.name

1.3.8 Files, Modules and Mixins

We 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 

  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
end

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:

>> 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 

  def volume
    (4.0/3) * Pi * radius ** 3
  end
end

1.3.9 Metaprogramming

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 

  def speak
    "Hello I am #{@name}"
  end
end


p = Person.new
p.name = "Karthik"
puts p.send()

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 , , , 
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:

=> Method: call_method with ["boo", 5] does not exist
=> Jake

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 , 

  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() 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.

1.4 Usecases for Ruby

Recommended:

Not Recommended:

1.5 Practical Examples

1.5.1 dRuby

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 

  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

1.5.2 Sinatra

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!'
end

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:

require 'sinatra'

get '/' do
  @name = params['name']

  erb 
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

1.6 Questions