-
Notifications
You must be signed in to change notification settings - Fork 93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixing the Many monad, respecting the specs #9
base: master
Are you sure you want to change the base?
Conversation
Here, I added a test that showcases the problem. You can checking out commit 6e3f0e6 and run the spec to see it failing, the next commit is the fix I proposed some times ago. Hope it helps, have fun ^^ |
That's seems like right solution. |
|
||
describe "unpacking nested many values" do | ||
let(:expected_values) do | ||
2.times.map do |i| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it is reasonable to use so many #map, #times and #flatten, just to introduce values we are expecting?
I think it would be more readable and useful (for understanding purpose) if you'll define simple arrays.
Just like in your issue.
WDYT?
Agreed, although I don't quite see how I can make the test more obvious ^^° The alternatives I tried were actually less obvious. I'll submit the best I can think of on top of my mind so we can compare. |
Well, here's another approach, with only one root and branch object. WDYT? |
describe "unpacking nested many values" do | ||
let(:expected_values) { %w(tiger lion hamster) } | ||
let(:branches) { [double(:branch, leaves: expected_values)] } | ||
let(:values) { [double(:root, branches: branches)] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's better a lot, but what's about example from your issue?
let(:expected_values) { %w(1984 Hamlet Macbeth) }
let(:orwel_books) { [double(:book, title: '1984')] }
let(:shakespeare_books) { [double(:book, title: 'Hamlet'), double(:book, title: 'Macbeth')] }
let(:values) { [double(:orwell, books: orwel_books), double(:shakespeare, books: shakespeare_books)] }
it { expect(many.books.title.values).to eq expected_values }
I didn't checked this. But something like that would be great I think (with more edge cases, maybe)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, let's see ^^
There it is ^^ |
cool. 👌 @tomstuart ? |
I agree that the implementation of The implementation of >> many = Many.new([[:a, [:b, :c]], [:d, [:e, :f]], [:g, [:h, :i]]])
=> …
>> many.slice(0, 1).values
=> [:a, :d, :g]
>> many.slice(0, 2).values
=> [:a, [:b, :c], :d, [:e, :f], :g, [:h, :i]]
>> many.reverse.values
=> [[:b, :c], :a, [:e, :f], :d, [:h, :i], :g] Issue #8 correctly identified that this gem, unlike the aforementioned code on which it is based, does the wrong thing: >> many = Many.new([[:a, [:b, :c]], [:d, [:e, :f]], [:g, [:h, :i]]])
=> …
>> many.slice(0, 1).values
=> [[:a], [:d], [:g]]
>> many.slice(0, 2).values
=> [[:a, [:b, :c]], [:d, [:e, :f]], [:g, [:h, :i]]]
>> many.reverse.values
=> [[[:b, :c], :a], [[:e, :f], :d], [[:h, :i], :g]] There is not enough flattening here; the result arrays should have been concatenated, but they haven’t been. (I missed this because However, the code in this pull request doesn’t do the right thing either: >> many = Many.new([[:a, [:b, :c]], [:d, [:e, :f]], [:g, [:h, :i]]])
=> …
>> many.slice(0, 1).values
=> [:a, :d, :g]
>> many.slice(0, 2).values
=> [:a, :b, :c, :d, :e, :f, :g, :h, :i]
>> many.reverse.values
=> [:b, :c, :a, :e, :f, :d, :h, :i, :g] Now there is too much flattening; the nested arrays have disappeared. This could be easily fixed by replacing How do we solve the underlying problem? |
I agree that |
Also agreed on my side... I think I have an idea on how to implement it without the .flatten(2) hack, I'll keep in touch. |
I've tried working around it but I keep on bumping on new problems ; there's something I'm missing, and I think it has something to do with the super-class, perhaps the way every new value is wrapped in an array in from_value. I'll keep trying after some cooldown period for my neurons ^^° |
What if we just change the code (on module Monads
class Many
# …
def self.from_value(value)
Many.new(Array(value))
end
end
end That makes my example above work correctly — is there a downside? |
Ok, I think I've had my epiphany of sorts. I've upped my PR with new code a test. Here's what went wrong if I'm actually right: # from lib/monads/monad.rb
def within(&block)
and_then do |value|
self.class.from_value(block.call(value))
end
end
# from the new take on lib/monads/many.rb
def self.from_value(value)
# since and_then can receive either a single value or values in something iterable,
# we test for :each to avoid re-wrapping the value in a redundant array
if value.respond_to? :each
Many.new(value)
else
Many.new([value])
end
end
def and_then(&block)
block = ensure_monadic_result(&block)
# now since we *necessarily* get a nested array from values.map(&block).map(&:values),
# we still need to flatten it, but we need to flatten only one level, the outer wrapping array
# that is now redundant ^^
Many.new values.map(&block).map(&:values).flatten(1)
end |
Okay, great, that’s similar to what I suggested. And your |
Glad to be of service ^^ As for the: module Monads
class Many
# …
def self.from_value(value)
Many.new(Array(value))
end
end
end I'm not sure how it's supposed to work but it breaks almost everything when I'm trying it (returning to the code as it was or as it is now). Is there something I'm missing there? ^^° |
It's seems like Array({a: 'b'}) # => [[:a, "b"]] So solution with |
Yep, as far as I can see, The only thing I see so far against the "each" fix is that you could implement "each" on an object that does not behave like a list of values, but I don't see how that would be a major problem. WDYT? |
Hmm. I checked it again. With > Monads::Many.from_value({a: 1})
=> #<Monads::Many:0x000000018ffb00 @values={:a=>1}>
> Monads::Many.from_value({a: 1}).keys
NoMethodError: undefined method `keys' for [:a, 1]:Array |
So lastly, it seems like we should check type. If current value is an Array, then skip wrapping, and wrap otherwise. |
The underlying problem is that >> words = Many.new(['foo', 'bar'])
=> …
>> words.upcase.values
=> ["FOO", "BAR"] # not flattened, because String#upcase returns a string
>> words.chars.values
=> ["f", "o", "o", "b", "a", "r"] # flattened, because String#chars returns an array (except in Ruby 1.9.3!) There is no watertight way to have both, and it doesn’t make algebraic sense. I think we should pick one of the two behaviours and stick with it. |
Good points. Well, at least insuring we don't wrap an Array in another unnecessary Array makes a resonable amount of sense. I'll make a spec with the hash example and improve my PR as soon as I have time. For the string example, this behaviour is about what I'd expect the "Many" to do so I wasn't surprised, so at least it seems like it wouldn't break the principle of least surprise. |
Trying to fix this issue: #8
The problem was that flat_map didn't actually necessarily return a flat array, just tore down one layer of array returned by the block, leaving a nested array standing.
This version keeps the specs running green, though I haven't yet put a spec to check there's no backsliding.