02. Списъци, хешове и функции

02. Списъци, хешове и функции

02. Списъци, хешове и функции

10 октомври 2012

Какво предстои

Преди това

Бонус точки

отвъд сникерсите

Първите ви точки:

Първа задача

Стил

Споделяне на решения

a.k.a. преписване

Малко тривиа

където ние питаме, а вие отговаряте

(ако сте записали курса)

Въпрос 1

Каква е разликата между тези имена?

foo  = 1
Bar  = 2
$baz = 3

foo е локална променлива, Bar е константа, $baz е глобална променлива

Въпрос 2

Кои от следните неща се оценяват до неистина?

0 [] nil {} ""

Само false и nil са неистина.

Въпрос 3

Какво прави <=>

Връща положително число (1), ако първия елемент е по-голям, нула, ако са равни и отрицателно (-1), ако първия е по-малък.

Помнете го като "минус".

Въпрос 4

Каква е разликата между единични и двойки кавички?

В единичните няма интерполация, в двойните - има.

В единичните могат да се екранират само \\ и \'. В двойните има ред екранирания и специални символи.

Въпрос 5

Какво извежда следният код и защо?

puts 'foo' == 'foo', 'foo'.equal? 'foo'
puts [] == [],       [].equal? []
puts 42 == 42,       42.equal? 42
puts :foo == :foo,   :foo.equal? :foo

== сравнява по стойност, а equal? по идентитет.

Числата и символите се интернират.

Кратко проучване

Колко от вас са запознати със следните неща:

Очакваният отговор: всички

Отклониение

#to_s и #inspect

Един обект се обръща до низ с #to_s. Има и друг вариант, #inspect, който го обръща до текстов низ, изглеждащ като ruby код. Целта е инспектиране.

Преди това

хилядите значение на диез

Списъци

Списъци

индексиране

numbers = [:zero, :one, :two]
numbers[1]   # :one
numbers[10]  # nil
numbers[-1]  # :two

numbers[5] = :five
numbers[5]   # :five
numbers      # [:zero, :one, :two, nil, nil, :five]

Списъци

fetch

Array#fetch хвърля грешка или връща друга стойност, при индексиране извън обема на списъка:

numbers = [:zero, :one, :two]

numbers.fetch(1)            # :one
numbers.fetch(10, :dunno)   # :dunno
numbers.fetch(10)           # error: IndexError

numbers[10] || :dunno       # като предното, ама не точно

Ползва се по-рядко, отколкото си мислите.

Списъци

типичните методи

numbers = [3, 1, 2, 4]
numbers.length  # 4
numbers.size    # 4
numbers.sort    # [1, 2, 3, 4]
numbers.reverse # [4, 2, 1, 3]
numbers[1..2]   # [1, 2]

sort и reverse връщат нов списък, без да променят numbers.

Списъци

#include?

#include? ви казва дали списък съдържа даден елемент.

prime_digits = [2, 3, 5, 7]

prime_digits.include? 2    # true
prime_digits.include? 4    # false

Внимание: линейно търсене. Подходящо за малки списъци.

Списъци

забавни оператори

[:a, :b, :c] + [:d, :e]             # [:a, :b, :c, :d, :e]
[:a, :b, :c, :b, :a] - [:b, :c, :d] # [:a, :a]
[:a, :b, :c] & [:b, :c, :d]         # [:b, :c]
[:a, :b, :c] | [:b, :c, :d]         # [:a, :b, :c, :d]

& и | конкатенират списъците и премахват повторенията.

В Ruby има множества, които са по-удачни в повечето случаи.

Списъци

мутиране

numbers = [1, 2, 3]

numbers << 4
p numbers   # [1, 2, 3, 4]

numbers.insert 0, :zero
p numbers   # [:zero, 1, 2, 3, 4]

result = numbers.delete_at(0)
p result    # :zero
p numbers   # [1, 2, 3, 4]

Списъци

push и pop

stack = [1, 2, 3]

stack.push 4
p stack         # [1, 2, 3, 4]

top = stack.pop
p stack         # [1, 2, 3]
p top           # 4

#shift и #unshift са аналогични, но работят с началото на списъка.

