A simple thought about key-value pairs

作者:   發佈於: ,更新於:   #perl #programming

Very often, we wrote Web API that produce JSON output and also very often, we want to ensure that certain values in the output are produced as numerical literals (42 instead of "42") or as boolean literals (true or false). The only reliable way, imho, is to cast them when printing the JSON output, or when preparing the structure for that output -- that is basically as "late" as possible.

Since we also often produce JSON with JSON module, we may have a TO_JSON around that looks like this, with many inline type-coercion expression:

sub TO_JSON ($self) {
    return {
        # string
        "align" => "". self->align,

        # number
        "height" => 0+ $self->height,
        "width" => 0+ $self->width,

        # boolean
        "closed" => $self->is_closed ? \1 : \0
        "is_landscape" => ( $self->height < $self->width)  ? \1 : \0
        "fits" => ( ($self->height * $self->width) < LIMIT ) ? \1 : \0
    }
}

When the unavoidable fate comes and we include more and more attributes in the TO_JSON, such inline expressions becomes bulky and a bit tedious to look at... just imagine the situation of having a few dozens of inline ternary expressions not perfectly aligned and one of them being wrong. It would be nicer to make them look good and make those inline expressions look like some sort of annotation:

sub TO_JSON ($self) {
    return {
        "align" => json_str(self->align),

        "height" => json_num( $self->height ),
        "width" => json_num( $self->width ),

        "closed" => json_bool( $self->is_closed ),
        "is_landscape" => json_bool( $self->height < $self->width ),
        "fits" => json_bool( ($self->height * $self->width) < LIMIT ),
    }
}

These helper methods that does type coercion by themselfs are trivial:

sub json_str ($x) { "". $x }
sub json_num ($x) { 0 + $x }
sub json_bool ($x) { $x ? \1 : \0 }

Or, perhaps even better, we could annotate the attributes in groups, arranged by their expected type:

sub TO_JSON ($self) {
    return {
        kvpairs_json_str(
            "align"  => self->align,
        ),

        kvpairs_json_num(
            "height" => $self->height,
            "width"  => $self->width,
        ),

        kvpairs_json_bool(
            "closed" => $self->is_closed,
            "is_landscape" => ( $self->height < $self->width ),
            "fits" => ( ($self->height * $self->width) < LIMIT ),
        )
    }
}

Using the pairmap function from List::Util, the new helper methods looks also trivial:

use List::Util ('pairmap');

sub kvpairs_json_str (@kvlist) {
    pairmap { ($a, json_str($b)) } pairs @kvlist
}
sub kvpairs_json_num (@kvlist) {
    pairmap { ($a, json_num($b)) } pairs @kvlist
}
sub kvpairs_json_bool (@kvlist) {
    pairmap { ($a, json_bool($b)) } pairs @kvlist
}

Since for our purpose of printing object as JSON, keys can be ignored because they are always string literals directly written as part of our code. We only have to deal all the values which are every 2nd elements in @kvlist. Also, because the overall result of TO_JSON is a HashRef, the order of keys as they are written in the code is irrelevant and that allows us to arrange those key-value pairs in groups so overall it looks nicer and organized.

Meanwhile, we also often hide partial kvpairs given some condition because some attributes are optional and perhaps convey special meaning when they are part of the output. A typical example would be the the conventional "error" attribute in API responses.

my $response = {
    defined($o->error) ? (
        "error" => $o->error,
    ):(),

    ...
};

That's a ternary op with an ever weirder look. I wonder if this is better:

my $response = {
    kvpairs_without_undef(
        "error" => $o->error,
        ...
    ),
    ...
};

That is like marking a section of kvpairs as optional and exclude all pairs with undef value. The helper function kvpairs_without_undef can be done with the help of pairgrep:

use List::Util ('pairgrep');

sub kvpairs_without_undef (@kvlist) {
    pairgrep { defined($b) } @kvlist
}

This is probably doing a lot of copying and perhaps there are some performance enhancements to be done. But well, these are just thoughts, optimizing thoughts is even before the so called premature optimization.

(Perhaps that's fun though.)