OO programmers, Monads may be useful to us too

Hello, I saw the “Monad” concept at first time at TDC(São Paulo) 2016 at the functional programming Track. I didn’t get any use possibility in speech (maybe because I don’t know Haskell), but three months ago, when I started to work in a Java project with two collegues, a senior job colleague convinced me to use java.util.Optional in some cases along this project “It’s cool”, he said. He already worked with Scala using some monadics built-in tools, then he knows the monads advantages.

This project has many sequencial steps, the code majority has to validate something, convert to another type, save in database, send to a queue and so on. After many sprints interactions, when it started to take form, the code did everything needed keeping all things simple basically composing a lot of Functions applied in Java Optional Objects.

After that, when I came back to Ruby, I started to search some gem with the similar behavior, I googled it and I found Dry-Monads inspired by Kleisli. When I visited the Kleisli page I saw this phrase “You can use Haskell-like function composition with F and the familiar”, I thought “Oh my Gosh, I got Monads now”. Moments later, I found a cool post in Quora website about Monads, it’s different(and cool) because don’t envolves a huge number of mathematical formulas to explain the Monads concept, you can take a look here. He uses Haskell in examples, I don’t know Haskell but when if you compare to Dry-monads you will understand too.

Introduction

Everyone that already programmed in Java, at least once, got an NullPointerException. This error occurs when someone call a function to null reference.

null.toString();

java.lang.NullPointerException

If we use Ruby:

2.3.1 :004 > nil.to_s
 => ""
 2.3.1 :005 > nil.methods
  => [:&, :^, :|, :===, :inspect, :to_a, :to_s, :to_i, :to_f, :nil?, ... , :instance_eval, :instance_exec, :__id__]

2.3.1 :006 > nil.bla
NoMethodError: undefined method `bla' for nil:NilClass
  from (irb):6
    from /home/gabriel/.rvm/rubies/ruby-2.3.1/bin/irb:11:in `<main>'

Ruby return an exception when we try to call an inexistent attribute or method, but when we call ‘.to_s’ it returns a blank string. If we use Java, when we call any method to null, it will return an “NullPointerException”.

Since Java 8, we have the java.util.Optional, it can be useful in this case, to return an empty string when we use a null reference. Let’s see the example below:

java.util.Optional.ofNullable(null).map(Object::toString).orElse("");

//And it returns
""

It’s extremely useful to avoid Java NullPointerException. Another cool thing is the chain methods possibility, we can do it using some functions as ‘#map’ and ‘#flatMap’. We can also use the Optional behavior to avoid ‘if’ sequences and we can to turn our code more readable using the same text way. In Elixir we can chain methods using the pipe operator ‘|>’, let’s check it.

"Some String" |> String.upcase

//And it returns
"SOME STRING"

We can do it in Java too.

java.util.Optional.ofNullable("Some String")
                    .map(String::toUpperCase).get();

//And it returns
"SOME STRING"

Starting

We used this behavior many times in our project than, in this post, I will try show a similar concept using Dry-Monads in a simple ‘Order’ example.

Ok, let’s start, Our very simple use case is:

/*
     We have an Order, this Order has a Products, every Product has one price, but it can be nil to gifts, for example. The products sum can't be negative. An Order has a discount. It can be informed or no. Our task is calculate the order total.

     Order
       has -> products
       calc -> total - discount

     Product
       price
*/

First, we need an Order class

class Order
  def total
    @products.pluck(:price).reduce(:+) - discount
  end

  def add_product(product)
    @products = Array(@products) << product
  end

  def discount=(value)
    @discount = value
  end

  def discount
    @discount
  end

  def products=(value)
    @products = value
  end

  def products
    @products
  end
end

Ps:. I wrote all getter and setters to turn easier to understand.

And the Product class

class Product
  def initialize(price:)
    @price = price
  end

  def price
    @price
  end
end

Now, let’s check if works creating an Order and some products

2.3.1 :001 > order = Order.new
=> #<Order:0x000000031aa0d8>
2.3.1 :002 > products = []
=> []
2.3.1 :003 > 10.times { products << Product.new(price: Random.rand * 10) }
=> 10
2.3.1 :004 > order.products = products
=> [#<Product:0x000000046b2a70 @price=6.490346125032528>, [...More products here], #<Product:0x000000046b24f8 @price=4.088797576323275>, #<Product:0x000000046b2430 @price=5.311290981668898>]
2.3.1 :005 > order.discount= 10
 => 10
 2.3.1 :006 > order.total
  => 34.6909868839012

Awesome, it works! But if we set discount as nil…

2.3.1 :009 > order.discount = nil
 => nil
 2.3.1 :010 > order.total
 TypeError: nil can't be coerced into Float
  from /home/gabriel/projects/ruby/blog/app/models/order.rb:3:in '-'

Oh :( it’s bad. Do you know what happen if at least one product has a nil price?

2.3.1 :011 > order.add_product(Product.new(price: nil))
 => [#<Product:0x000000046b2a70 @price=6.490346125032528>, [...More products here], #<Product:0x000000046b2430 @price=5.311290981668898>, #<Product:0x000000045fc108 @price=nil>]
 2.3.1 :012 > order.total
 TypeError: nil can't be coerced into Float
  from /home/gabriel/projects/ruby/blog/app/models/order.rb:3:in '+'

Is it easy to correct, right? We can add the code below to avoid these exceptions.

class Product
...
  def price
    @price || 0
  end
end

class Order
 ...
  def discount
    @discount || 0
  end
end
2.3.1 :001 > order = Order.new
=> #<Order:0x00000004d27a68>
2.3.1 :002 > products = []
=> []
2.3.1 :003 > 10.times { products << Product.new(price: Random.rand * 10) }
=> 10
2.3.1 :004 > order.products = products
=> [#<Product:0x00000004d09248 @price=1.7050223472417392>, [...More products here], #<Product:0x00000004d08b90 @price=1.9066992998913168>, #<Product:0x00000004d08b18 @price=3.178449375930751>]
2.3.1 :005 > order.total
=> 41.55253855960062
2.3.1 :006 > order.add_product(Product.new(price: nil))
=> [#<Product:0x00000004d09248 @price=1.7050223472417392>, [...More products here], #<Product:0x00000004d08b18 @price=3.178449375930751>, #<Product:0x00000004cd1b40 @price=nil>]
2.3.1 :007 > order.total
=> 41.55253855960062

Magic, it’s works! but could we call an order without products?

2.3.1 :008 > order.products = nil
 => nil
 2.3.1 :009 > order.total
 NoMethodError: undefined method 'map' for nil:NilClass

Oh damn it, we can use the ‘||’ operator too or Array(value), but we can try something different using dry-monads. Let’s change the code!

class Product
  def initialize(price:)
    @price = price
  end

  def price
    Dry::Monads::Maybe(@price)
  end
end

// And the order class

class Order
  def total
    products.bind(-> (products) { products.map { |p| p.price.value_or(0) }.reduce(:+) - discount.value_of(0) })
  end

  def add_product(product)
    @products = Array(@products) << product
  end

  def discount=(value)
    @discount = value
  end

  def discount
    Dry::Monads::Maybe(@discount)
  end

  def products=(value)
    @products = value
  end

  def products
    Dry::Monads::Maybe(@products)
  end
end

We used Maybe in this case to get an Optional behavior. It will return None if we pass null or Some in non null cases as code below.

2.3.1 :003 > Dry::Monads::Maybe(nil)
=> None
2.3.1 :004 > Dry::Monads::Maybe(1)
=> Some(1)

The bind function was used to apply a function in object. We can use it to do an Upcase like previously.

2.3.1 :005 > Dry::Monads::Maybe("Bla").bind(->(str) { str.upcase })
=> "BLA"
2.3.1 :006 >Dry::Monads::Maybe("Bla").bind(&:upcase)
=> "BLA"

Then let’s try the new version

2.3.1 :009 > order = Order.new
=> #<Order:0x00000004d11e70>
2.3.1 :010 > products = []
=> []
2.3.1 :011 > 10.times { products << Product.new(price: Random.rand * 10) }
=> 10
2.3.1 :012 > order.products = products
=> [#<Product:0x00000004cf1788 @price=8.306382864273731>, [...More products here], #<Product:0x00000004cf1080 @price=0.4676475154069548>]
2.3.1 :013 > order.total
=> 38.75444516036125
2.3.1 :014 > order.products = nil
=> nil
2.3.1 :015 > order.total
=> None

It’s works, but the total function isn’t readable, we can improve it, but first, take a look:

  def total
    products
      .bind(-> (products) { products.map { |p| p.price.value_or(0) }.reduce(:+) - discount.value_of(0) })
  end

The total function has three responsabilities, it collects all products price, sum it and subtract the discount. We can break it into 3 separated functions, something like the code below.

class Order
  def total
    products
      .fmap(collect_prices)
      .fmap(sum)
      .bind(subtract_discount)
  end

  def add_product(product)
    @products = Array(@products) << product
  end

  def discount=(value)
    @discount = value
  end

  def discount
    Dry::Monads::Maybe(@discount)
  end

  def products=(value)
    @products = value
  end

  def products
    Dry::Monads::Maybe(@products)
  end

  private

  def collect_prices
    -> (products) { products.map { |p| p.price.value_of(0) } }
  end

  def sum
    -> (prices) { prices.reduce(:+) }
  end

  def subtract_discount
    ->(total) { discount.fmap(-> (discount) { total - discount }).or(total) }
  end
end

The code above used a #fmap function, it works like the bind function but it wrap the result into Some. The #fmap function returns None if applied in a None object. It works like the example below.

2.3.1 :003 > Dry::Monads::Some("Bla").fmap(&:upcase)
 => Some("BLA")
 2.3.1 :005 > Dry::Monads::Maybe(nil).fmap(&:upcase)
 => None

It’s cool, but now we need to add another feature. If our order doesn’t has products, we don’t need to calculate the total in these cases, then we can add this behavior using the ‘Either mixin’. Let’s see how we can use it. We will create a new command class ‘Commands::Order::Total’ and use it to calculate the order price.

class Order
  def add_product(product)
    @products = Array(@products) << product
  end

  def discount=(value)
    @discount = value
  end

  def discount
    Dry::Monads::Maybe(@discount)
  end

  def products=(value)
    @products = value
  end

  def products
    Dry::Monads::Maybe(@products)
  end
end

class Commands::Order::Total
  include Dry::Monads::Either::Mixin

  def call(order)
     order.products
      .bind(collect_prices)
      .fmap(sum)
      .bind(subtract_discount(order))
  end

  private

  def collect_prices
    -> (products) {
      return Left('You should add products') if products.empty?
      Right(products.map { |p| p.price.value_or(0) })
    }
  end

  def sum
    -> (prices) { prices.reduce(:+) }
  end

  def subtract_discount(order)
    ->  (total) { order.discount.fmap(-> (discount) { total - discount }).or(total) }
  end
end

/* Some examples */

2.3.1 :001 > order = Order.new
=> #<Order:0x00000004eea670>
2.3.1 :002 > products = []
=> []
2.3.1 :003 > 10.times { products << Product.new(price: Random.rand * 10) }
=> 10
2.3.1 :004 > calc = Commands::Order::Total.new
=> #<Commands::Order::Total:0x00000004e96c28>
2.3.1 :005 > calc.call order
=> None
2.3.1 :006 > order.products = []
=> []
2.3.1 :007 > calc.call order
=> Left("You should add products to calculate the total")
2.3.1 :008 > order.products = products
=> [#<Product:0x00000004ed4af0 @price=3.8706171589310223>, [...More products here], #<Product:0x00000004ed4668 @price=3.8299514817332634>, #<Product:0x00000004ed45f0 @price=0.595434488114438>]
2.3.1 :009 > calc.call order
=> 37.45434887890415
2.3.1 :010 > order.discount = 10
=> 10
2.3.1 :011 > calc.call order
=> Some(27.45434887890415)

The Dry-Monads page describe Left and right as “The Right can be thought of as “everything went right” and the Left is used when “something has gone wrong.”. Left is something like an exception, then when we have a function that returns Left the next chain function isn’t executed. Right is used when we have an expected result, then it can pass to next chain function.

It works! but the code returns None, Some or Left in some cases. If we use only Left and right we can check if we had success or no. Another thing is the sum of prices can be negative, we can return an failure if it occurs. Let’s make some changes.

class Commands::Order::Total
  include Dry::Monads::Either::Mixin

  def call(order)
    order.products
    .bind(collect_prices)
    .bind(sum)
    .bind(subtract_discount(order))
  end

  private

  def collect_prices
    -> (products) {
      return Left('You should add products') if products.empty?
      Right(products.map { |p| p.price.value_or(0) })
    }
  end

  def sum
    -> (prices) {
      total = prices.reduce(:+)
      return Left('The total can\'t be negative') if total < 0
      Right(total)
    }
  end

  def subtract_discount(order)
    ->(total) {
      order.discount
        .bind(-> (discount) { total - discount })
        .bind(-> (total) { Right(total) } )
        .or(Right(total))
    }
  end
end

2.3.1 :039 > order.products = products
=> [#<Product:0x000000043358e8 @price=0.3621462392006869>, [...More products here], #<Product:0x000000044ea0a8 @price=50>, #<Product:0x000000044da630 @price=-120>]
2.3.1 :040 > calc.call order
=> Left("The total can't be negative")
2.3.1 :041 > order.products = []
=> []
2.3.1 :042 > calc.call order
=> Left("You should add products")
2.3.1 :043 > order.add_product(Product.new(price: 300))
=> [#<Product:0x0000000414a088 @price=300>]
  2.3.1 :044 > calc.call order
=> Right(300)
  2.3.1 :045 > order.add_product(Product.new(price: 300))
  => [#<Product:0x0000000414a088 @price=300>, #<Product:0x0000000410ded0 @price=300>]
  2.3.1 :046 > calc.call order
=> Right(600)
2.3.1 :058 > order.discount = 10
 => 10
 2.3.1 :059 > calc.call order
=> Right(590)
2.3.1 :017 > Commands::Order::Total.new.call order
 => Right(51.08628673669591)
2.3.1 :014 > 10.times { products << Product.new({price: Random.rand * 10}) }
 => 10
 2.3.1 :015 > products
 => [#<Product:0x000000052ff4b8 @price=1.8427640303941961>, [...More products here], #<Product:0x000000052feef0 @price=9.708438235799372>]
 2.3.1 :016 > order.products = products
 => [#<Product:0x000000052ff4b8 @price=1.8427640303941961>, [...More products here], #<Product:0x000000052feef0 @price=9.708438235799372>]
2.3.1 :018 > r=_
=> Right(51.08628673669591)
2.3.1 :019 > r.success?
=> true
2.3.1 :020 > products << Product.new({price: -60})
=> [#<Product:0x000000052ff4b8 @price=1.8427640303941961>, [... More products here], #<Product:0x00000005252fd8 @price=-60>]
2.3.1 :021 > order.products = products
=> [#<Product:0x000000052ff4b8 @price=1.8427640303941961>, [...More products here] #<Product:0x00000005252fd8 @price=-60>]
2.3.1 :022 > Commands::Order::Total.new.call order
=> Left("The total can't be negative")
2.3.1 :023 > r=_
=> Left("The total can't be negative")
2.3.1 :024 > r.success?
=> false

That’s all, Is it cool, right? If you like it you can check the page project here, there are many amazing features.

Thanks!

References

  • http://dry-rb.org/gems/dry-monads/
  • https://medium.com/@sinisalouc/demystifying-the-monad-in-scala-cc716bb6f534#.g2jyjrme3
  • https://www.quora.com/What-are-monads-in-functional-programming-and-why-are-they-useful
  • https://github.com/txus/kleisli