46

According to the documentation unset attributes of Struct are set to nil:

unset parameters default to nil.

Is it possible to specify the default value for particular attributes?

For example, for the following Struct

Struct.new("Person", :name, :happy)

I would like the attribute happy to default to true rather than nil. How can I do this? If I do as follows

Struct.new("Person", :name, :happy = true)

I get

-:1: syntax error, unexpected '=', expecting ')'
Struct.new("Person", :name, :happy = true)
                                    ^
-:1: warning: possibly useless use of true in void context
N.N.
  • 8,336
  • 12
  • 54
  • 94

6 Answers6

31

This can also be accomplished by creating your Struct as a subclass, and overriding initialize with default values as in the following example:

class Person < Struct.new(:name, :happy)
    def initialize(name, happy=true); super end
end

On one hand, this method does lead to a little bit of boilerplate; on the other, it does what you're looking for nice and succinctly.

One side-effect (which may be either a benefit or an annoyance depending on your preferences/use case) is that you lose the default Struct behavior of all attributes defaulting to nil -- unless you explicitly set them to be so. In effect, the above example would make name a required parameter unless you declare it as name=nil

rintaun
  • 697
  • 8
  • 14
  • 5
    Also when you put the `initialize` definition on one line, it says "super end" which feels fun and makes me happy :D – rintaun Aug 09 '14 at 03:58
  • 9
    There's no need to add another layer class to it. You can just override or shadow the initialize method: `Person = Struct.new(:name, :happy){ def initialize(name, happy=true); super; end }`. The only caveat with this implementation is that you have to specify `nil` to every argument preceding the argument you want to give a default value with which could be messy if your attributes are already more than 5. – konsolebox Sep 26 '14 at 09:58
  • 3
    Agree with @konsolebox on this one, plus is a bad practice http://www.rubydoc.info/gems/rubocop/0.29.1/RuboCop/Cop/Style/StructInheritance – Calin Aug 03 '16 at 12:39
23

Following @rintaun's example you can also do this with keyword arguments in Ruby 2+

A = Struct.new(:a, :b, :c) do
  def initialize(a:, b: 2, c: 3); super end
end

A.new
# ArgumentError: missing keyword: a

A.new a: 1
# => #<struct A a=1, b=2, c=3> 

A.new a: 1, c: 6
# => #<struct A a=1, b=2, c=6>

UPDATE

The code now needs to be written as follows to work.

A = Struct.new(:a, :b, :c) do
  def initialize(a:, b: 2, c: 3)
    super(a, b, c)
  end
end
6ft Dan
  • 2,365
  • 1
  • 33
  • 46
  • This is technically correct, although also terse. See my answer below in the same exact direction, but with a more real-world example. – Konstantin Gredeskoul Aug 09 '16 at 05:42
  • no you can't. It actually assigns all keyword arguments to first `a` variable (Ruby 2.3.1) so in last case you game I got `#1, :b=>2, :c=>6}, b=nil, c=nil>` – goodniceweb Sep 03 '17 at 13:13
  • 1
    @goodniceweb I've updated my answer to work with later versions of Ruby. – 6ft Dan Sep 04 '17 at 01:57
4

@Linuxios gave an answer that overrides member lookup. This has a couple problems: you can't explicitly set a member to nil and there's extra overhead on every member reference. It seems to me you really just want to supply the defaults when initializing a new struct object with partial member values supplied to ::new or ::[].

Here's a module to extend Struct with an additional factory method that lets you describe your desired structure with a hash, where the keys are the member names and the values the defaults to fill in when not supplied at initialization:

# Extend stdlib Struct with a factory method Struct::with_defaults
# to allow StructClasses to be defined so omitted members of new structs
# are initialized to a default instead of nil
module StructWithDefaults

  # makes a new StructClass specified by spec hash.
  # keys are member names, values are defaults when not supplied to new
  #
  # examples:
  # MyStruct = Struct.with_defaults( a: 1, b: 2, c: 'xyz' )
  # MyStruct.new       #=> #<struct MyStruct a=1, b=2, c="xyz"
  # MyStruct.new(99)   #=> #<struct MyStruct a=99, b=2, c="xyz">
  # MyStruct[-10, 3.5] #=> #<struct MyStruct a=-10, b=3.5, c="xyz">
  def with_defaults(*spec)
    new_args = []
    new_args << spec.shift if spec.size > 1
    spec = spec.first
    raise ArgumentError, "expected Hash, got #{spec.class}" unless spec.is_a? Hash
    new_args.concat spec.keys

    new(*new_args) do

      class << self
        attr_reader :defaults
      end

      def initialize(*args)
        super
        self.class.defaults.drop(args.size).each {|k,v| self[k] = v }
      end

    end.tap {|s| s.instance_variable_set(:@defaults, spec.dup.freeze) }

  end

