http://www.perlmonks.org?node_id=1014174

gideondsouza has asked for the wisdom of the Perl Monks concerning the following question:

I beseech you dear monks to read my question. I know its a bit of code and text but I made good efforts to keep it tight

I'm on the learning road and I'm building an API wrapper as practise. Note, I'm not using Moose/Moo and building this to learn perl and perl OOP

I'm writing a wrapper over stack exchange api.

Usage would look like this (currently prints urls for testing) :

# for the following API urls: ##http://api.stackexchange.com/answers/{ids}/comments/?query_str ##http://api.stackexchange.com/answers/?query_str use Net::StackExchange; my $a = Net::StackExchange->new(); print $a->answers->{answers_comments}->(123,134,145,{ order=> "desc", sort=>"votes" }); ######## THE ABOVE PRINTS: #http://api.stackexchange.com/answers/123;134;145/comments?sort=votes& +order=desc print $a->answers->{answers}->({ order=> "desc", sort=>"votes" }); ######## THE ABOVE PRINTS: #http://api.stackexchange.com/answers/?sort=votes&order=desc

Mostly urls are of the form api/{name}/{ids_list}/{anothename} or api/{name}/. After that there are query params which are represented by a hash in my code.

My questions begin:

  1. Does the consumer code look ok? Is it neat enough abstraction to have anonymous functions like this: $object->answers->{answers_comments}->(..)
  2. Is there I way I can build methods so my consumer code looks like $object->answers->answers_comments() would that be correct/better/perl-ish?
  3. Inside my anonymous why don't I have a reference to $self?

My Code

On my disk it looks like this:

Net-StackExchange/
├── Changes
├── MANIFEST
├── Makefile.PL
├── README
├── ignore.txt
├── lib
│   └── Net
│       ├── StackExchange
│       │   ├── V2
│       │   │   ├── Answers.pm
│       │   │   └── Common.pm
│       │   └── V2.pm
│       └── StackExchange.pm
└── t
    ├── 00-load.t
    ├── boilerplate.t
    ├── manifest.t
    ├── pod-coverage.t
    └── pod.t

The code in each of the FOUR pm files

################### # Inside StackExchange.pm ################### sub new { return Net::StackExchange::V2->new(); } ################### # Inside V2.pm ################### sub new { my ($class) = @_; my $self = {}; bless $self, $class; return $self; } sub answers { return Net::StackExchange::V2::Answers->new(); } ################### # Inside Answers.pm ################### use Net::StackExchange::V2::Common qw(query no_params one_param); sub new { my ($class) = @_; my $self = { site => "stackoverflow", answers => no_params("answers"), answers_comments => one_param("answers","comments"), }; bless $self, $class; return $self; } ################################################# ################################################# # THIS IS WHAT NEEDS ATTENTION # Inside Common.pm ################################################# use constant BASE_URL => "http://api.stackexchange.com/"; our @ISA = qw(Exporter); our @EXPORT = qw(query no_params one_param); sub query { #get the hash from the last param and use it for the query str my $queryStrHash = pop @_; #add rest of the params into the url. my $url = join("/",@_); my @params = (); while ( my ($key, $value) = each(%$queryStrHash) ) { push @params, $key."=".$value; } my $query = '?'.join '&',@params; return BASE_URL.$url.$query; } ##function to generate a function that generates url with #a name and no parameters.. sub no_params { #WHY don't I get $self = shift here? my $name = shift; return sub { #WHY don't I get $self = shift here? my $queryStr = pop @_; return query($name, $queryStr); } } ##function to generate a function that generates url #with a name and one parameter.. sub one_param { my $param1 = shift; my $param2 = shift; return sub { my $q = pop @_; my @ids = @_; return query($param1, join(";",@ids), $param2, $q); } }

