How can I implement 2d subscripts via AT-POS for different classes?

Viewed 87

here is an MRE (showing two attempts, with debug left in to be helpful) to try and get 2d subscripting working with AT-POS across a DataFrame that has columns of Series...

class Series does Positional {
    has Real @.data = [0.1,0.2,0.3];

    method AT-POS( $p ) { 
        @!data[$p]
    }   
}

class DataFrame does Positional {
    has Series @.series;

#`[ ATTEMPT #1
    method AT-POS( $p, $q? ) { 
        given $q {
            when Int {                  #say 'Int';
                @!series[$p][$q]
            }   
            when Whatever {             #say '*';
                @!series[$p].data 
            }   
            default {                   #say 'default';
                @!series[$p] 
            }   

        }   
    }   
#]
    # ATTEMPT #2
    method AT-POS(|c) is raw { #`[dd c;] @!series.AT-POS(|c) }
}

my $df = DataFrame.new( series => [Series.new xx 3] );

say $df[1].data;            #[0.1 0.2 0.3]
say $df[1][2];              #0.3
say $df[0,1];               #(Series.new(data => $[0.1, 0.2, 0.3]) Series.new(data => $[0.1, 0.2, 0.3]))
say $df[1;2];               #0.3
say $df[1;*];               #got (0.1) ... expected [0.1 0.2 0.3]
say $df[*;1];               #got (0.2) ... wanted [0.2 0.2 0.2]

I already researched on SO and found three related questions here, here and here ... and the attempt #2 in my code seeks to apply @lizmats Answer to the third one. Encouragingly both the attempts in my MRE have the same behaviour. But I cannot workout

  • why the when Whatever {} option is not entered (attempt #1)
  • what the |c is doing - even though I can see that it works in the single subscript case (attempt #2)

I have done some experimenting with multi postcircumfix:<[ ]>( DataFrame:D $df, @slicer where Range|List ) is export {} but this seems to overcomplicate matters.

==================

Great answer from @jonathan building on the original from @Lizmat - thanks! Here is the final, working code:

class Series does Positional {
    has Real @.data = [0.1,0.2,0.3];

    method elems {
        @!data.elems
    }   

    method AT-POS( |p ) is raw {
        @!data.AT-POS( |p )
    }   
}

class DataFrame does Positional {
    has Series @.series;

    method elems { 
        @!series.elems
    }   

    method AT-POS( |p ) is raw { 
        @!series.AT-POS( |p )
    }   
}

my $df = DataFrame.new( series => Series.new xx 3 );

say $df[1].data;            #[0.1 0.2 0.3]
say $df[1][2];              #0.3
say $df[0,1];               #(Series.new(data => $[0.1, 0.2, 0.3]) Series.new(data => $[0.1, 0.2, 0.3]))
say $df[1;2];               #0.3
say $df[1;*];               #(0.1 0.2 0.3)
say $df[*;1];               #(0.2 0.2 0.2)
2 Answers

The AT-POS method is only ever passed integer array indices.

The logic to handle slicing (with *, ranges, other iterables, the zen slice) is located in the array indexing operator, which is implemented as the multiple-dispatch subroutine postcircumfix:<[ ]> for single-dimension indexing and postcircumfix:<[; ]> for multi-dimension indexing. The idea is that a class that wants to act as an array-alike need not worry about re-implementing all of the slicing behavior and, further, that the slicing behavior will behave consistently over different user-defined types.

For slicing to work, one must implement elems as well as AT-POS. Adding:

method elems() { @!data.elems }

Is Series and:

method elems() { @!series.elems }

In DataFrame gives the results you're looking for.

If one really wants different slicing semantics, or a far more efficient implementation than the standard one is possible, one can also add multi candidates for the indexing operator (remembering to mark them is export).

This answer is simply an elaboration of the point @raiph made in a comment:

You may be able to simplify the code by using handles

Indeed you can – so much so that I thought it was worth showing what that would look like in a code block without a comment's formatting limits.

Using handles, you can simplify each of the two classes from 9 non-whitespace lines to 3:

class Series does Positional {
    has Real @.data handles <elems AT-POS> = [0.1,0.2,0.3];
}

class DataFrame does Positional {
    has Series @.series handles <elems AT-POS>;
}

(or you could even have each class on 1 line, if you format them the way I'd be tempted to.)

This code produces all the same results to the say statements from the code in the question.

Related