Давай представим ситуацию, когда тебе, в зависимости от какой-то логики нужно вернуть данные разного типа, например есть такая функция:
def function(foo, bar)
result = if foo > bar
10
else
"wrong"
end
end
Если мы захотим использовать такую функцию, то нам всегда придется проверять тип возвращаемого значения:
result = function(foo, bar)
if result.is_a?(Integer)
result * 100
else
0
end
это выглядит глупо, поэтому давай попробуем убрать данную проверку. Начнем городить абстракции. Для начала сделаем простой объект, который будет содержать в себе какое-то значение (любое):
class Value
def initialize(data)
@data = data
end
def data
@data
end
end
Теперь давай сделаем 2 класса, один будет нужен нам для "правильных" значений, TrueValue
, а другой для "не правильных", FalseValue
:
class Value
def initialize(data)
@data = data
@type = :true
end
def data
@data
end
def type
@type
end
end
class TrueValue < Value
def initialize(data)
@data = data
@type = :true
end
end
class FalseValue < Value
def initialize(data)
@data = data
@type = :false
end
end
Зачем мы сделали это? Теперь мы можем обернуть наши данные в "правильные" и "не правильные" value объекты, после чего, в зависимости от типа объекта работать с ними определенным образом:
def function(foo, bar)
result = if foo > bar
TrueValue.new(10)
else
FalseValue.new("wrong")
end
end
result = function(foo, bar)
Array(result)
.select { |value| value.type == true }
.map { |value| value.data * 100 } # => [1000]
Как видно, нам не очень удобно постоянно писать проверку на "правильные"данные, постоянно делать массив и так далее, поэтому давай сделаем функцию, которая будет делать все за нас:
def fmap(value)
Array(value).map do |value|
if value.data == :true
TrueValue.new(yield(value.data))
else
value
end
end
end
result = TrueValue.new(10)
fmap(result) { |v| v * 100 } # => TrueValue.new(1000)
result = FalseValue.new("wrong")
fmap(result) { |v| v * 100 } # => FalseValue.new("wrong")
Поздравляю, мы с тобой сделали нашу реализацию Either monad (http://dry-rb.org/gems/dry-monads/). Давай посмотрим на примеры из доки:
require 'dry-monads'
M = Dry::Monads
result = if foo > bar
M.Right(10)
else
M.Left("wrong")
end.fmap { |x| x * 2 }
# If everything went right
result # => Right(20)
# If it did not
result # => Left("wrong")
как видишь, мы с тобой сделали все один в один.
Теперь, давай подумаем, как можно сделать что-то действительно полезное с этим. Мы можем делать различные действия в зависимости от того, какой тип значения (TrueValue или FlaseValue) нам вернула функция, т.е.:
result = TrueValue.new(10)
result = monad_matcher.call(result) do |m|
m.true_value do |data|
"All is good. Our data: #{data}"
end
m.flase_value do |data|
"Oh no, we have a error, data: #{data}"
end
end
Но что бы не писать это самому, давай посмотрим на dry-matcher (http://dry-rb.org/gems/dry-matcher/):
require "dry-monads"
require "dry/matcher/either_matcher"
value = Dry::Monads::Either::Right.new("success!")
result = Dry::Matcher::EitherMatcher.(value) do |m|
m.success do |v|
"Yay: #{v}"
end
m.failure do |v|
"Boo: #{v}"
end
end
result # => "Yay: success!"
как видишь, один в один. Теперь у нас появилась абстракция для того, что бы работать с "правильными" и "не правильными" данными, что мы можем использовать где угодно. Из удобных юз кейсов: http реквесты, экшены и т.д.