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

Dear Monks, I am doing some moose programming. I love it, but I have found myself manually repeating this design pattern. I think it could be automated by a MooseX or maybe turned into a perl6 feature or something. Consider this code:
package Identity; use Moose; subtype 'first_name_type', as 'Str'; where {/^\S+$}; subtype 'last_name_type', as 'Str'; where {/^\S+$}; subtype 'full_name_type', as 'Str', where {/^\S+ \S+$/}; coerce 'first_name_type', from 'full_name_type', via {(split(' '))[0]}; coerce 'last_name_type', from 'full_name_type', via {(split(' '))[1]}; has 'first_name' => ( isa => 'first_name_type', coerce => 1, lazy => 1, default => sub {$_[0]->full_name}, ); has 'last_name' => ( isa => 'last_name_type', coerce => 1, lazy => 1 default => sub {$_[0]->full_name}, ); has 'full_name' => ( isa => 'full_name_type', coerce => 1, lazy => 1, default => sub {$_[0]->first_name.' '.$_[0]->last +_name}, );

The idea is that if you've set last_name and first_name already and then you call full_name it will build full_name from first_name and last_name and vise versa. Doing it this way isn't too bad but if keep adding new ways of making things the default methods get a lot of if statements in them and get annoying.

What if we could just declare full_name with something like this:

