5

I'd like to know how I can run operations like this

$T = 25 C;
@specs = (273.15 K, 23 bar, 2.0 mol/s);

and get them to compile. I'm not picky about what their result is, or how it's implemented. My goal is to let expressions for physical quantities with conventional postfix unit annotations compile to perl expressions for those units.

I think I need to use custom parsing techniques, but I'd prefer to use any existing functionality or parsing modules over just applying regex filters to my raw source.

Parse::Keyword seemed promising, but I can't see whether it can parse postfix operations, and it claims to be deprecated.

Edit: I'd like to avoid source filters if possible because I'd rather not write regexes for Perl's syntactical corner cases (e.g. "25 (J/K)").

The error Perl produces here is telling:

perl -E "25 C"
Bareword found where operator expected at -e line 1, near "25 C"
(Missing operator before C?)

It seems like I need to hook into where Perl detects operators after numeric literals.

Could Devel::Declare add postfix operators? If so, how?

alexchandel
  • 532
  • 6
  • 15
  • 3
    Sounds like a job for a [source filter](http://perldoc.perl.org/perlfilter.html). Understand, though, that [source filters are almost always a bad idea](http://stackoverflow.com/questions/1785852/why-are-perl-source-filters-bad-and-when-is-it-ok-to-use-them). I would recommend an approach that doesn't require screwing with perl's parsing rules; it's not as pretty, but I would prefer something like `$T = My::Measurement->new(value => 25, units => 'C');`. – ThisSuitIsBlackNot Aug 02 '16 at 16:57
  • The goal of this module is to explicitly allow the terse form, like a domain-specific language. – alexchandel Aug 02 '16 at 17:16
  • Please describe more precisely the result that you would like. Is `$T = '25 C'` acceptable? – Borodin Aug 02 '16 at 17:16
  • No. My goal is to not quote these expressions and parse them later manually, but to teach the perl compiler (or hack into perl's parsing) just enough so that it can parse these postfix expressions itself. – alexchandel Aug 02 '16 at 17:25
  • I don't think you can do this with Devel::Declare because it only lets you inject new text starting at the position of your declarator (e.g. `C` in `$T = 25 C;`). You *could* write your own XS module that uses the same hooks as Devel::Declare, though. – ThisSuitIsBlackNot Aug 02 '16 at 19:41
  • 3
    Would you find writing `$T = 25 .C` acceptable? The idea is to (ab)use operator overloading to construct the objects. Proof of concept [here](https://ideone.com/CxyPBw). – Slade Aug 03 '16 at 00:06

3 Answers3

3

You can abuse overloading to get something close to what you want.

#!/usr/bin/perl

use strict;
use warnings;
use 5.010;

use Data::Dumper;
use MyUnits;

my $T = '25 C';

say Dumper $T;

my @specs = ('273.15 K', '23 bar', '2.0 mol/s');

say Dumper \@specs;

As you'll see, you get objects back with "value" and "type" attributes.

MyUnits.pm looks like this:

package MyUnits;

use strict;
use warnings;

use overload
  '""' => \&to_string;

my %_const_handlers = (
  q => \&string_parser,
);

sub string_parser {
  my $c = eval { __PACKAGE__->new($_[0]) };
  return $_[1] if $@;
  return $c;
}

sub import {
  overload::constant %_const_handlers;
}

sub new {
  my $class = shift;

  # ->new(type => ..., value => ...)
  if (@_ == 4) {
    return bless { @_ }, $class;
  }
  # ->new({ type => ..., value => ...)
  if (@_ == 1 and ref $_[0] eq 'HASH') {
    return bless $_[0], $class;
  }
  # -> new('999xxx')
  if (@_ == 1 and ! ref $_[0]) {
    my ($val, $type) = $_[0] =~ /(\d+\.?\d*)\s*(.+)/;
    return bless({
      value => $val, type => $type,
    });
  }
}

sub to_string {
  return "$_[0]->{value}$_[0]->{type}";
}

1;

You'd want to add more methods to enable it to do something useful.

In most cases, overloading isn't much less of a party trick than source filters are. It will almost certainly make your program far slower.

Dave Cross
  • 68,119
  • 3
  • 51
  • 97
2

If you are willing to use an intermediating function, you can get something that sorta looks like what you want if you squint hard enough. I don't it is possible to write Haskell in Perl ;-)

package My::Units;

use strict;
use warnings;

use Importer 'Math::Units::PhysicalValue', 'PV';

our @EXPORT = qw();
our @EXPORT_OK = qw( with_units );

sub with_units(\$@) {
    my (undef, $value, $units) = @_;
    ${ $_[0] } = PV "$value $units";
    return;
}

__PACKAGE__;
__END__

Use it from a script:

#!/usr/bin/env perl

use feature 'say';
use strict;
use warnings;

use lib '.';
use Importer 'My::Units', 'with_units';

with_units my $x => 25 => 'C';
with_units my $y => 20 => 'F';
with_units my $z =>  0 => 'C';

say $x + $y;
say $y + $z;

Output:

C:\...\t> perl t.pl
97 F
-6.67 C

Now, Math::Units::PhysicalValue unconditionally uses Math::BigFloat so arithmetic should be slow, but accurate. If you really need this kind of thing, you might want to look into scavenging parts of Math::Units and Math::Units::PhysicalValue and creating something speedier from those parts.

Sinan Ünür
  • 116,958
  • 15
  • 196
  • 339
1

Source filters are notoriously brittle but probably the easiest way of getting what you want without diving deep into perl's less than appetizing bowels. Something like this perhaps:

package U;
use strict;
use warnings;
use Filter::Simple;

my @UNITS = qw( degC degK bar mol s );

FILTER {
    my $unit_re = '(?:' . join('|', @UNITS) . ')';
    s#(\d+(?:\.\d\+)?)\s?((?:${unit_re}[*/])*$unit_re)\b#Units->new({value => $1, unit => '$2'})#g;
};

package Units;
use Class::Accessor 'antlers';
has value => ( is => "ro", isa => "Num" );
has unit => ( is => "ro", isa => "Str" );

1;

I was anal about it and changed the "C" as it looks you don't mean Coulomb. You could probably go all use utf8 and write °C though ;)

Test:

perl -I. -MU -e'my $val = 23 degK/s; printf "Value: %g, Unit: %s\n", $val->value, $val->unit'
Value: 23, Unit: degK/s

Of course the simple regexp leaves much to be desired, like parentheses and such for which you'd probably need Text::Balanced, and the Units class may actually want to to a lot more like parsing that unit string and overloading a few operators so you can calculate with units.

mbethke
  • 935
  • 8
  • 19