Списъци

разни други методи

[1, 2, 3].join("-")        # "1-2-3"
[1, 2, 3].permutation      # сещате се какво връща
[1, 2].product([3, 4])     # [[1, 3], [1, 4], [2, 3], [2, 4]]
[[1, 2], [3, 4]].transpose # [[1, 3], [2, 4]]
[1, 2, 3, 4].shuffle       # разбърква списъка произволно

Има ред такива методи, които може да намерите в APIDock.

Документация

свиквайте с нея отсега

Списъци

последната запетая

Може да оставите запетая след последния елемент на списъка. Така редовете се разместват по-лесно. Важи и за хешове.

songs = [
  'My Favorite Things',
  'Alabama',
  'A Love Supreme',
]

Списъци

пърладжийски истории

Има специален синтаксис за списък от думи.

%w(chunky bacon)     == ['chunky', 'bacon']
%w[a b c]            == ['a', 'b', 'c']
%w{cool stuff}       == ['cool', 'stuff']
%w<coffee tea water> == ['coffee', 'tea', 'water']
%w|foo bar|          == ['foo', 'bar']

Може да използвате различни видове символи, които да ограждат списъка:

! @ # $ * - _

Този списък е непълен.

Списъци

Array#slice voodoo

Списъци

Array#slice

numbers = [1, 2, 3, 4, 5, 6]

numbers[0..2]   # [1, 2, 3]
numbers[-3..-1] # [4, 5, 6]
numbers[1, 1]   # [2]

numbers[0..2] = [:wat]

numbers         # [:wat, 4, 5, 6]

Списъци

итерация

Итерира се с #each, както всичко останало в Ruby:

primes = [2, 3, 5, 7, 11]

primes.each { |n| puts n }

primes.each do |n|
  puts n
end

Sidenote: for

...или как де губим точки

"Къдрави скоби" и do/end

next и break

next прескача изпълнението до края на блока и продължава със следващия елемент, ала continue в C/Java.

break прекъсва итерацията и продължава изпълнението след блока.

numbers.sort.each do |number|
  next if number.odd?
  break if number > 100

  puts number
end

Хешове

Хешове

общи факти

Хешове

Индексиране

numbers = {:one => :eins, :two => :zwei}

numbers[:one]     # :eins
numbers[:three]   # nil

numbers[:three] = :drei

numbers[:three]                     # :drei
numbers.fetch(:four, :keine_ahnung) # :keine_ahnung
numbers.fetch(:four)                # error: KeyError

Хешове

итерация

numbers = {:one => :eins, :two => :zwei}
numbers.keys    # [:one, :two]
numbers.values  # [:eins, :zwei]

numbers.each { |pair| puts pair }
numbers.each { |key, value| puts key, value }

Хешове

разни методи

numbers = {1 => 2, 3 => 4}

numbers.has_key?(:three) # false
numbers.size             # 2
numbers.invert           # {2=>1, 4=>3}
numbers.merge({5 => 6})  # {1=>2, 3=>4, 5=>6}
numbers.to_a             # [[1, 2], [3, 4]]
Hash[1, 2, 3, 4]         # {1=>2, 3=>4}

Хешове алтернативен синтаксис

Долните два реда произвеждат еднакви хешове. Второто е 1.9 синтаксис:

{:one => 1, :two => 2}
{one: 1, two: 2}

Има интересна врътка при извикването на методи, за която ще говорим по-натам.

Масиви и хешове

накратко

Методи

дефиниране

Дефинирането става с ключовата дума def. Резултатът от функцията е последният оценен израз, ако няма return някъде.

def factorial(n)
  unless n == 1
    factorial(n - 1) * n
  else
    1
  end
end

В Ruby няма tail recursion оптимизация. Този код яде стек.

Методи

отгоре-отгоре

Методи в съществуващи класове

Ще ви трябва за домашното

За да добавите метод в съществуващ клас, например Array, просто "отваряте" класа и дефинирате метода:

class Array
  def fourty_second
    self[41]
  end
end

list     = []
list[41] = 'The Universe'

list.fourty_second # "The Universe"

Методи

return