end

Struct.extend StructWithDefaults
dbenhur
  • 20,008
  • 4
  • 48
  • 45
4

I also found this:

Person = Struct.new "Person", :name, :happy do
  def initialize(*)
    super
    self.location ||= true
  end
end
Sixty4Bit
  • 12,852
  • 13
  • 48
  • 62
Oleksandr Holubenko
  • 4,310
  • 2
  • 14
  • 28
3

Just add another variation:

class Result < Struct.new(:success, :errors)
  def initialize(*)
    super
    self.errors ||= []
  end
end
Kris
  • 19,188
  • 9
  • 91
  • 111
2

I think that the override of the #initialize method is the best way, with call to #super(*required_args).

This has an additional advantage of being able to use hash-style arguments. Please see the following complete and compiling example:

Hash-Style Arguments, Default Values, and Ruby Struct

# This example demonstrates how to create Ruby Structs that use
# newer hash-style parameters, as well as the default values for
# some of the parameters, without loosing the benefits of struct's
# implementation of #eql? #hash, #to_s, #inspect, and other
# useful instance methods.
#
# Run this file as follows
#
# > gem install rspec
# > rspec struct_optional_arguments.rb --format documentation
#
class StructWithOptionals < Struct.new(
    :encrypted_data,
    :cipher_name,
    :iv,
    :salt,
    :version
    )

    VERSION = '1.0.1'

    def initialize(
        encrypted_data:,
        cipher_name:,
        iv: nil,
        salt: 'salty',
        version: VERSION
        )
        super(encrypted_data, cipher_name, iv, salt, version)
    end
end

require 'rspec'
RSpec.describe StructWithOptionals do
    let(:struct) { StructWithOptionals.new(encrypted_data: 'data', cipher_name: 'AES-256-CBC', iv: 'intravenous') }

    it 'should be initialized with default values' do
        expect(struct.version).to be(StructWithOptionals::VERSION)
    end

    context 'all fields must be not null' do
        %i(encrypted_data cipher_name salt iv version).each do |field|
            subject { struct.send(field) }
            it field do
                expect(subject).to_not be_nil
            end
        end
    end
end
  • Your answer does not add anything. See [@rintaun](http://stackoverflow.com/a/25215058/445221)'s answer. It's also not necessary to use another class since the product of Struct.new is virtually just an empty class in which the superclass is Struct. – konsolebox Aug 09 '16 at 08:25
  • The main thing I added is the new hash-style parameter arguments. I find them to be much easier to read and use. With my example you can have a Struct that receives hash arguments, some of them with default values. So it's a specific case of that answer. Of course it's not necessary to use another class __name__. By that argument it shouldn't be necessary to use named variables or functions. Why not just derive everything from Object? – Konstantin Gredeskoul Aug 13 '16 at 08:12
  • Ok, I missed that. If anyone would prefer passing values to an initialize method through keyword arguments, that would be a solution. – konsolebox Aug 13 '16 at 19:15
  • I'm not talking about whether you'd use another class **name** or not. I'm talking about whether it's necessary to derive from another anonymous class or not. I don't see your *exaggerated* point about everything deriving from Object. A class created from Struct.new is virtually an empty class. It doesn't even have an initialize function. It only has attribute readers/writers. There's no point having an empty class in between, and it's [not recommended](http://ruby-doc.org/core-2.3.1/Struct.html#method-c-new). – konsolebox Aug 13 '16 at 19:36
  • Ok thanks for that reference. I actually had no idea that the two methods were actually different behind the scenes. Fascinating! So having thought about this a bit, I wonder if I'm using structs in a different way that was intended. I use them mostly as a shorthand for creating classes that depend on several required attributes. So it's like attr_accessor on steroids. I like that I have a reusable class created and think of the struct as an implementation detail. What do you think about that usage, @konsolebox? – Konstantin Gredeskoul Aug 16 '16 at 10:00
  • It's hard to elaborate. All I can say is that I use a Struct when the purpose of the class is closer to that of the Struct, OTOH I use a "Class" and use attr_* when it deviates far already from the purpose of a simple Struct. Inheriting from a Struct is not a bad thing, that I could say at least. The only thing I certainly don't like is inheriting from an anonymous Struct. I suggest you ask a question about it in http://programmers.stackexchange.com/ instead. You'll find more elaborate answers about it in there. – konsolebox Aug 25 '16 at 15:12