Quite a while ago, Santa got a feature request for a web application called AGRAMMON, developed by the elves of one of his sub-contractors Oetiker+Partner AG in what then was called Perl 5. When Santa asked the elf responsible for this application to get to work, the elf suggested that some refactoring was in order, as the application dated back almost 10 years and had been extended regularly.
As the previous year had seen a real Christmas wonder, namely the release of Perl 6c, the elf suggested, that instead of bolting yet another feature onto the web application’s Perl backend, a rewrite in Perl 6 would be a bold but also appropriate move. The reason being that the application used a specially developed format for describing it’s functionality by none-programmers. What better choice for rewriting the parser than Perl 6’s grammars, the elf reasoned. Fittingly, the new AGRAMMON was going to be version 6.
When Santa asked when the rewrite would be finished, the elf’s obivous answer was “by Christmas”. And as things went in Perl 6 land, by the time the rewrite is finally going into production, the backend is now implemented in Raku.
AGRAMMON
Most people nowadays know about the negative side-effects of agriculture on climate, namely emissions of methane and nitrous oxide (strong greenhouse gases) and deforestation. A lesser known, but also significant environmental problem, are ammonia (NH3) and nitrous oxide (NOx) emissions. For the agricultural production, NH3 is a major gaseous pollutant while NOx is of minor importance.
The main source of these emissions are excretions of farm animals, mainly from cattle, pigs, and poultry. Both liquid and solid manure contain nitrogen compounds such as urea. These compounds are decomposing when the excretions are deposited onto the farm surfaces, subsequently stored in manure stores, and when applied to the field as fertilizer.
In addition to induce negative environmental impacts, these emissions entail a substantial loss of nitrogen (N) from manure and either result in diminished productivity of farms or the loss must be compensated by mineral fertilizers at additional costs to the farmers. In Switzerland alone, about 40,000 tonnes of nitrogen are lost every year, amounting to about 30% of the N load in manure.
In order to address these problems, the processes of ammonia volatilisation are studied, emission mitigation options developed, and the effects measured where possible under controlled conditions. However, as controlled conditions are difficult to implement at farm-scale, the effects of the reduction measures as well as the total amount of emissions can be simulated by model calculations. AGRAMMON is a tool that facilitates such simulations at the scale of a single farm. Such calculations can also be done at regional scale by means of simulating “typical farm types” using average process types and cumulated numbers of animals, storage facilities, and fertilizer application. The following picture shows the processes simulated by the model:

The Application
AGRAMMON is a typical web application, with data stored in a PostgreSQL database, a web frontend implemented in JavaScript using the Qooxdoo framework , and a Raku backend. The physical and chemical processes are not directly implemented in the backend, but as already mentioned in a none-programmer-friendly custom “language”, describing (user) inputs, model parameters, calculations, and outputs (results).
Each process is broken down into smaller sub-processes and each is described in its own file, including documentation and references to appropriate scientific sources. Here is a small example for such a file:
*** general ***
author = Agrammon Group
date = 2008-03-30
taxonomy = Livestock::DairyCow::Excretion
+short
Computes the annual N excretion of a number of dairy cows as a function of the
milk yield and the feed ration.
+description
This process calculates the annual N excretion (total N and Nsol (urea plus
measured total ammoniacal nitrogen)) of a number of dairy cows as a
function of the milk yield and the supplied feed ration. Nitrogen
surpluses from increased nitrogen uptake are primarily excreted as
Nsol in the urine. Eighty percent of the increased N excretion is
therefore added to the Nsol fraction.
*** input parameters ***
+dairy_cows
type = integer
validator = ge(0)
++labels
en = Number of animals
de = Anzahl Tiere
fr = Nombre d'animaux
++units
en = -
++description
Number of dairy cows in barn.
++help
+++en
<p>Actual number of animals
in the barn.</p>
+++de ...
+++fr ...
*** technical parameters ***
+standard_N_excretion
value = 115
++units
en = kg N/year
de = kg N/Jahr
fr = kg N/an
++description
Annual standard N excretion for a
dairy cow according to
Flisch et al. (2009).
*** external ***
+Excretion::CMilk
+Excretion::CFeed
*** output ***
+n_excretion
print = 7
++units
en = kg N/year
de = kg N/Jahr
fr = kg N/an
++formula
Tech(standard_N_excretion)
* Val(cmilk_yield, Excretion::CMilk)
* Val(c_feed_ration,Excretion::CFeed)
* In(dairy_cows);
++description
Annual total N excreted by a specified
number of animals.
In the current version of the AGRAMMON model there are 133 such model files with 31,014 lines. From those, the backend can generate
- the PDF documentation of the model (allowing LaTeX formatting in the files)
- the actual model simulation using the user’s input data
- a description of the web GUI which can be rendered by the frontend