has 'full_name' => ( isa => 'full_name_type', build_by => { A => 'first_name', B => 'last_name', with => 'A B', },

and that would do all the coercions and default methods behind the scenes.

When calling full_name for the first time (assuming first and last have been initialized) it would build it by replacing A and B in the string with the corresponding attributes first_name and last_name.

When calling last_name or first_name for the first time (assuming full_name had already been set) it would build them by looking at where they are in the string, and applying an appropriate regex (or whatever) to full_name to produce them. You wouldn't have to put a build_by in first/last name it would just recognize that full_name can use both of them to create itself and therefore it can be used to create them by reversing whatever you do to create it.

Well that's my idea. It could get much cooler and more advanced than that. If people respond/are interested I'll talk about it some more. I've never extended moose so I'm also interested in what people have to say about where to start to do something like this.
Thanks for reading

Replies are listed 'Best First'.
Re: Idea: Moose Mutal Coercion
by tye (Sage) on Feb 14, 2012 at 14:22 UTC

    Why do you so hate Bobbie Joe van Hilton?

    (I'm not sure who to feel more sorry for, the people with spaces in their first name and/or last name, the people who will try to use your software but have some of the first set of people as their customers, or the people who inherit your code and try to maintain it.)

    - tye        

      Indeed. Dividing names up into "first name", "last name", etc fields is an internationalisation disaster waiting to happen.

      Just have a single "name" field for storing the person's full name. If you need to be able to address people in a variety of ways ("Joe Bloggs" on an envelope, "Dear Joe" at the start of the letter, and "Mr J Bloggs" on billing) then expand that into "formal_name", "informal_name", "legal_name", etc fields.

      Splitting the name up and then recombining it simply won't work once you step out of the cosy little world of $ENV{LC_ALL}.

        I once worked with a gentleman named 'Tiger'. It said that on his birth certificate, driver's licenses, military ID, and Social Security records. He drove the Payroll and Taxes into fits. Another place I worked had a 'Gunther Karl-Dieter von Augsbach von und zu Berchtal am Saur Zimmerman'. Payroll tried several times to get an acceptable abbreviated name, we finally expanded the name field to 200 characters.

        ----
        I Go Back to Sleep, Now.

        OGB

      OK, so perhaps first and last names wasn't the best way to describe my idea. I agree with all these points about the difficulties of storing names but there's no need to lament about users who don't exist! It was just an example.

      The idea is: A formal way of describing the relationship between attributes that can constructed from each other. Lets start again. After thinking about the problem some more I'm going to rename this from "mutal coercions" to "invertible builders".

      Here's a different example that is hopefully less confusing:
      package Equation_Problem; use Moose; use MooseX::Invertible_Build; has 'a' => ( isa => Num); has 'b' => ( isa => Num); has 'c' => ( isa => Num, invertible_build => { #declare which attrs are part of #our invertible builder group #and the invertible function to base #their builders off of A => 'a', B => 'b', via => 'sum(A, B)', }, ); package main; $math_problem = new Equation_Problem( a => 4, b => 5); print $math_problem->c # prints 9 $math_problem = new Equation_Problem(c => 7, b => 4); print $math_problem->a #prints 3

      So what is MooseX::Invertible_Build doing?
      It defines this function sum() which can be used in a string as part of the 'via' value in the invertible_build option. sum adds two numbers together and returns the result, so in this case it would add the 'a' and 'b' attributes together and return the result.

      But aha! this function has an simple inverse. The inverse of the sum of 'a' and 'b' with respect to a is the subtraction of b from c. And so behind the scenes MooseX::Invertible_Build creates not only a builder for c which in this case is something like:
      default => sub{$_[0]->a + $_[0]->b}
      but it also creates a builder for 'a' and 'b' which for 'a' would look like:
      default => sub{$_[0]->c - $_[0]->b}
      Now that my idea is more clear lets return to the original example which involves strings and change it a little (and ignore the complexities behind generalizing human names).
      has 'full_name' => ( isa => 'full_name', coerce => 1, lazy => 1, invertible_build => ( A => 'first_name', B => 'last_name', via => 'join(' ', A, B)', }, );

      now our 'via' statement contains the 'join'function....and Once again it has an inverse! the inverse of join(A,B) with respect to A is (split(' ', C))[0] (with C being full_name). Not only do we know what the builder for full_name is but we also know it for first_name and last_name. MooseX::Invertible_Build goes ahead and defines the default methods for first_name and last name with their respective split statements.

      So right now concrete idea for MooseX::Invertible_Build is:
    • a library that contains a number of functions and their inverses that may form part of an expression to describe the relationship between attributes
    • some sugar to associate multiple attributes with the same expression
    • A parser that can parse these expressions and set the appropriate builder methods on all attributes involved

    • Thanks for taking the time to read all this monks. I'll try and get something working this weekend. Any comments/ideas/advice on where to start welcome. Also a description of the mathematical concepts involved would be cool. I used the word 'inverse' a lot which I think is correct, but I forgot all the mathemical jargon to do with sets, functions and mappings etc.

        A problem I see with this is there are two types of builder this could be used for:

        • Simple ones. For example, inverting the following builder:
          sub _build_net_income { my $self = shift; return $self->gross_income - $self->taxes; }
          to automatically create _build_gross_income and _build_taxes.

        • Complex ones. For example, inverting the following:
          sub _build_net_income { my $self = shift; my $gross = $self->gross_income; my $income_tax = do { if ($gross < 6_475) { 0 } elsif ($gross < 43_875) { (0.2 * ($gross - 6_475)) } elsif ($gross < 150_000) { (0.2 * (43_875 - 6_475)) + (0.4 * ($gross - 43_875)) } else { (0.2 * (43_875 - 6_475)) + (0.4 * (150_000 - 43_875)) + (0.5 * ($gross - 150_000)) } }; my $child_tax_credit = 545 * scalar @{$self->children}; my $taxes = $income_tax - $child_tax_credit; $taxes = 0 if $taxes < 0; return $gross - $taxes; }
          to automatically create _build_gross_income and _build_children.

        In the case of complex builders, I don't think your module has much chance of success. In the case of simple ones, you're not saving me much work.

        This is no criticism of the goals of your module.

        But ... you knew it was coming right? ... Do you have a more realistic example of how and why it might be used?

        The problem with using a toy example to illustrate the functionality, is that it ends up looking like a solution looking for a problem.


        With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.

        The start of some sanity?

Re: Idea: Moose Mutal Coercion
by educated_foo (Vicar) on Feb 14, 2012 at 12:13 UTC
    It seems like you're making your life harder than you have to. You would be much better off picking one standard data representation and supplying multiple initialization functions, e.g.:
    package Identity; sub init_first_last { my ($c, $first, $last) = @_; die unless $first =~ /^\S+$/ && $last =~ /^\S+$/; bless { first => $first, last => $last }, $c; } sub init_fullname { my $c = shift; my ($first, $last) = split ' ', shift; init_first_last $c, $first, $last; } sub fullname { join ' ', @{$_[0]}{qw(first last)}; } sub first { shift->{first}; } sub last { shift->{last}; }

      Well, I was going to convert your code into a moose BUILDARGS method, but decided that I think the best approach along this line would be simply lazy-building. Of course, the advantage of the OP's approach is re-usability. The re-usability can be mostly recovered by moving the attributes to a role:

      package HumanName; use Moose::Role; use re 'taint'; # Can use isa => first_name_type ... if you prefer for (qw/ first last full /) { has $_."_name => isa => "Str", lazy_build => 1, predicate => "has_ +${_}_name"; } sub _build_full_name { my $self = shift; if ($self->has_last_name and $self->has_first_name) { return join " ", $self->first_name, $self->last_name; } if ($self->does("Gender")) { return "John Doe" if $self->has_gender and "M" eq $self->gende +r; return "Jane Doe" if $self->has_gender and "F" eq $self->gende +r; } # ... whatever complex constructions we like die "Can not build full name"; } sub _build_first_name { my $self = shift; return (split /\s+/, $self->full_name)[0];# or more complex chain. +.. } sub _build_last_name { my $self = shift; return (split /\s+/, $self->full_name)[1];# or more complex chain. +.. }

      Though, hopefully you intend to use these methods only to provide (semi-sane) defaults when first/last/full are needed but not known - names are too complex for the above to be reliable generally.

      Good Day,
          Dean

Re: Idea: Moose Mutual Coercion
by sundialsvc4 (Abbot) on Feb 15, 2012 at 15:32 UTC

    has also allows an array ...   has ['foo', 'bar', 'bletch', 'y2', 'plugh'] => ...

    Now arrange to get the property-name that is being requested and set up a hashref, indexed by property name, where each element is a sub (closure...) that returns it.   Which I leave as an exercise to the reader.