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

Document how sum types works with structs

Open solnic opened this issue 4 years ago • 4 comments

refs #158

solnic avatar May 22 '20 11:05 solnic

For what it's worth, this was very unexpected behavior to me:

class A < Dry::Struct
  attribute :id,    Types::Strict::Integer
  attribute :type,  Types::Strict::String
  attribute :name,  Types::Strict::String
end

class B < Dry::Struct
  attribute :id,               Types::Strict::Integer
  attribute :type,             Types::Strict::String
  attribute :name,             Types::Strict::String
  attribute :primary_function, Types::Strict::String.optional
end

class WithSum < Dry::Struct
  attribute :sum, A | B
end

WithSum.new(
  sum: B.new(
    id: 123, 
    type: 'foo', 
    name: 'bar', 
    primary_function: 'yo'
  )
) # => #<WithSum sum=#<A id=123 type="foo" name="bar">>

I expected it to return my instance of B, not A.

A nice first step, would be to at least document how sum types work.

But I wonder if it's intended to work this way too.

johanlunds avatar Oct 26 '21 16:10 johanlunds

But I wonder if it's intended to work this way too.

The problem lies in implicit coercion with to_hash. B gets coerced to a hash and to an A in order. It wasn't a good design choice. You can reject extra key by making the schema strict with schema schema.strict:

class StrictStruct < Dry::Struct
  schema schema.strict
end

class A < StrictStruct
  attribute :id,    Types::Strict::Integer
  attribute :type,  Types::Strict::String
  attribute :name,  Types::Strict::String
end

class B < StrictStruct
  attribute :id,               Types::Strict::Integer
  attribute :type,             Types::Strict::String
  attribute :name,             Types::Strict::String
  attribute :primary_function, Types::Strict::String.optional
end

class WithSum < Dry::Struct
  attribute :sum, A | B
end

This will still coerce data internally but these attempts will fail. I think we'll remove to_hash in 2.0.

flash-gordon avatar Oct 26 '21 16:10 flash-gordon

Thank you for the context @flash-gordon 🙏.

And thanks for the suggestion. I solved our case with constrained types:

Types::Strict::String.constrained(eql: 'my_type')

johanlunds avatar Oct 26 '21 17:10 johanlunds

Yeah, I do something similar to discriminate variants. 👍

flash-gordon avatar Oct 26 '21 17:10 flash-gordon