Problem of duplication with my function

Hello, I have a problem with my function for my project, I discovered that yesterday because I had new files. To explain, my project actually have 3 directories named with the software name, and each directory contain only one directory with one version name.

But every time, when this function finish his work, Every availableSoftware.versions contains 3 items ??? Why ?

def loadSoftwareDatabase
            if !Dir.exists?(ISM::Default::Path::SoftwaresDirectory)
                Dir.mkdir(ISM::Default::Path::SoftwaresDirectory)
            end
            
            softwareDirectories = Dir.children(ISM::Default::Path::SoftwaresDirectory)

            softwareDirectories.each do |softwareDirectory|
                versionDirectories = Dir.children(ISM::Default::Path::SoftwaresDirectory+softwareDirectory)
                availableSoftware = ISM::AvailableSoftware.new(softwareDirectory)

                versionDirectories.each do |versionDirectory|
                    softwareInformation = ISM::SoftwareInformation.new

                    #
                    puts versionDirectories.size

                    if File.exists?(ISM::Default::Path::SettingsSoftwaresDirectory +
                                    softwareDirectory + "/" +
                                    versionDirectory + "/" +
                                    ISM::Default::Filename::SoftwareSettings)
                        softwareInformation.loadInformationFile(ISM::Default::Path::SettingsSoftwaresDirectory +
                                                                softwareDirectory + "/" +
                                                                versionDirectory + "/" +
                                                                ISM::Default::Filename::SoftwareSettings)
                    else
                        softwareInformation.loadInformationFile(ISM::Default::Path::SoftwaresDirectory +
                                                                softwareDirectory + "/" +
                                                                versionDirectory + "/" +
                                                                ISM::Default::Filename::Information)
                    end
                    availableSoftware.versions.push(softwareInformation)
                end
                @softwares.push(availableSoftware)
            end
        end
 zohran   master  ~  Documents  Programmation  ISM  ls
ISM  LICENSE  Main.cr  Settings  Softwares  Version.json
 zohran   master  ~  Documents  Programmation  ISM  tree Softwares/
Softwares/
├── Binutils
│   └── 2.37
│       ├── 2.37.cr
│       └── Information.json
├── Binutils-Pass1
│   └── 2.37
│       ├── 2.37.cr
│       └── Information.json
└── Binutils-Pass2
    └── 2.37
        ├── 2.37.cr
        └── Information.json

6 directories, 6 files
 zohran   master  ~  Documents  Programmation  ISM 

If you need more informations, I done this test, look:

def loadSoftwareDatabase
            if !Dir.exists?(ISM::Default::Path::SoftwaresDirectory)
                Dir.mkdir(ISM::Default::Path::SoftwaresDirectory)
            end
            
            softwareDirectories = Dir.children(ISM::Default::Path::SoftwaresDirectory)

            softwareDirectories.each do |softwareDirectory|
                versionDirectories = Dir.children(ISM::Default::Path::SoftwaresDirectory+softwareDirectory)
                availableSoftware = ISM::AvailableSoftware.new(softwareDirectory)

                versionDirectories.each do |versionDirectory|
                    softwareInformation = ISM::SoftwareInformation.new

                    if File.exists?(ISM::Default::Path::SettingsSoftwaresDirectory +
                                    softwareDirectory + "/" +
                                    versionDirectory + "/" +
                                    ISM::Default::Filename::SoftwareSettings)
                        softwareInformation.loadInformationFile(ISM::Default::Path::SettingsSoftwaresDirectory +
                                                                softwareDirectory + "/" +
                                                                versionDirectory + "/" +
                                                                ISM::Default::Filename::SoftwareSettings)
                    else
                        softwareInformation.loadInformationFile(ISM::Default::Path::SoftwaresDirectory +
                                                                softwareDirectory + "/" +
                                                                versionDirectory + "/" +
                                                                ISM::Default::Filename::Information)
                    end

                    availableSoftware.versions << softwareInformation
                    
                end

                @softwares << availableSoftware
                puts "For this directory:" + softwareDirectory
                puts "AvailableS name:" + availableSoftware.name
                puts "Versions number:" + availableSoftware.versions.size.to_s
                puts "\n"

            end

            puts "__________________________________"

            @softwares.each do |sft|
                puts sft.name
                puts sft.versions.size.to_s
                puts sft.versions.last.name
                puts sft.versions.last.description
                puts  "\n"
            end
        end

