Чудили ли сте се някога какво всъщност е Proc? Или сте чули някой да споменава затваряния, без наистина да знае какво е това? Е, това е основната причина, поради която пиша тази публикация, за да хвърля малко светлина върху тези термини и може би да ви насърча да разгледате малко по-нататък какво прави Ruby tick и програмирането като цяло!

Затваряния?

Това е просто термин в „програмирането“, който се отнася до концепцията за блок от код, който може да бъде предаден в променлива или обект, но също така помни неща от момента на създаването му. Така че Procs, Lambdas и Methods в Ruby всъщност са затваряния, но за да говоря за тях, ще започна със споменаване на основите...

Блокове

Блокът е основната форма за групиране на набор от инструкции в Ruby, подобно на стените, които образуват къща. И почти като стените, те не могат да правят много сами, освен да стоят там, и тъй като в Ruby всичко трябва да бъде обект („почти…“), блоковете сами в кода винаги ще генерират грешки!

Също така си струва да се спомене, че блоковете са обхванати от техните граници ({къдрави скоби} или „do…end“) и ще бъдат наясно със своето обкръжение в зависимост от това към кого са прикрепени. Което ме води до...

Методи

Ако сте кодирали Ruby или друг OO език, със сигурност вече сте запознати с концепцията за методи, но бих искал да обърна внимание на факта, че блоковете също са основна част от тях!

Ако го сведете надолу, методите са блокове, които се прикачват към конкретен обект, когато ги дефинирате, и изпълняват същия този блок върху тяхната дефиниция, когато са извикани от обекта, към който принадлежат, с възможност за вземане на данни за работа …

И сега казвате: „Чакай малко! Но когато започнах да кодирам Ruby, никога не посочих обектите, към които принадлежат, когато дефинирах методи! По дяволите, тогава дори не знаех какви са предметите!”. Е, помните ли, когато казах преди всичко е обект в Ruby? Това е един от онези случаи, при които, ако не кажете към кой обект принадлежи, Ruby го прикачва към винаги готовия Object.main от най-високо ниво и се уверява, че всичко се държи като, добре познахте… обект!

Също така Блокирането, когато се прилага към метод, знае всичко за променливите на екземпляра, които съществуват в обхвата на този екземпляр на обекта, но всички ние знаехме това вече!

процеси

Според документацията на Ruby процедурните обекти се споменават, както следва:

„Често е желателно да можете да определите отговорите на неочаквани събития. Както се оказа, това се прави най-лесно, ако можем да предаваме блокове от код като аргументи към други методи, което означава, че искаме да можем да третираме кода като данни.”

Това е хубава и потенциално полезна концепция, макар и малко неясна и, честно казано, не толкова ясна като начало… така че как да постигнем това?

Влиза в обекта Proc! Като прикрепите към екземпляр на този обект блок, можете да го използвате навсякъде във вашия код, както желаете. Изглежда нещо подобно:

my_proc = Proc.new { |x| puts x }

След като бъде създаден, Proc обект може да бъде извикан навсякъде и може да се използва като аргумент за други методи!

def foo (a, b)
      a.call(b)
  end
  
  putser = Proc.new {|x| puts x}
  foo(putser, 34)

И най-голямата особеност на Proc е фактът, че той помни всички локални променливи на обхвата, където е създаден, докато се предава като променливи на други методи!

def gen_times(factor)
  return Proc.new {|n| n*factor }
end

times3 = gen_times(3)
times5 = gen_times(5)

times3.call(12)               #=> 36
times5.call(5)                #=> 25
times3.call(times5.call(4))   #=> 60

И накрая, Procs също са много буйни и темпераментни момчета! Ако ги извикате вътре в метод, те ще принудят метода да върне върнатата им стойност

def return_from_proc
  a = Proc.new { return 10 }.call
  puts "This will never be printed."
end

Ламбда

С две думи, Lambda са почти същите като Procs. Те са създадени и достъпни по същия начин:

putser = lambda {|x| puts x}

Но има две основни разлики:

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

pnew = Proc.new {|x, y| puts x + y}
  lamb = lambda {|x, y| puts x + y}

  # works fine, printing 6
  pnew.call(2, 4, 11)
  
  # throws an ArgumentError
  lamb.call(2, 4, 11)

И ако се извикат вътре в методите, те позволяват изпълнението да продължи (те са учтиви хора!)

def return_from_lambda
  a = lambda { return 10 }.call
  puts "The lambda returned #{a}, and this will be printed."
end

И това ги прави по-предвидими от Procs!

Какво ще кажете за това кога да ги използвате?

Цялата концепция за затваряния идва главно от „функционалните програмни езици“, където всички функции по същество нямат състояние и поради това се превърна в характеристика възможността да се предават блокове от код, които запомнят своя обхват на създаване като променливи. В днешно време ориентираните езици нямат този вид недостатъци, но повечето от програмистката общност вярва, че този вид поведение е плюс и много езици имат тази философия по-добре внедрена и стават нещо като „хибрид“, като Javascript.

Имайки това предвид, за какво могат да се използват Procs и Lamdas в Ruby?

Правейки кода си по-сух, можете бързо да създадете Proc с блок от код, който може да се използва повторно във вашия код

class Account < ApplicationRecord
validates :password, confirmation: true,
unless: Proc.new { |a| a.password.blank? }
end

Всяка ситуация, при която би било удобно да получите достъп до данни от други части на програма, за да извършите някаква операция, без да се налага да присвоявате тези данни някъде другаде. За тази цел Procs се използват и в комбинация с yield statement и символа амперсанд (&)

def contrived(a, &f)
      # the block can be accessed through f
      f.call(a)
      
      # but yield also works !
      yield(a)
  end
  
  # this works
  contrived(25) {|x| puts x}
  
  # this raises ArgumentError, because &f 
  # isn't really an argument - it's only there 
  # to convert a block
  contrived(25, lambda {|x| puts x})

Като цяло затварянията са още една неясна, но полезна функция, внедрена в Ruby, която го прави толкова мощен език!