The results are presented in the web GUI in tabular form (showing various subsets of the data that can also be defined in the model files)

and can be exported as PDF report or Excel file, together with the actual inputs provided by the user.
A special instance of AGRAMMON is used by a regional government agency in the evaluation process of the environmental impact of modifications to local farms and the approval of the respective building applications. For this, the ammonia emissions before and after the planned modifications must be simulated by the applicant and can be directly submitted to the agency’s AGRAMMON account, including a notification of the agency by eMail with the PDF report attached.
The Raku backend
The refactored backend as of today consists of 59 .pm6
modules/packages with 6,942 lines and is covered by tests in 38 .t
files with 5,854 lines. It uses the 13 Raku modules shown in the following excerpt of the META6.json file:
"depends": [
"Cro::HTTP",
"Cro::HTTP::Session::Pg",
"Cro::OpenAPI::RoutesFromDefinition",
"Cro::WebApp::Template",
"DB::Pg",
"Digest::SHA1::Native",
"Email::MIME",
"LibXML:ver<0.5.10>",
"Net::SMTP::Client::Async",
"OO::Monitors",
"Spreadsheet::XLSX:ver<0.2.1+>",
"Text::CSV",
"YAMLish"
],
"build-depends": [],
"test-depends": [
"App::Prove6",
"Cro::HTTP::Test",
"Test::Mock",
"Test::NoTabs"
],
Those modules can be found on the Raku Modules Directory. Note that Spreadsheet::XLSX
was specifically implemented for this project. As a side-effect, just yesterday our expert elf (see below) submitted a pull request for LibXML used in Spreadsheet::XLSX leading to a factor of 2 performance improvement.
Speaking of the actual implementation, although our brave elf didn’t have much experience with either grammars, parsers, or even Perl 6 / Raku, he was smart enough to engage a real expert elf for that. This elf did most of the heavy lifting of the backend implementation and helped our elf with advice and code review for the parts he implemented himself.
Please note that the goal of this rewrite was to leave most of the syntax of the model implementation and also the frontend as is, so the blame for all the sub-optimal design decisions are solely on our primary elf as well as the responsibility for imperfect implementation details passing under the review radar.
Some Raku features used in AGRAMMON
In this section we’ll present a few Raku features used in AGRAMMON. This is not meant as a hardcore technical explanation for experts, but rather as a means to give a taste to people interested in Raku.
Most code examples are taken straight from the current implementation, sometimes the examples are slightly shortened by leaving of code that is irrelevant to the concept presented. There are many links to the original modules on GitHub. As AGRAMMON is still being worked on, those links might point at a more recent version of the module.
bin/agrammon.pl6
The actual AGRAMMON “executable” is just a three-liner (of which only two are Raku):
#!/usr/bin/env raku
use lib "lib"
use Agrammon::UI::CommandLine;
This exploits the fact that Rakudo (the Raku implementation used here) has a pretty nice pre-compilation feature which is useful for minimizing (the still not neglegible) startup time after the first run of the program.
Agrammon::UI::CommandLine
This module contains the main functions of the AGRAMMON application available from the command line.
Usage
Running ./bin/agrammon.pl6
gives the following output:
Usage: ./bin/agrammon.pl6 web <cfg-filename> <model-filename> [<technical-file>] -- Start the web interface ./bin/agrammon.pl6 [--language=<SupportedLanguage>] [--prints=<Str>] [--variants=<Str>] [--include-filters] [--include-all-filters] [--batch=<Int>] [--degree=<Int>] [--max-runs=<Int>] [--format=<OutputFormat>] run <filename> <input> [<technical-file>] -- Run the model ./bin/agrammon.pl6 [--variants=<Str>] [--sort=<SortOrder>] dump <filename> -- Dump model ./bin/agrammon.pl6 [--variants=<Str>] [--sort=<SortOrder>] latex <filename> [<technical-file>] ./bin/agrammon.pl6 create-user <username> <firstname> <lastname> -- Create Agrammon user <cfg-filename> configuration file <model-filename> top-level model file [<technical-file>] optionally override model parameters from this file See https://www.agrammon.ch for more information about Agrammon.
This usage message is created automagically from the implementation of the multi
subroutine MAIN
instances as shown for the first line:
subset ExistingFile of Str where { .IO.e or note("No such file $_") && exit 1 }
#| Start the web interface
multi sub MAIN(
'web',
ExistingFile $cfg-filename, #= configuration file
ExistingFile $model-filename, #= top-level model file
ExistingFile $technical-file? #= override model parameters from this file
) is export {
my $http = web($cfg-filename, $model-filename, $technical-file);
react {
whenever signal(SIGINT) {
say "Shutting down...";
$http.stop;
done;
}
}
}
Note that the parameter $technical-file
is marked as optional by the trailing ?
and that the usage message thus also marks this parameter as optional by enclosing it in [ ]
.
The first line in the above code example defines a subset
ExistingFile
of the data type Str
, namely those strings that refer to a locally existing file. If called with a filename foo.cfg
of a none-existing file, the program aborts with the message No such file foo.cfg
.
The usage message also shows the command line calls for
- running the model in batch mode from the command line (
run
), - showing the simulation flow by
dump
ing the model structur, - generation of the model documentation (
latex
), - and for creation of user accounts for the web application (
create-user
), - and at the end lists those parameters that have comments “attached” in the source of the
sub MAIN
above shown above.
sub web()
This subroutine is called to start the web service as shown in the first line of the above usage message.
sub web(Str $cfg-filename, Str $model-filename, Str $technical-file?) is export {
# initialization
# ...
my $model = timed "Load model from $module-path/$module.nhd", {
load-model-using-cache($*HOME.add('.agrammon'), $module-path, $module, preprocessor-options($variants));
}
my $db = DB::Pg.new(conninfo => $cfg.db-conninfo);
PROCESS::<$AGRAMMON-DB-CONNECTION> = $db;
my $ws = Agrammon::Web::Service.new(:$cfg, :$model, :%technical-parameters);
# setup and start web server
my $host = %*ENV<AGRAMMON_HOST> || '0.0.0.0';
my $port = %*ENV<AGRAMMON_PORT> || 20000;
my Cro::Service $http = Cro::HTTP::Server.new(
:$host, :$port,
application => routes($ws),
after => [
Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR)
],
before => [
Agrammon::Web::SessionStore.new(:$db)
]
);
$http.start;
say "Listening at http://$host:$port";
return $http;
}
The subroutine uses a signature to describe it’s arguments (all of them are of type Str
and the third argument is again marked as optional by the trailing ?
.
sub run()
The AGRAMMON application can also be used directly from the command line by providing input data from a CSV file. This mode is used from scientists to automate the running large amounts of simulations for regional and national projections. It is planned to make this mode available via a REST API call in the future.
sub run (IO::Path $path, IO::Path $input-path, $technical-file, $variants, $format, $language, $prints,
Bool $include-filters, $batch, $degree, $max-runs, :$all-filters) is export {
# initialization
# ...
my $rc = Agrammon::ResultCollector.new;
my atomicint $n = 0;
my class X::EarlyFinish is Exception {}
race for $ds.read($fh).race(:$batch, :$degree) -> $dataset {
my $my-n = ++⚛$n;
my $outputs = timed "$my-n: Run $filename", {
$model.run(
input => $dataset,
technical => %technical-parameters,
);
}
# create output
# ...
}
Here we use race
, one of the various concurrency features of Raku, to run the actual model simulation using multiple threads in parallel to speed-up execution.
The function’s signature again specifies the types of some parameters. In addition to (too many) positional arguments, :$all-filters
is a by default optional named argument.
Agrammon::Web::Routes
While already having shown the start-up of the web service above, here we see an example of setting up the routes of AGRAMMON’s REST interface using Cro::HTTP::Router
from Edument’s Cro Services:
use Cro::HTTP::Router;
use Cro::OpenAPI::RoutesFromDefinition;
use Agrammon::Web::Service;
use Agrammon::Web::SessionUser;
subset LoggedIn of Agrammon::Web::SessionUser where .logged-in;
sub routes(Agrammon::Web::Service $ws) is export {
my $schema = 'share/agrammon.openapi';
my $root = '';
route {
include static-content($root);
include api-routes($schema, $ws);
...
after {
forbidden if .status == 401 && request.auth.logged-in;
.status = 401 if .status == 418;
}
}
}
sub static-content($root) {
route {
get -> {
static $root ~ 'public/index.html'
}
...
}
}
sub api-routes (Str $schema, $ws) {
openapi $schema.IO, {
# working
operation 'createAccount', -> LoggedIn $user {
request-body -> (:$email!, :$password!, :$key, :$firstname, :$lastname, :$org, :$role) {
my $username = $ws.create-account($user, $email, $password, $key, $firstname, $lastname, $org, $role);
content 'application/json', { :$username };
CATCH {
note "$_";
when X::Agrammon::DB::User::CreateFailed {
not-found 'application/json', %( error => .message );
}
when X::Agrammon::DB::User::AlreadyExists
| X::Agrammon::DB::User::CreateFailed {
conflict 'application/json', %( error => .message );
}
}
}
}
...
}
the latter using the (abbreviated) OpenAPI definition
openapi: 3.0.0
info:
version: 1.0.0,
title: OpenApi Agrammon,
paths:
/create_account:
post:
summary: Create new user account
operationId: createAccount
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
- password
properties:
email:
description: User's email used as username
type: string
firstname:
description: Firstname
type: string
lastname:
description: Lastname
type: string
responses:
'200':
description: Account created.
content:
application/json:
schema:
type: object
required:
- username
properties:
username:
type: string
'404':
description: Couldn't create account
content:
application/json:
schema:
$ref: "#/components/schemas/CreationFailed"
'409':
description: User already exists
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
handled by Cro::OpenAPI::RoutesFromDefinition
.
Agrammon::OutputFormatter::PDF
For documenting AGRAMMON calculations, PDF reports of the inputs to the model and the simulation results can be created by first creating a LaTeX file using the Cro::WebApp::Template
module. While tailored towards generation of HTML pages, it worked quite well for our purpose. The module does escape its input data appropriate for HTML, however, a simple-minded escaping of characters with special meaning in LaTeX was implemented outside the module:
sub latex-escape(Str $in) is export {
my $out = $in // '';
$out ~~ s:g/<[\\]>/\\backslash/;
$out ~~ s:g/(<[%#{}&$|]>)/\\$0/;
$out ~~ s:g/(<[~^]>)/\\$0\{\}/;
# this is a special case for Agrammon as we use __ in
# the frontend at the moment for indentation in the table
$out ~~ s:g/__/\\hspace\{2em\}/;
$out ~~ s:g/_/\\_/;
return $out;
}
An addition function is used for beautification of chemical molecules:
sub latex-chemify(Str $in) is export {
my $out = $in // '';
$out ~~ s:g/NOx/\\ce\{NO_\{\(x\)\}\}/;
$out ~~ s:g/(N2O|NH3|N2|NO2)/\\ce\{$0\}/;
return $out;
}
These functions use simple regular expression substitutions. A more generic handling of LaTeX special characters would need porting something like the LaTeX::Encode
Perl module to Raku. Alternatively, Inline::Perl5
could be employed to utilize the Perl module.
This code fragment shows how a LaTeX file is created
%data<titles> = %titles;
%data<dataset> = $dataset-name // 'NO DATASET';
%data<username> = $user.username // 'NO USER';
%data<model> = $cfg.gui-variant // 'NO MODEL';
%data<timestamp> = ~DateTime.now( formatter => sub ($_) {
sprintf '%02d.%02d.%04d %02d:%02d:%02d',
.day, .month, .year,.hour, .minute, .second,
});
%data<version> = latex-escape($cfg.gui-title{$language} // 'NO VERSION');
%data<outputs> = @output-formatted;
%data<inputs> = @input-formatted;
%data<submission> = %submission;
template-location $*PROGRAM.parent.add('../share/templates');
my $temp-dir = $*TMPDIR.add($temp-dir-name);
my $source-file = "$temp-dir/$filename.tex".IO;
my $latex-source = render-template('pdfexport.crotmp', %data);
$source-file.spurt($latex-source, %data);
by calling render-template
with a %data
hash and a template file pdfexport.crotmp
like
\nonstopmode
\documentclass[10pt,a4paper]{article}
\begin{document}
\section*{<.titles.report>}
\section{<.titles.data.section>}
\begin{tabular}[t]{@{}l@{\hspace{2em}}p{7cm}}
\textbf{<.titles.data.dataset>:} & <.dataset>\\
\textbf{<.titles.data.user>:} & <.username>\\
\textbf{Version:} & <.model>\\
\end{tabular}
\section{<.titles.outputs>}
<@outputs>
<?.section>
<!.first>
\bottomrule
\end{tabular}
</!>
\subsection{<.section>}
\noindent
\rowcolors{1}{LightGrey}{White}
\begin{tabular}[t]{lllrl}
\toprule
</?>
<!.section>
& & <.label> & <.value> & <.unit>\\
</!>
</@>
\bottomrule
\end{tabular}
\end{document}
as arguments. The generated LaTeX source is then written to a file using the spurt
function.
While the above template might seem a bit cryptic if you are not familiar with LaTeX, the relevant parts are the HTML-like tags like <.titles.report>
accessing a value of the hash data structured passed to render-template
, <@output> ... </@>
being an array in this data structure being iterated over, or the conditionals <?.section> ... </?>
or <!.section> ... </!>
. For details please consult the documentation of the Cro::WebApp::Template
module.
The LaTeX file is then rendered into a PDF file with the external program lualatex
and the built-in Proc::Async
class:
# setup temp dir and files
my $temp-dir = $*TMPDIR.add($temp-dir-name);
my $source-file = "$temp-dir/$filename.tex".IO;
my $pdf-file = "$temp-dir/$filename.pdf".IO;
my $log-file = "$temp-dir/$filename.log".IO;
# create PDF, discard STDOUT and STDERR (see .log file if necessary)
my $exit-code;
my $signal;
my $reason = 'Unknown';
my $proc = Proc::Async.new: :w, '/usr/bin/lualatex',
"--output-directory=$temp-dir", '--no-shell-escape', '--', $source-file, ‘-’;
react {
# discard any output of the external program
whenever $proc.stdout.lines {
}
whenever $proc.stderr {
}
# save exit code and signal if program was terminated
whenever $proc.start {
$exit-code = .exitcode;
$signal = .signal;
done; # gracefully jump from the react block
}
# make sure we don't end up with a hung-up lualatex process
whenever Promise.in(5) {
$reason = 'Timeout';
note ‘Timeout. Asking the process to stop’;
$proc.kill; # sends SIGHUP, change appropriately
whenever Promise.in(2) {
note ‘Timeout. Forcing the process to stop’;
$proc.kill: SIGKILL
}
}
}
# write appropriate error messages if program didn't terminate sucessfully
if $exit-code {
note "$pdf-prog failed for $source-file, exit-code=$exit-code";
die X::Agrammon::OutputFormatter::PDF::Failed.new: :$exit-code;
}
if $signal {
note "$pdf-prog killed for $source-file, signal=$signal, reason=$reason";
die X::Agrammon::OutputFormatter::PDF::Killed.new: :$reason;
}
# read content of PDF file created in binary format for further use
my $pdf = $pdf-file.slurp(:bin);
# remove created files if successful, otherwise keep for debugging
unlink $source-file, $pdf-file, $aux-file, $log-file unless %*ENV<AGRAMMON_KEEP_FILES>;
A react
block with several whenever
blocks is used to handle the events from the asynchronously running external program to avoid blocking of the otherwise already asynchronous backend.
Typed exceptions are used to handle errors occuring in the external process.
Agrammon::OutputFormatter::Excel
Here we create Excel exports of the simulation results and the user inputs, using Spreadsheet::XLSX
. This module allows to read and write XLSX files from Raku. The current functionality is by no means complete, but implements what was needed for AGRAMMON. Please feel free to provide pull requests or funds for the implementation of additional features.
# get data to be shown
my %data = collect-data();
# ...
my $workbook = Spreadsheet::XLSX.new;
# prepare sheets
my $output-sheet = $workbook.create-worksheet('Results');
my $input-sheet = $workbook.create-worksheet('Inputs');
my $timestamp = ~DateTime.now( formatter => sub ($_) {
sprintf '%02d.%02d.%04d %02d:%02d:%02d',
.day, .month, .year, .hour, .minute, .second,
});
# add some meta data to the sheets
for ($output-sheet, $input-sheet) -> $sheet {
$sheet.set(0, 0, $dataset-name, :bold);
$sheet.set(1, 0, $user.username);
$sheet.set(2, 0, $model-version);
$sheet.set(3, 0, $timestamp);
}
# set column width
for ($output-sheet, $input-sheet) -> $sheet {
$sheet.columns[0] = Spreadsheet::XLSX::Worksheet::Column.new:
:custom-width, :width(20);
$sheet.columns[1] = Spreadsheet::XLSX::Worksheet::Column.new:
:custom-width, :width(32);
$sheet.columns[2] = Spreadsheet::XLSX::Worksheet::Column.new:
:custom-width, :width(20);
$sheet.columns[3] = Spreadsheet::XLSX::Worksheet::Column.new:
:custom-width, :width(10);
}
# add input data to sheets
my $row = 0;
my $col = 0;
my @records := %data<inputs>;
for @records -> %rec {
$input-sheet.set($row, $col+2, %rec<input>);
$input-sheet.set($row, $col+3, %rec<value>, :number-format('#,#'), :horizontal-align(RightAlign));
$input-sheet.set($row, $col+4, %rec<unit>);
$row++;
}
# add output data to sheets
# ...
This example shows a variety of Raku basics:
%data
,%rec
are hash variables. Contrary to Perl, in Raku the sigils don’t change when accessing elements of variables.for ($output-sheet, $input-sheet) -> $sheet { ... }
andfor @records -> %rec { ... }
are loops over a lists, each assigning the current element to a variable in the loop’s scope using the pointy block syntax.my $timestamp = ~DateTime.now( formatter => sub ($_) { ... } ...)
uses the builtinDateTime
method to create a timestamp, using the~
operator to coerce it into a string. The string is being formatted by the unamed anonymous subroutinesub ($_) { ... }
which uses the topic variable$_
as argument on which the various methods of the the DateTime class are being called by just prepending a.
For example,.year
is just a short-cut for$_.year
.
Agrammon::Email
As mentioned above PDF reports of simulations can be mailed to certain AGRAMMON users directly from the web application. First, a multi-part MIME message is created using the Email::MIME
module.
# create PDF attachment
my $attachment = Email::MIME.create(
attributes => {
'content-type' => "application/pdf; name=$filename",
'charset' => 'utf-8',
'encoding' => 'base64',
},
body => $pdf,
);
# create main body part
my $msg = Email::MIME.create(
attributes => {
'content-type' => 'text/plain',
'charset' => 'utf-8',
'encoding' => 'quoted-printable'
},
body-str => 'Attached please find a PDF report from a AGRAMMON simulation,
);
# build multi-part Email
my $from = 'support@agrammon.ch';
my $to = 'foo@bar.com';
my $mail = Email::MIME.create(
header-str => [
'to' => $to,
'from' => $from,
'subject' => 'Mail from AGRAMMON'
],
parts => [
$msg,
$attachment,
]
);
This message is then sent to the mail’s recipient using the promise based Net::SMTP::Client::Async
module:
# asynchronously send Email via AGRAMMON's SMTP server
with await Net::SMTP::Client::Async.connect(:host<mail.agrammon.ch>, :port(25), :!secure) {
# wait for SMTP server's welcome response
await .hello;
# send message
await .send-message(
:$from,
:to([ $to ]),
:message(~$mail),
);
# terminate connection on exit
LEAVE .quit;
# catch exceptions and emit user friendly error message
CATCH {
when X::Net::SMTP::Client::Async {
note "Unable to send email message: $_";
}
}
}
The await
function is used to handle the asynchronous communication with the SMPT server. The LEAVE
is called upon exit from the with await { ... }
block to close the connection to the server.
Unicode operators
My favorite code fragments used in AGRAMMON demonstrate the use auf Unicode codepoints in Raku source code:
my $my-n = ++⚛$n;
is incrementing a variable of typeatomicint
,- and
$var-print.split(',') ∩ @print-set
gives the intersection of two sets.
While Unicode can also be used for other purposes, e.g. for numerical values like ⅓, 𝑒, π, or τ or in variable names my $Δ = 1;
, their use as operators definitely makes for better readable code (compare ∩
to (&)
).
Apart from using the appropriate mathematical symbol, the existence of such powerful operators is by itself a great feature of Raku and makes for much shorter code than implementing such operations in other programming languages.
Parser and Compiler
Finally, a few words about the parser and compiler used to process the AGRAMMON model files shown above. Agrammon::ModuleParser
is the top-level element for parsing the model files:
use v6;
use Agrammon::CommonParser;
grammar Agrammon::ModuleParser does Agrammon::CommonParser {
token TOP {
:my $*TAXONOMY = '';
:my $*CUR-SECTION = '';
<.blank-line>*
<section>+
[
|| $
|| <.panic('Confused')>
]
}
proto token section { * }
token section:sym<general> {
<.section-heading('general')>
[
| <option=.single-line-option>
| <option=.multi-line-str-option('+')>
| <.blank-line>
]*
}
token section:sym<external> {
<.section-heading('external')>
[
| <.blank-line>
| <external=.option-section>
]*
}
token section:sym<input> {
<.section-heading('input')>
[
| <.blank-line>
| <input=.option-section>
]*
}
token section:sym<technical> {
<.section-heading('technical')>
[
| <.blank-line>
| <technical=.option-section>
]*
}
token section:sym<output> {
<.section-heading('output')>
[
| <.blank-line>
| <output=.option-section>
]*
}
token section:sym<results> {
<.section-heading('results')>
[
| <.blank-line>
| <results=.option-section>
]*
}
token section:sym<tests> {
<.section-heading('tests')>
[
| <.blank-line>
| <tests=.option-section>
]*
}
}
It handles parsing of the various sections of the model file sections, using various elements from module Agrammon::CommonParser
such as
token section-heading($title) {
\h* '***' \h* $title \h* '***' \h* \n
{ $*CUR-SECTION = $title }
}
token option-section {
\h* '+' \h* <name> \h* \n
[
| <.blank-line>
| <option=.single-line-option>
| <option=.subsection-map>
| <option=.multi-line-str-option('++')>
]*
}
token single-line-option {
\h* <key> \h* '=' \h*
$<value>=[[<!before \h*'#'>\N]*]
\h* ['#'\N*]?
[\n || $]
}
token blank-line {
| \h* \n
| \h* '#' \N* \n
| \h+ $
}
Raku grammars are basically build top-down from regular expressions. Such grammars can be extended by means of action classes that further process the match objects generated while parsing the data fed to the grammar.
Please consult this tutorial or other resources to learn more about those concepts.
If you want to know more about the (real-world) AGRAMMON parser/compiler you can have a look at the other parser elements in the Agrammon::Formula::Parser
, Agrammon::Formula::Builder
, Agrammon::ModuleBuilder
, Agrammon::TechnicalParser
, Agrammon::TechnicalBuilder
, and Agrammon::LanguageParser
modules, the latter being a simple none-grammmar based function.
The compiler consists of the modules Agrammon::Formula::Compiler
and Agrammon::Formula::Builtins
.
Finally, as a recent addition, AGRAMMON also got a C-style preprocessor in Agrammon::Preprocessor
for conditionally including or excluding parts of the model using the following syntax:
?if FOO
...
?elsif BAR
...
?else
...
?endif
with optional ?elsif
and ?else
parts. The keywords can also be negated, such as ?if !FOO
.
So, which Christmas?
Well, as you can see from this presentation at the Swiss Perl Workshop 2018, the original plan was not quite met, mostly due to another project being given higher priority (which was a very poor decision, but this is another long story).
We had hoped to have AGRAMMON 6 deployed and in production before the appearance of this article and almost suceeded. All the critical features are in place, a bit of polishing is still to be done. One of the biggest relieves for our brave elf was that test calculations done with the Perl and Raku backends using the same model and input values gave identical results. As those implementations were done not only in two different languages, but also by different programmers with a completely different architecture, this gives a lot of trust in their correctness.
In addition, the customer has done a pretty extensive refactoring of the model files and is currently in the process of verifying both the model calculations and the functionality of the Raku based web application.
The current setup is already online as demo/test version and you are welcome to give it a try. We expect the Raku implementation to finally go into production in early 2021 and to replace the current Perl implementation.
Conclusion
Is Raku ready for use in production? Definitely yes!
While having already delivered a few smaller customer projects implemented in Raku, AGRAMMON 6 will be Oetiker+Partner AG’s first publically accessible web application with a Raku backend and we hope for many more to come. It was a great pleasure to work with our colleague on this project and we also want to thank our customer and partners for this opportunity.
And the most important outcome: Santa now has another elf able to work on future Raku projects. Raku is a very rich language and have no doubt, “There’s more than one way to do it”, for any defintion of “it”. While it is not necessary to learn everything at once, it is certainly helpful to have some expert knowledge near by to ask questions and to learn about the more elegant and often very concise options available. The Raku community is very friendly and welcoming to newcomers (and even to the occasional trolls hanging around).
Raku itself tries very hard to help the programmer to not shoot himself into the foot. Error messages are often very helpful and so are the problem reports or suggestions the Comma IDE has to offer (and they become more and more with every release). So, go ahead and take a dive!
Hi @zaucker – great to see Raku in production and a very interesting use-case. This kind of eco-calc / comparison is a main driver of why I am writing raku Physics::Unit and Physics::Measure.
As a piece of fun, I thought I would try this…
1 #!/usr/bin/env raku
2 use lib ‘../lib’;
3 use Physics::Unit;
4 use Physics::Measure;
5
6 my $emission ♎️ ’11 kg N/year’;
7 say ~$emission;
8 say $emission.pretty; #official SI units
Output is:
11 kg N/year
11 m⋅kg²⋅s⁻³
Pleased to say that that example works OOTB!!
LikeLiked by 2 people
Hi, thanks for the nice words. About your comments:
Cheers,
Fritz
LikeLiked by 2 people
Hi Fritz – good point – so much for the “it works” [red face] – this is now an open issue… I will get it fixed in the next release. I will have a think about the best way to reconcile the N vs N ambiguity too… thanks for the info.
LikeLiked by 1 person
Hi Steve (?),
didn’t want to make you blush 🙂
I guess about N, the only useful approach I can see is
my $N_emission = 11 kg/yr;
or something similar. I guess claiming the unit to be “kg N / year” as I do in Agrammon’s result table (see screenshot) is the problem. Obviously works well enough for people or at least for people familiar with the field of work, but certainly not for machine processing.
Happy holidays,
Fritz
LikeLiked by 1 person
1 #!/usr/bin/env raku
2 use lib '../lib';
3 use Physics::Unit;
4 use Physics::Measure;
5
6 my $nempu = Unit.new( defn => 'kg/year', names => ['kg N/year'] );
7 $nempu.NewType( 'Emissions' );
8 class Emissions is Measure {}
9
10 my $em1 ♎️ '11 kg N/year';
11 say "Result one is $em1 of ", $em1.WHAT;
12
13 Unit.new( defn => 'kg/year', names => ['kg CH₄/year'] ); #subscript 4 is (U+2084)
14
15 my $em2 ♎️ '16 kg CH₄/year';
16 say "Result two is $em2 of {$em2.units.type}…";
17 say "…which is {$em2.pretty} in SI Units.";
18
Now gives the right answers… (will take a few days to release this version)
Result one is 11 kg N/year of (Emissions)
Result two is 16 kg CH₄/year of Emissions……which is 0.00000051 kg⋅s⁻¹ in SI Units.
LikeLiked by 1 person
Cool!
LikeLiked by 1 person