I have this result:


 zohran   master  ~  Documents  Programmation  ISM  crystal Main.cr software search binutils
For this directory:Binutils
AvailableS name:Binutils
Versions number:1

For this directory:Binutils-Pass1
AvailableS name:Binutils-Pass1
Versions number:2

For this directory:Binutils-Pass2
AvailableS name:Binutils-Pass2
Versions number:3

__________________________________
Binutils
3
Binutils-Pass2
The GNU collection of binary tools (Temporary Tools).

Binutils-Pass1
3
Binutils-Pass2
The GNU collection of binary tools (Temporary Tools).

Binutils-Pass2
3
Binutils-Pass2
The GNU collection of binary tools (Temporary Tools).

Completely crazy result.

Honestly I’m lost, I don’t understand why this happened

It looks like versions in your class ISM::AvailableSoftware is not unique to each instance of the class, and you are simply appending everything to the one array. Perhaps provide the code of your class?

I changed the implementation and I done that, this work. But honestly, even I solved my problem, I’m not sure to understand why my old code didn’t work:

def loadSoftwareDatabase
            if !Dir.exists?(ISM::Default::Path::SoftwaresDirectory)
                Dir.mkdir(ISM::Default::Path::SoftwaresDirectory)
            end
            
            softwareDirectories = Dir.children(ISM::Default::Path::SoftwaresDirectory)
            softwareDirectories.delete("SoftwaresLibrairies.cr")

            softwareDirectories.each do |softwareDirectory|
                versionDirectories = Dir.children(ISM::Default::Path::SoftwaresDirectory+softwareDirectory)
                softwaresInformations = Array(ISM::SoftwareInformation).new

                versionDirectories.each do |versionDirectory|

                    softwareInformation = ISM::SoftwareInformation.new

                    if File.exists?(ISM::Default::Path::SettingsSoftwaresDirectory +
                                    softwareDirectory + "/" +
                                    versionDirectory + "/" +
                                    ISM::Default::Filename::SoftwareSettings)
                        softwareInformation.loadInformationFile(ISM::Default::Path::SettingsSoftwaresDirectory +
                                                                softwareDirectory + "/" +
                                                                versionDirectory + "/" +
                                                                ISM::Default::Filename::SoftwareSettings)
                    else
                        softwareInformation.loadInformationFile(ISM::Default::Path::SoftwaresDirectory +
                                                                softwareDirectory + "/" +
                                                                versionDirectory + "/" +
                                                                ISM::Default::Filename::Information)
                    end

                    softwaresInformations << softwareInformation
    
                end

                @softwares << ISM::AvailableSoftware.new(softwareDirectory,
                softwaresInformations)

            end

        end

My class is very small:

module ISM

    class AvailableSoftware

        property name = ISM::Default::AvailableSoftware::Name
        property versions = ISM::Default::AvailableSoftware::Versions

        def initialize( name = ISM::Default::AvailableSoftware::Name,
                        versions = ISM::Default::AvailableSoftware::Versions)

            @name = name
            @versions = versions
        end

    end

end

Finally again this bug persist.Something is wrong definitly.

Any idea ?

It’s strange

Are you still adding stuff to @versions in AvailableSoftware somewhere? I suspect that what’s happening is the following:

Each loop you make an instance of AvailableSoftware with a directory but a default @versions. The default, ISM::Default::AvailableSoftware::Versions, is a constant instance of a class, and you’re actually getting a reference to it in each AvailableSoftware, not a copy. So that means that you’re actually adding elements to the default versions collection each loop.

If ISM::Default::AvailableSoftware::Versions is actually a standard library collection (e.g. an Array), you can change the default to ISM::Default::AvailableSoftware::Versions.dup. That should make a copy of the default array without you accidentally adding elements to it.

If I understood properly your explanation (I’m not sure),
the problem come from my module because it’s a constant ?

module ISM

    module Default

        module AvailableSoftware

            Name = ""
            Versions = Array(ISM::SoftwareInformation).new

        end

    end

end

I use a default module for each class just to initialize each property to a value and infer the type. Is it bad ?