Можете да излезете от функция с return:

def includes?(array, element)
  array.each do |item|
    return true if item == element
  end
  false
end

Разбира се, такава функция е излишна. Може да ползвате array.include?(element).

Методи

стойности по подразбиране

Параметрите в Ruby могат да имат стойности по подразбиране:

def order(drink, size = 'large')
  puts "A #{size} #{drink}, please!"
end

order 'tea'
order 'coffee', 'small'

Методи

стойности по подразбиране (2)

Методи

променлив брой аргументи

Методите в ruby могат да вземат променлив брой аргументи. Параметърът се означава със * и при извикване на функцията съдържа списък от аргументите.

def say_hi(name, *drinks)
  puts "Hi, I am #{name} and I enjoy: #{drinks.join(',')}"
end

say_hi 'Stefan', 'coffee', 'tea', 'water'

Методи

променлив брой аргументи

Параметърът за променлив брой аргументи може да е на всяка позиция в дефиницията:

def something(*a, b, c)
end

def something(a, *b, c)
end

Очевидно, може да има само един такъв параметър във функция.

Методи

...и техните приятели, хешовете

Когато последния аргумент е хеш, може да изтървете фигурните скобо около него. Долните редове правят едно и също:

def order(drink, preferences)
end

order 'Latte', {:size => 'grande', :syrup => 'hazelnut'}
order 'Latte', :size => 'grande', :syrup => 'hazelnut'
order 'Latte', size: 'grande', syrup: 'hazelnut'

Така ruby симулира извикане на функция с наименовани аргументи.

Методи

...и хешове, отново

Често ще видите код в този вид:

def order(drink, preferences = {})
end

order 'Latte'
order 'Latte', size: 'grande', syrup: 'hazelnut'

Така preferences е незадължителен и няма нужда да го подавате, ако нямате предпочитания.

Методи

предикати

Името на метод може да завършва на ?. Това се ползва, за методи, които връщат лъжа или истина (предикати):

def even?(n)
  n % 2 == 0
end

even? 2
even? 3

Това е само конвенция.

Методи

две версии

Името на метод може да завършва на !. Това се ползва, когато методът има две версии с различно поведение:

numbers = [4, 1, 3, 2, 5, 0]

numbers.sort   # връща нов списък
numbers.sort!  # променя списъка на място

В случая, "по-опасният" метод завършва на удивителна.

Анонимни функции

ламбди

Анонимни функции в Ruby се дефинират с lambda. Имат три начина на извикване:

pow = lambda { |a, b| a ** b }

pow.call 2, 3
pow[2, 3]
pow.(2, 3)

За нещастие, не може да извиквате така: double(2). Това е несъвместимо с изтърваването на скобите при извикването на метод.

Анонимни функции

ламбди (2)

Може и така:

double = lambda do |x|
  x * 2
end

Важи стандартната конвенция за { } и do/end.

Анонимни функции

ламбди (3)

От 1.9 има по-симпатичен синтаксис за ламбди:

say_hi = lambda { puts 'Hi there!' }
double = lambda { |x| x * 2 }
divide = lambda { |a, b| a / b }

say_hi = -> { puts 'Hi there' }
double = ->(x) { x * 2 }
divide = ->(a, b) { a / b }

Блокове

където става забавно

Всеки метод в може да приеме допълнителен аргумент, който е "анонимна функция". Може да го извикате от метода с yield:

def twice
  yield
  yield
end

twice { puts 'Ruby rocks!' }

Блокове

аргументи

Блокът може да приема аргументи:

def sequence(first, last, step)
  current = first
  while current < last
    yield current
    current += step
  end
end

sequence(1, 10, 2) { |n| puts n }
# Извежда 1, 3, 5, 7, 9

Блокове

стойности

yield се оценява до стойността на блока:

def calculate
  result = yield(2)
  puts "The result for 2 is #{result}"
end

calculate { |x| x ** 2 }
# The result for 2 is 4

Блокове

преди примера

Колко от вас знаят какво прави filter?

Блокове

един пример

Или как можем да напишем filter:

def filter(array)
  result = []
  array.each do |item|
    result << item if yield item
  end
  result
