dry-monads icon indicating copy to clipboard operation
dry-monads copied to clipboard

Can't extract wrapped array through pattern-matching

Open waiting-for-dev opened this issue 2 years ago • 8 comments

Describe the bug

When a right-biased type is wrapping an array value, it's not possible to unwrap it via pattern-matching.

To Reproduce

require "dry/monads"
include Dry::Monads[:result]
Success(1) in Success(x) # => true
x # => 1 AS EXPECTED
Success([1]) in Success(x) # => true
x # => 1 IT SHOULD BE [1]
Success([1, 2]) in Success(x) # => true
x # => 1 IT SHOULD BE [1, 2]

Expected behavior

The wrapped value should be extracted as it is.

My environment

  • Affects my production application: NO
  • Ruby version: v3.2

Proposed solution

We should stop branching depending on whether the wrapped value is an array. Instead, #deconstruct should be simply:

def deconstruct
  if Unit.equal?(@value)
    EMPTY_ARRAY
  else
    [@value]
  end
end

However, the above would be a breaking change. Now we have:

Success([1]) in [1] # => true

The above would change:

Success([1]) in [1] # => false

IMO, we should nonetheless change it, as the most common use-case for results is extracting the wrapped value after some operations have been performed.

I'm happy to submit a PR fixing the issue. If we consider it as a bug fix, it should go into the next minor or patch release. However, if we consider it as a new feature breaking code, we should wait until v2.0.

waiting-for-dev avatar Oct 02 '23 11:10 waiting-for-dev

It is intentional, it's also documented in the docs. I don't remember the details but I think it's related to using PM in the following scenario:

case Failure[:error, 'oops']
in Failure[:error, message]
  message # => 'oops'
end

Again, I don't remember details but I think removing the branch will force using in Failure([:error, message]) which is inconsistent with using the constructor.

flash-gordon avatar Oct 02 '23 14:10 flash-gordon

Thanks for getting back, @flash-gordon. I'm failing to understand the inconsistency with the constructor, shouldn't we use Failure([:error, :message]) when using it? :thinking:

waiting-for-dev avatar Oct 02 '23 14:10 waiting-for-dev

IIRC the shorthand Failure[error_code, message] was added some time before PM was a thing in ruby. Then, in order to support it in PM, I had to add that hack. The justification was using arrays of variable length is a lot less common then using arrays as tuples for encoding errors. Unfortunately, for consistency I had to make both Failure and Success work with arrays in the same manner. Clearly, it's a trade off.

flash-gordon avatar Oct 02 '23 15:10 flash-gordon

I see. Do you see it as something we could change in a major release (including the constructor)? Surely not the end of the world, but it's a pity we can't blindly use pattern matching for any kind of wrapped value and need to rely on not-as-pure methods like value! :slightly_smiling_face:

waiting-for-dev avatar Oct 02 '23 15:10 waiting-for-dev

For context, I'd like to use it for the code at https://github.com/dry-rb/dry-operation/pull/6

waiting-for-dev avatar Oct 02 '23 15:10 waiting-for-dev

I see. Do you see it as something we could change in a major release (including the constructor)? Surely not the end of the world, but it's a pity we can't blindly use pattern matching for any kind of wrapped value and need to rely on not-as-pure methods like value! :slightly_smiling_face:

Not likely at this point. I have hundreds if not thousands of usage in my codebase alone, changing them all would be a time-consuming task. On the other hand, I paid attention to this caveat, and so far I've got only a couple of questions about this documented behavior. How did you come across this issue?

flash-gordon avatar Oct 02 '23 19:10 flash-gordon

How did you come across this issue?

While working on dry-operation, we need to unwrap intermediate steps (or throw on failure). That needs to be done blindly, without knowing whether the inner type is an array or not. I was hoping to do it through pattern-matching for a couple of reasons:

  • It's the canonical way to destructure composite types.
  • I'm considering the idea of making it independent of the underlying data type (e.g., use it with something different from dry-monads). Implementing pattern-matching would be a lighter requirement than, for instance, implementing value! or value_or {}.

waiting-for-dev avatar Oct 03 '23 03:10 waiting-for-dev

Ah, I see. I think it's ok not to use PM here, it's library code so matching cases it not as important. Moreover, calling .success? and value! is likely faster.

flash-gordon avatar Oct 03 '23 14:10 flash-gordon