monads icon indicating copy to clipboard operation
monads copied to clipboard

Optional plays nice with Hashie::Mash. Should it play nice with ActionController::Parameters and OpenStruct too?

Open olistik opened this issue 10 years ago • 1 comments

I was experimenting the use of Monads::Optional with chains of calls that imply hash access, such as when dealing with Rails parameters:

params[:profile][:resume].tempfile.path

Initially I thought that wrapping an OpenStruct around a Hash would've been a good fit for Monads::Optional but then I discovered that it doesn't work. Here's a list of tests that I've run:

require 'monads'
require 'hashie'
require 'ostruct'

require 'action_pack'
require 'rack/test'
require 'action_controller'

params = {
  foo: {
    bar: 42
  }
}
rails_params = ActionController::Parameters.new(params)
ostruct_params = OpenStruct.new(params)
hashie_params = Hashie::Mash.new(params)
hashie_rails_params = Hashie::Mash.new(rails_params)
ostruct_rails_params = OpenStruct.new(rails_params)

Monads::Optional.new(ostruct_params).foo
 # => #<struct Monads::Optional value={:bar=>42}>

Monads::Optional.new(hashie_params).foo
 # => #<struct Monads::Optional value=#<Hashie::Mash bar=42>>

Monads::Optional.new(ostruct_rails_params).foo
 # => #<struct Monads::Optional value={"bar"=>42}>

Monads::Optional.new(hashie_rails_params).foo
 # => #<struct Monads::Optional value=#<Hashie::Mash bar=42>>

Monads::Optional.new(ostruct_params).foo.bar
# NoMethodError: undefined method `bar' for {:bar=>42}:Hash
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:11:in `public_send'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:11:in `block in method_missing'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:5:in `call'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:5:in `block in within'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:21:in `call'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:21:in `block in ensure_monadic_result'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/optional.rb:13:in `call'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/optional.rb:13:in `and_then'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:4:in `within'
  # from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:10:in `method_missing'
  # from (irb):23
  # from /Users/olistik/.rvm/rubies/ruby-2.2.2/bin/irb:11:in `<main>'

Monads::Optional.new(hashie_params).foo.bar
# => #<struct Monads::Optional value=42>

Monads::Optional.new(ostruct_rails_params).foo.bar
# NoMethodError: undefined method `bar' for {"bar"=>42}:ActionController::Parameters
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:11:in `public_send'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:11:in `block in method_missing'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:5:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:5:in `block in within'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:21:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:21:in `block in ensure_monadic_result'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/optional.rb:13:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/optional.rb:13:in `and_then'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:4:in `within'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:10:in `method_missing'
#   from (irb):25
#   from /Users/olistik/.rvm/rubies/ruby-2.2.2/bin/irb:11:in `<main>'

Monads::Optional.new(hashie_rails_params).foo.bar
# => #<struct Monads::Optional value=42>

Monads::Optional.new(ostruct_params).foo.bar.value
# NoMethodError: undefined method `bar' for {:bar=>42}:Hash
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:11:in `public_send'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:11:in `block in method_missing'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:5:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:5:in `block in within'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:21:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:21:in `block in ensure_monadic_result'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/optional.rb:13:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/optional.rb:13:in `and_then'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:4:in `within'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:10:in `method_missing'
#   from (irb):27
#   from /Users/olistik/.rvm/rubies/ruby-2.2.2/bin/irb:11:in `<main>'

Monads::Optional.new(hashie_params).foo.bar.value
# => 42

Monads::Optional.new(ostruct_rails_params).foo.bar.value
# NoMethodError: undefined method `bar' for {"bar"=>42}:ActionController::Parameters
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:11:in `public_send'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:11:in `block in method_missing'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:5:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:5:in `block in within'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:21:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:21:in `block in ensure_monadic_result'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/optional.rb:13:in `call'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/optional.rb:13:in `and_then'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:4:in `within'
#   from /Users/olistik/.rvm/gems/ruby-2.2.2/gems/monads-0.0.1/lib/monads/monad.rb:10:in `method_missing'
#   from (irb):29
#   from /Users/olistik/.rvm/rubies/ruby-2.2.2/bin/irb:11:in `<main>'

Monads::Optional.new(hashie_rails_params).foo.bar.value
# => 42

It looks like that the only viable solution is to wrap params into a Hashie::Mash and then pass it to Monads::Optional.

Would you like/expect to have an implementation that supports OpenStruct and ActionController::Parameters?

olistik avatar Aug 11 '15 16:08 olistik

Hi @olistik can you explain to me why are you using such examples? in my thoughts the valid examples should be:

require 'monads'
require 'ostruct'

require 'action_pack'
require 'rack/test'
require 'action_controller'

params = {
  foo: {
    bar: 42
  }
}
rails_params = ActionController::Parameters.new(params)
ostruct_params = OpenStruct.new(params)
ostruct_rails_params = OpenStruct.new(rails_params)

Monads::Optional.new(ostruct_params).foo.bar
# NoMethodError: undefined method `bar' for {:bar=>42}:Hash
# ... but
Monads::Optional.new(ostruct_params).foo[:bar]
# => #<struct Monads::Optional value=42>

Monads::Optional.new(ostruct_rails_params).foo.bar
# NoMethodError: undefined method `bar' for {"bar"=>42}:ActionController::Parameters
# ... but
Monads::Optional.new(ostruct_rails_params).foo[:bar]

Monads::Optional.new(ostruct_params).foo.bar.value
# NoMethodError: undefined method `bar' for {:bar=>42}:Hash
# ... but
Monads::Optional.new(ostruct_params).foo[:bar].value
# => 42

Monads::Optional.new(ostruct_rails_params).foo.bar.value
# NoMethodError: undefined method `bar' for {"bar"=>42}:ActionController::Parameters
# ... but
Monads::Optional.new(ostruct_rails_params).foo[:bar].value
# => 42

Or I misunderstand something ?

sclinede avatar Feb 20 '16 13:02 sclinede