end

filter([1, 2, 3, 4, 5]) { |n| n.odd? }

Блокове

#block_given?

block_given? ще ви каже дали методът е извикан с блок:

def i_can_haz_block
  if block_given?
    puts 'yes'
  else
    puts 'no'
  end
end

i_can_haz_block                  # no
i_can_haz_block { 'something' }  # yes

Блокове

& при извикване на метод

Ако имате ламбда, която искате да подадете като блок, може да ползвате &:

is_odd = lambda { |n| n.odd? }

filter([1, 2, 3, 4, 5], &is_odd)
filter([1, 2, 3, 4, 5]) { |n| n.odd? }

Горните са (почти) еквиваленти. Има малка разлика в някои други случаи.

Блокове

в сигнатурата

Ако искате да вземете блока като обект, има начин:

def invoke_with(*args, &block)
  block.(*args)
end

invoke_with(1, 2) { |a, b| puts a + b }

Блокове

в сигнатурата (2)

Може и така:

def make_block(&block)
  block
end

doubler = make_block { |n| n * 2 }
doubler.(2) # 4

Proc.new

където става странно

В Ruby има два вида анонимни функции. Другият е Proc.

double = Proc.new { |x| x * 2 }

double.call(2)
double[2]
double.(2)

Дотук е същото, но има разлики при извикване.

Разлики между Proc.new и lambda

f =Proc.new { |x, y| p x, y }lambda { |x, y| p x, y }
f.call(1)1 nilArgumentError
f.call(1, 2)1 21 2
f.call(1, 2, 3)1 2ArgumentError
f.call([1, 2])1 2ArgumentError
f.call(*[1, 2])1 21 2

Блокове

...и една особенност

Ако първия аргумент на функция е хеш, трябва да изтървете скобите на хеша, ако изтървете скобите на метода.

# Валиден код
order drink: 'latte', size: 'grande'
order({drink: 'latte', size: 'grande'})

# Невалиден код
order {drink: 'latte', size: 'grande'}

Във втория случай, Ruby си мисли, че му подавате блок.

Блокове

в Ruby като цяло

Функционални закачки

Стандартните функционални неща:

numbers   = [-9, -4, -1, 0, 1, 4, 9]

positive = numbers.select { |n| n >= 0 }
even     = numbers.reject { |n| n.odd? }
squares  = numbers.collect { |n| n ** 2 }
roots    = numbers.select { |n| n > 0 }.collect { |n| n ** 0.5 }

Функционални закачки

синоними

#select и #collect имат синоними #find_all и #map:

numbers   = [-9, -4, -1, 0, 1, 4, 9]

squares  = numbers.map { |n| n ** 2 }
positive = numbers.find_all { |n| n >= 0 }

В Ruby подобни синоними се срещат често.

#inject

ако разбирате това, значи сте ОК

#inject свежда списък до единична стойно с някаква операция:

numbers = [1, 2, 3, 4, 5]

numbers.inject(0) { |a, b| a + b }
numbers.inject(1) { |a, b| a * b }

numbers.inject { |a, b| a + b }
numbers.inject { |a, b| "#{a}, #{b}" }

#inject

често срещана грозотия

Имаме списък с думи. Искаме да получим хеш от вида {дума => дължина на думата}:

words = %w[chunky bacon is awesome]
words.inject({}) { |hash, word| hash[word] = word.length; hash }

Това е неидиоматично използване на #inject, но е интересен пример.

#inject

примерна имплементация

def inject(array, initial = nil)
  remaining = array.dup
  buffer    = initial || remaining.shift

  until remaining.empty?
    buffer = yield buffer, remaining.shift
  end

  buffer
end

inject([1, 2, 3, 4]) { |a, b| a + b }
inject([1, 2, 3, 4], 0) { |a, b| a + b }

#inject

още по-примерна имплементация

За забавлението. Неяснотите — следващия път.

class Array
  def inject(initial = nil)
    remaining = dup
    buffer    = initial || remaining.shift

    until remaining.empty?
      buffer = yield buffer, remaining.shift
    end

    buffer
  end
end

Следващия понеделник

където пием бира

Въпроси