Говоря о различиях между proc и lambda, как правило, достаточно знать, что оператор return в случае proc будет выкидывать из текущего метода, а в случае lambda - из самой лямбды.

Собственно, для простоты можно сказать, что Proc, создаваемый с помощью метода proc, является объектным представлением блока, а, создаваемый с помощью lambda или его сахара ->() - чем-то более приближенным к объектному представлению метода (но не стоит путать с Method).

Но в чем же действительно разница?

Ох уж этот return

Оператор return в Ruby весьма и весьма странный. Ожидаемо, он возвращает значение. Но посмотрите на этот кусок кода из моего любимого справочника по Ruby от Мацумото:

def procBuilder(message)
  Proc.new { puts message; return }
end

def test
  puts 'entering method'
  p = procBuilder('entering proc')
  p.call                # prints 'entering proc' and raises LocalJumpError
  puts 'exiting method' # it is never executed
end

test

Все потому, что Proc объект создается из блока, который объявляется в методе procBuilder, а блок “захватывает” return из текущего метода.

Соответственно, когда мы вызываем p.call, выполняется блок и пытается выйти из метода, который уже давно завершен!

Поэтому важно не относиться к Proc, как к объектному представлению блоков кода, потому что на самом деле они дают лишь обертку над ними, чтобы предоставить возможность сохранить их в переменную и передавать между методами.

Кстати, обратите внимание, что если убрать из блока return, то все становится хорошо и радужно, т.к. в случае неявного (без использования return) возврата значения, блок возвращает его сам по себе, как если бы это был вызов метода.

lambda

Лямбды в руби - это и вовсе нечто волшебное. Если в случае с примером выше все более-менее понятно, то почему же lambda, которая тоже создается из блока, не “хватает” return текущего метода?

Этот вопрос оставляю нерешенным. Чтобы с ним разобраться, нужно попрыгать по сишному коду интерпретатора. Боюсь, это может отнять слишком много времени, и мое любопытство того не стоит.

Кстати, помимо операторов передачи управления лямбды до кучи еще и чувствительны к списку аргументов.

Различия в операторах

Т.к. return уже был разобран, не вижу смысла останавливаться на нем подробно.

break

У блоков и proc, от которых ожидается поведение, идентичное блоку, break возвращает управление методу, в котором был объявлен блок.

Пример:

def test
  puts 'entering #test'
  some_method { break }
  puts 'exit #test'
end

def some_method(&block)
  puts 'entering #some_method'
  [1, 2, 3].each(&block)
  puts 'exit #some_method' # never reached
end

test

# output:
#
# entering #test
# entering #some_method
# exit #test

У лямбды же break ведет себя точно так же, как и return, так что нет никакого смысла в том, чтобы использовать его внутри lambda:

f = -> { break 1 }
f.call # => 1

next

Оператор next везде работает одинаково: досрочно возвращает значение из текущего контекста. Думаю, многие новички обжигаются при использовании return в блоке:

[1, 2, 3, 4].map do |e|
  return e if e < 3

  abracadabra(e)
  another_abracadabra(e)
  e ** 2
end

Другие операторы

Еще есть redo, yield, retry, raise.

Почти все они работают везде одинаково, кроме retry. Его использование не позволяется для Proc объектов (не больно-то и хотелось).

Аргументы

При передаче аргументов в блок работает принцип, как при следующей конструкции:

x1, x2, x3, ..., xn = val1, val2, ...

Соответственно, если значений будет не хватает, то переменные инициализируются с nil. Если справа значений больше, чем слева, то лишние просто проигнорируются.

Если справа значение одно, и это массив, то он распакуется:

x, y = [1, 2, 3] # x = 1, y = 2

Для лямбд же работают те же правила, что и для метода. Ни о каком автоматическом распаковывании и речи не идет, количество аргументов должно быть четко таким, как заявлено:

f = -> (x) { x * x }
f[1, 2] # => ArgumentError

Заключение

А не будет его. Мне просто нужно было немного систематизировать инфу.