I think you was talking about this line :

@softwares << ISM::AvailableSoftware.new(softwareDirectory,
                softwaresInformations)

Oh okay I think I understood, simply because I initialize every class with a constant, I can’t change it after, is it that ?

More so that since the default value is assigned from a constant, they all reference the same array.

I need to correct all of my classes o_o quickly

I’m not sure but, I need to correct with something like that ?
I have always an error:

module ISM

    class AvailableSoftware

        property name : String
        property versions : Array(ISM::SoftwareInformation)

        def initialize( name : String,
                        versions : Array(ISM::SoftwareInformation))

            @name = name
            @versions = versions
        end

    end

end

If I do that, I have the same error.
I’m lost. It’s not possible to implement a default module like what I done ?

Try doing like versions = ISM::Default::AvailableSoftware::Versions.dup. But in the end a defaults module isn’t very common. I’d just do versions = [] of ISM::SoftwareInformation and call it a day. Also fwiw you can write your AvailableSoftware type as, which is abit cleaner imo:

class ISM::AvailableSoftware
  property name : String
  property versions : Array(ISM::SoftwareInformation)

  def initialize(@name : String, @versions : Array(ISM::SoftwareInformation)); end
end

If I need some default value as constant, ( for example a default path or value), can I use a global class variable or it’s not good as well ?

Something to keep in mind is constants in Crystal just prevent re-assignment, not immutability. E.g. when you have a constant array, you’re assigning a reference to that singleton array, which is most likely not what you want.

Using a constant to avoid duplicating its value it is a good idea, but maybe try and only do that for simple values, like strings or numbers. If you’re mainly wanting to avoid longer names/duplication of that longer name, it might be better to look into using an alias.

E.g.

alias SoftwareInfo = Array(ISM::SoftwareInformation)

From here you could then use that as both a way to type the argument and initialize an empty array as its default:

def initialize(@versions : SoftwareInfo = SoftwareInfo.new); end

Finally, I can keep my module and just change all constances by an alias ?

Is it normal for example when I replace with aliases, I have this error:

module ISM

    module Default

        module AvailableSoftware

            alias Name = ""
            alias Versions = Array(ISM::SoftwareInformation).new

        end

    end

end

 zohran   master  ~  Documents  Programmation  ISM  crystal Main.cr 
In ISM/Default/AvailableSoftware.cr:7:26

 7 | alias Name = ""
                  ^
Error: unexpected token: DELIMITER_START

Yes, that’s expected because that’s not what aliases for are. See alias - Crystal.

The idea is you could have done:

module ISM::Default::AvailableSoftware
  Name = ""
  alias Versions = Array(ISM::SoftwareInformation)
end

Then like:

def initialize(
  @name : String = ISM::Default::AvailableSoftware::Name, 
  @versions : ISM::Default::AvailableSoftware::Versions =  ISM::Default::AvailableSoftware::Versions.new
); end

I.e. where the scalar string value is a constant, but you use an alias to centralize the type of the array without having it be a constant such that you do not assign everything the same array.

One other way, is it better to remove all of my Default modules and just do something like that ?

module ISM

    class AvailableSoftware

        @@DefaultName = ""
        @@DefaultVersions = Array(ISM::SoftwareInformation).new

        property name = @@DefaultName
        property versions : @@DefaultVersion

        def initialize( name : @@DefaultName,
                        versions : @@DefaultVersion)

            @name = name
            @versions = versions
        end

    end

end

Or I will have the same result ?

You’d have the same problem.

If I initialize manually properties and I just use constants for default value if the user put nothing, I think it’s fine no ?

Like this :

module ISM

    class AvailableSoftware

        property name : String
        property versions : Array(ISM::SoftwareInformation)

        def initialize( name = ISM::Default::AvailableSoftware::Name,
                        versions = Array(ISM::SoftwareInformation)

            @name = name
            @versions = versions
        end

    end

end

EDIT: I think no it’s wrong as well because the compiler while infer the type as well. Okay definitely it’s wrong

Can I do that ?

module ISM

    class AvailableSoftware

        property name : String
        property versions : Array(ISM::SoftwareInformation)

        def initialize( @name, @versions)

            @name = name
            @versions = versions
        end

    end

end