Search code examples
arraysmatlabobjectconstructorsuperclass

Construct object array with no input arguments


Background

I have different types of files that I extract from and store data in, for example .csv and database files. They are usually large files and I already have appropriate methods for extracting data from them.

Desired behaviour

Let's use the class textfile as an example. I'd like to be able to create an array of textfile objects where each element corresponds to a unique text file. I'd like to be able to call textfile with a single argument, or no argument at all.

  1. If I pass no argument, I'll be taken to a file select screen to select the files, and an object array will be created, one element for each file I selected.

  2. If one argument is passed, I'd like the array to be created depending on what that argument was. If it was a directory path, open up the file select screen again. If it was a cell array of filepaths, create an object array of those files.

I'd like to be able to easily create more classes like textfile that have the same basic behaviour.

Attempts so far

I've defined a superclass file to take care of behaviour common to all files. This includes things like assigning properties such as filename and extension. My other classes are all subclasses of file.

In the constructor for file, I have two input arguments. My idea was that in the relevant subclass constructors I'd call the file constructor. The first input argument would be the type of file, e.g. txt, while the second would be the argument I chose when creating the textfile array. For simplicity, let's say the only type of arg I want to be able to handle is a directory path from where to choose files from.

classdef textfile < file
    methods
        function textfileObject = textfile(arg)
            if nargin == 0
                arg = '';
            end
            textfileObject@file('txt',arg);
    end
end

The file constructor, depending on what arg is when I create textfile objects, generates an array of objects in accordance with using the No Input Argument Constructor Requirement.

classdef file < handle
    properties
        Path
    end
    methods
        function FileObject = file(FileType,arg)
            if nargin == 2
                FileList = file.SelectFiles(FileType,arg);
                FileObject(numel(FileList),1) = file;
                for filecount = 1:numel(FileObject)
                    FileObject(filecount,1).Path = FileList{filecount};
                end
            end
        end
    end
    methods(Static)
        function FileList = SelectFiles(DirectoryPath)
             % Some selection dialogs. Returns a cell array of filepaths
        end
    end
end

This works when arg is a directory, because when the object array is initalized in textfile with no input argument, arg is set to '' which works with the rest of the constructors.

However, I want to be able to not have any input arguments when creating textfile objects, but that doesn't work with the No Input Argument rules for constructors.

The question

Is there a way to create object arrays while

  1. calling a superclass constructor and

  2. not using any input arguments?

I'm open to other solutions that can help with my problem of creating classes for each of my file types.

Solution

It turns out you can call the subclass constructor from the superclass using an anonymous function. In effect, you can call the subclass constructor with no input arguments, and then generate the filenames in the superclass before calling the subclass again (recursively) with these filenames as inputs. Thanks @Suever.

The subclass constructor will simply be:

classdef textfile < file
    methods
        function textfileObject = textfile(varargin)
            textfileObject@file('txt',varargin{:});
        end
    end
end

And in the superclass:

classdef file < handle
    properties
        Path
    end
    methods
        function FileObject = file(FileType,varargin)

            % Subclass constructor handle
            Constructor = @(FilePath)feval(class(FileObject),FilePath);

            % No subclass arguments
            if nargin == 1
                FileList = file.SelectFiles(FileType,'');
                for a = 1:numel(FileList)
                    FileObject(a,1) = Constructor(FileList{a});
                end

            % One subclass argument
            elseif nargin == 2
                arg = varargin{1};
                if ischar(arg)
                    FileStruct = dir(arg);
                    if numel(FileStruct) == 1
                        FileObject.Path = arg;
                    end
                end

            % Too many subclass arguments
            else
                error('Subclasses of file only take one or no input arguments');
            end
        end
    end
end

Solution

  • The way to do this would be to call the constructor of your class recursively to generate the array of textfile objects.

    classdef textfile < file
    
        methods
            function self = textfile(arg)
                if ~exist('arg', 'var')
                    % Get list of files somehow
                    [fnames, pname] = uigetfile('MultiSelect', 'on');
                    filelist = fullfile(pname, fnames);
    
                    % Call the constructor again for each file
                    output = cellfun(@textfile, filelist, 'uniformoutput', 0);
    
                    % Flatten the cell array of objects into an array of the right shape
                    self = reshape([output{:}], size(filelist));
                else
                    % Do default construction here
                    self = self@file('txt', arg);
                end
            end
        end
    end
    

    If you want to implement this in the base class, you could change the recursive call to be something like the following which would call the correct subclass constructor from within the file superclass

    constructor = @(varargin)feval(class(self), varargin{:});
    output = cellfun(constructor, filelist, 'uniformoutput', 0);