Replies are listed 'Best First'.
Re: is my api wrapper abstraction ok (is it perlish?)
by tobyink (Canon) on Jan 19, 2013 at 07:28 UTC

    You are not calling your anonymous subs as methods, thus they don't get any invocant ($self). You could call them like this:

    my $method = $a->answers->{answers_comments}; $a->answers->$method( 123, 134, 145, { order => "desc", sort => "votes", }, );

    Then they'll have an invocant.

    It would be much saner though to install your methods as genuine methods, not coderefs floating around attached to blessed objects.

    # in Answers.pm use Sub::Name 'subname'; *answers_comments = subname( "Net::StackExchange::V2::Answer::answers_comments", one_param("answers","comments"), );

    Now you can call it like a proper method:

    $a->answers->answers_comments( 123, 134, 145, { order => "desc", sort => "votes", }, );
    perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'

      Thanks a ton for your reply

      .Yep. This is the sorcery I was looking for.

      Is this from the Sub::Name module on cpan? It's not part of perls core modules right?

      May I ask, what language construct is this :  *answers_comments?? A pointer of some sort?

        *answer_comments is a glob - basically an entry in Perl's symbol table. It is kind of a bundle which contains sub answer_comments, our $answer_comments, our @answer_comments, our %answer_comments and the answer_comments bareword file handle.

        If you assign one glob to another, like *xxx = *yyy, then it makes sub xxx and sub yyy aliases for each other, and our $xxx and our $yyy aliases for each other, etc.

        use 5.010; use strict; use warnings; our ($xxx, $yyy, @xxx, @yyy); *xxx = *yyy; # alias $xxx = 42; say $yyy; # says "42" @xxx = (9,9,9); say "@yyy"; # says "9 9 9"

        Anyway, that's what happens when you assign one glob to another glob. But you can also assign references to a glob, in which is doesn't alias the whole bundle, but just the relevant slot within the glob. For example:

        use 5.010; use strict; use warnings; our ($xxx, $yyy, @xxx, @yyy); *xxx = \$yyy; # alias $yyy, but not @yyy $xxx = 42; say $yyy; # says "42" @xxx = (9,9,9); say "@yyy"; # says nothing

        And so one_param returns a coderef, and we can assign it to the glob:

        *answers_comments = one_param("answers","comments");

        This effectively installs the sub into the package with a given name. Almost as if you'd done:

        package ...; sub answers_comments { ...; }

        (This is what Exporter does internally - that module is not as magic as it seems.)

        I say "almost" though. There's one key difference, and that's what the CPAN module Sub::Name takes care of. When the answers_comments method is called, Perl still internally treats it as an anonymous coderef. Which means that stack traces (e.g. Carp) would show it as "__ANON__" rather than "answers_comments".

        Naming the sub is also important if you use something like namespace::autoclean which deletes subs it finds in your package that don't look like methods. Naming a sub makes it look like a method.

        So Sub::Name is well worth using when you muck around with globs; it's a shame it's not bundled with Perl.

        perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'
Re: is my api wrapper abstraction ok (is it perlish?)
by Arunbear (Prior) on Jan 19, 2013 at 18:01 UTC
    OOP is about hiding low level operations. It's about delegating, not micro-managing. E.g. at a restaurant you give the waiter your order, and they bring your meal. The consumer code above would be like the waiter bringing back the cooking tools and letting you prepare the meal.

    Here is a (hopefully) more obvious interface (and a simpler implementation)

    package main; use strict; use v5.10; my $obj = My::StackExchange->new; my $uri; $uri = $obj->build_uri( type => 'answers', ids => [123,134,145], comments => 1, options => { order => "desc", sort => "votes" } ); say $uri; $uri = $obj->build_uri( type => 'answers', options => { order => "desc", sort => "votes" } ); say $uri; package My::StackExchange; use URI::FromHash qw( uri ); sub new { bless {} } sub build_uri { my ($self, %arg) = @_; #TODO validate input my @paths = ($arg{type}); if ( $arg{ids} ) { push @paths, join(';', @{ $arg{ids} }); } if ( $arg{comments} ) { push @paths, 'comments'; } return uri( scheme => 'http', host => 'api.stackexchange.com', path => \@paths, query => $arg{options}, ); } __DATA__ http://api.stackexchange.com/answers/123;134;145/comments?sort=votes;o +rder=desc http://api.stackexchange.com/answers?sort=votes;order=desc

      Thanks for your reply. I see. Yours is an interesting abstraction.

      Also, there are urls like this: "/answers/{ids}/comments" where it returns comments on the answers returned by ids. So then I'd have to introduce another parameter or type for it.

        Also, there are urls like this: "/answers/{ids}/comments" ...
        But isn't that the same as the first example above that returns
        http://api.stackexchange.com/answers/123;134;145/comments?sort=votes;o +rder=desc
        minus the query parameters? The code example I gave already handles that case.

      Much saner. :)

      - tye        

Re: is my api wrapper abstraction ok (is it perlish?)
by Anonymous Monk on Jan 19, 2013 at 07:32 UTC
     answers->{answers_comments}; is one answers too many , it should be ->answers and ->answers_comments