Skip to content
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

enumerable methods not correctly iterating over an enumerable #16

Closed
HoneyryderChuck opened this issue Sep 7, 2015 · 4 comments
Closed

Comments

@HoneyryderChuck
Copy link

I'm finding this behaviour non-intuitive in terms of applying the monad on enumerable objects, like arrays. For instance, here we have the example from the tutorial:

Maybe([nil, 1, 2, 3, 4, 5, 6].sample)
#=> <Some:0x007ffe198e6128 @value="I'M A VALUE">

when we do:

Maybe([nil, 1, 2, 3, 4, 5, 6])
#=> <Some:0x007ffe198e6128 @value="[nil, 1, 2, 3, 4, 5, 6]">

this is where I get confused. I'll show you my expectation and what it does:

Maybe([nil, 1, 2, 3]).reduce(0, :+)
#=> expecting 6, throws TypeError:Array can't be coerced into Fixnum

So, my expecting in this case, when using enumerable method on the monad, was that the monad would yield monads, which is not. Is this a valid expectation?

@rap1ds
Copy link
Owner

rap1ds commented Sep 8, 2015

@TiagoCardoso1983 Remember that the Maybe is just a thin Array-like wrapper. You can think Some as an array of one element ([1]) and None as an array of no elements ([]). If the value for the Some is an array, then you can think it like an array of array.

So, Maybe([nil, 1, 2, 3]) is like [[nil, 1, 2, 3]].

And Maybe([nil, 1, 2, 3]).reduce(0, :+) is like [[nil, 1, 2, 3]].reduce(0, :+) and that gives you the error you saw.

Since Maybe implements the Enumerable interface, all Enumerable methods (like reduce) are applied to the Maybe object, not to the value object. If you want to apply the Enumerable method to the value object, then you need to map first.

Maybe([1, 2, 3]).map { |xs| xs.reduce(0, :+) } #=> Some(6)

If this seems complicated, again, you can think it like an array:

[[1, 2, 3]].map { |xs| xs.reduce(0, :+) }

There is an open Pull Request #7, which implements inner method, which can be used to pass the method call directly to the value object. With inner you don't need the map:

Maybe([1, 2, 3]).inner.reduce(0, :+) #=> Some(6)

The PR #7 is still open, mostly because I'm not too happy with the method name inner...

@rap1ds rap1ds closed this as completed Sep 8, 2015
@HoneyryderChuck
Copy link
Author

I understood the thin-wrapper logic, I was just making the point, it would make sense to me to delegate enumerable methods to the elements of the Some instance, i.e., if Some is composed of:

# one could deleate map calls to the enumerables in the value container
=> Maybe([1, 2, 3]).reduce(0, :+)
#=> 6

So, just making a case that it could potentially make sense to handle it this way. The #inner method implementation, while going in that direction, seems to not yield Maybe's, which is the whole point of the other enumerable. I just wanted to know potential drawbacks from this approach.

@rap1ds
Copy link
Owner

rap1ds commented Sep 9, 2015

So what you're proposing is that Maybe should not implement the Enumerable interface, as it currently does? Did I understand right? Fair enough.

(Btw. is there a mistake in your example? Did you really mean that reduce should return unwrapped value 6, or maybe Some(6)?)

The Enumerable interface provides some methods that are pretty handy with Maybe, e.g. each, select, reject, and of course map. Also, other enumerable methods can be used, (e.g. reduce, like Maybe(10).reduce(10, :*) #=> 100), but I'm not sure how useful those are in the real world.

So if Maybe would not implement Enumerable, at least some form of map, each, select and reject should be implemented by Maybe. Those methods should be named to something different than what they are now (map could be fmap, select could be filter, each could be on_value for example). If those methods had names that do not collapse with the Enumerable method names, then it would be possible to forward the Enumerable methods to the value inside the wrapper. On the one hand changing the names might be a good thing, since for example on_value is a lot better name than each. On the other hand that may be a bad thing, since Enumerable methods are already familiar to all and people know what they do.

In addition, this would be a change that will break a lot of code. At least we at Sharetribe are using Maybe quite heavily, and a lot of code would break. That's definitely a drawback :)

Thanks for your comments! I'll think about it. I do agree that the behaviour is somewhat non-intuitive. I have personally made mistakes where I have called enumerable method and actually intended that method call to be passed to the value inside the wrapper (i.e. called Maybe([1, 2, 3]).first, when I actually meant Maybe([1, 2, 3]).map { |xs| xs.first })

The #inner method definitely yields Maybe values. Maybe([1, 2, 3]).inner.reduce(0, :+) is equal to Maybe([1, 2, 3].reduce(0, :+)), which both return Some(6).

@HoneyryderChuck
Copy link
Author

I might be going out of scope with my request on what the Maybe monad should provide, but then again, I only read Haskell and never wrote code myself.

My understanding of the Monads is that it behaves the same whether the value is one or many. Therefore I think that Some should be an Enumerable and should be able to implement #each. If the value would be an enumerable, block would be passed to value wrapping yielded values as Some, and if a normal value, wrapped it as Some and yield. It would also potentially have the benefit of decrease array allocations, depending of how much interaction there is with that __enumerable_value method. I think Some (and therefore None) should be de facto Enumerables instead of reimplementing all Enumerable methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants