Skip to content

Instantly share code, notes, and snippets.

@davydovanton
Last active September 5, 2019 16:48
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davydovanton/cdb5232284dae74667e24b5939f1a5c8 to your computer and use it in GitHub Desktop.
Save davydovanton/cdb5232284dae74667e24b5939f1a5c8 to your computer and use it in GitHub Desktop.

Давай представим ситуацию, когда тебе, в зависимости от какой-то логики нужно вернуть данные разного типа, например есть такая функция:

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 реквесты, экшены и т.д.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment