August 2022 Archives

Introducing Venus, a new world for Perl 5

Abstract

Programming in Perl is choices all the way down. Perl is a multi-paradigm programming language which means that Perl can support the development of software using different programming paradigms, e.g. functional programming, object-oriented programming, and more.

Programming languages are culture, and culture follows philosophy. Philosophy is how every culture provides itself with justification for its decisions, values, beliefs, and worldview.

Perl's philosophy

Perl's philosophy is TIMTOWTDI, i.e. “there’s more than one way to do it”. You might even say that Perl takes the position of not taking a position, and this disposition applies to the topic of a standard library as well.

To be clear, what some will call Perl's standard library is little more than a grab bag of functions, pragmas, and packages shipped together with the core. This collection is a mix of functional and object-oriented styles and is intentionally lightweight.

It’s probably worth mentioning that the Raku team has also adopted this posture (and tradition).

What if Perl had a standard library?

Not so fast. Let’s say we agree to provide a robust standard library, what paradigm should that library use? Should that library be written in C to maximize performance, or be written in Perl itself? Should the library be readable and serve as an example of how to Perl, or simply be a means to an end? Decisions, decisions.

CPAN in lieu of a standard library

Perl and its CPAN are overwhelmingly object-oriented, and here's where the TIMTOWTDI motto is on full display. This is both a gift and curse, simultaneously the source of Perl's greatest strengths and its greatest weaknesses. Although, even in the face of such an abundance of choice there have been a handful of CPAN distributions that have become de facto standards for the community. Still, the paradox of choice is real and paralyzing, and it’s really hard to find and/or create unity amongst all of the diversity.

Sustained object-orientation in Perl is difficult because the concept and mechanisms were bolted onto the language as an afterthought, and because it's optional, so one has to oscillate between this paradigm and others, i.e. some things are objects, most things are not, so when using Perl you have to constantly reaffirm your object-orientation.

The TL;DR

An OO standard library would make it a lot easier to write Perl in a way that avoids having to come up with similar or the same solutions to common computing tasks. It would make code easier to read and write and eliminate a lot of decision fatigue, especially that which comes with the "search for a library" or "write this library myself" obstacle, which is exactly the type of situation the NIH (not invented here) crowd enjoy. It would make downstream applications more performant and have fewer dependencies. So, is TIMTOWTDI a gift or a curse? Is convention over configuration a straightjacket? Is the Python "batteries included" motto best or bloat?

What's in a standard library

Typically a standard library will have routines to access the file system, and perform I/O operations; routines that work with the data types of the language; routines that allow manipulating memory; routines that enable concurrency/parallelism; routines to handle math, and date and time operations; and routines for error handling and assertions. Often, to ensure optimal performance, these routines are written in very low-level code, e.g. C.

What makes a good standard library

I believe a good standard library should have all of the standard fares, but should also be well organized, be dogfooding itself, and be written as to be readable in ways that help to enforce that derivative programs are idiomatic. In other words, be prescriptive, which Perl tries not to be.

A good standard library should have the goal of helping to reduce the number of decisions that an engineer is required to make without compromising flexibility or the don't repeat yourself (DRY) principles.

A standard library should answer the call and respond to what are people building (in the here and now), or what they're trying to build. What they don't have that they wish they did; what they want to adopt from other languages and frameworks which are successful in ways that we desire our software to be. To facilitate developer ergonomics.

Enter Venus

Introducing Venus, an attempt at establishing an opinionated idiomatic non-core object-oriented standard library for Perl 5, without years of navel-gazing, committee legislation, infighting, or stalling.

Venus has a simple modular architecture, robust library of classes, methods, and traits (roles), supports pure-Perl autoboxing, advanced exception handling, backward-compatible "true" and "false" keyword functions, simple package introspection, command-line options parsing, and much more.

The project motto and ethic is to "be a compliment, not a cudgel". This ethic is a kind of guiding principle that governs decision-making. The Venus system is strictly complimentary, all features are optional and opt-in (even autoboxing; especially autoboxing). This system can be easily extended without monkey-patching (i.e. via plugins), and useful behaviors can be reused outside of the system via traits (roles).

Some Features

  • Supports Perl 5.18.0+
  • Backward-compatible Booleans
  • Composable Standards
  • Exception Handling
  • Fast Object-Orientation
  • Package Reflection
  • Perl-Perl Autoboxing
  • Pluggable Standard Library
  • Robust Documentation
  • Utility Classes
  • Value Classes
  • Zero Dependencies

Guiding Principles

  • The standard library should be a standard
  • The library should have zero-dependencies
  • The library should be prescriptive yet malleable
  • The library should service two major categories of need: values and utilities
  • The library should support wrapping all major native data types
  • The value class methods should use idiomatic pure-Perl algorithms
  • The library should leverage roles and interfaces to maximize behavior-portability
  • The library should expose and make use of the raw materials in the Perl core
  • The library classes can be easily extended (i.e. subclassed)
  • The library should allow plugins and discourage monkey-patching
  • The library should provide mechanisms for error handling (throwing, catching, etc)
  • The library should provide DMMT support for JSON and YAML
  • The library should be consistent in namings and conventions to increase predictability
  • The library should ease the multi-paradigm identity crisis
  • The library should provide robust documentation with maximal test coverage

The World Tour

All Venus packages are classes and can usually be thought to belong in one of four categories; core classes, value classes, utility classes, or abstract behaviors (roles).

CORE CLASSES

Venus - The main module which exports a handful of useful functions that come standard with most other languages.

package main;

use Venus qw(
  catch
  error
  raise
);

# the "catch" function for trapping exceptions, e.g. the "error" function
my ($error, $result) = catch {
  error;
};

# the "true" and "false" keyword functions
if ($result and $result eq false) {
  true;
}

# the "raise" function for raising custom exceptions
if (false) {
  raise 'MyApp::Error';
}

# and much more!
true ne false;

Venus::Class - The class builder module which uses the Mars architecture and supports integrating superclasses, mixins, roles, and interfaces.

package Person;

use Venus::Class 'attr';

attr 'fname';
attr 'lname';

package User;

use Venus::Class 'attr', 'base';

base 'Person';

attr 'email';

package main;

my $user = User->new(
  fname => 'Elliot',
  lname => 'Alderson',
);

# bless({fname => 'Elliot', lname => 'Alderson'}, 'User')

Venus::Mixin - The mixin builder module which uses the Mars architecture and supports is essentially an exporter builder that supports dynamic exports.

package Access;

use Venus::Mixin;

sub login {
  # ...
}

sub logout {
  # ...
}

sub EXPORT {
  ['login', 'logout']
}

package User;

use Venus::Class 'attr', 'mixin';

mixin 'Access';

attr 'email';
attr 'password';

package main;

my $user = User->new(
  fname => 'Elliot',
  lname => 'Alderson',
);

# bless({fname => 'Elliot', lname => 'Alderson'}, 'User')

# $user->login;

# undef

# $user->logout;

# undef

Venus::Role - The role builder module which uses the Mars architecture and supports integrating superclasses, mixins, roles, and interfaces.

package Authenticable;

use Venus::Role 'error';

sub AUDIT {
  my ($self, $from) = @_;

  error "${from} missing the email attribute" if !$from->can('email');
  error "${from} missing the password attribute" if !$from->can('password');
}

package User;

use Venus::Class 'attr', 'with';

with 'Authenticable';

attr 'email';

package main;

# Exception! "User missing the password attribute"

VALUE CLASSES

Venus::Array - The Array class provides methods for manipulating array references.

package main;

use Venus::Array;

my $array = Venus::Array->new([1..4]);

# bless({'value' => [1, 2, 3, 4]}, 'Venus::Array')

$array->count;

# 4

# $array->all(sub{ $_ > 0 });
# $array->any(sub{ $_ > 0 });
# $array->each(sub{ $_ > 0 });
# $array->grep(sub{ $_ > 0 });
# $array->map(sub{ $_ > 0 });
# $array->none(sub{ $_ < 0 });
# $array->one(sub{ $_ == 0 });
# $array->random;

Venus::Boolean - The Boolean class provides methods for representing and operating on boolean values.

package main;

use Venus::Boolean;

my $boolean = Venus::Boolean->new(1);

# bless({'value' => 1}, 'Venus::Boolean')

$boolean->negate;

# 0

Venus::Code - The Code class provides methods for manipulating code references.

package main;

use Venus::Code;

my $code = Venus::Code->new(sub {1});

# bless({'value' => sub {...}}, 'Venus::Code')

$code->call;

# 1

Venus::Float - The Float class provides methods for manipulating floating-point numbers.

package main;

use Venus::Float;

my $float = Venus::Float->new(1.23);

# bless({'value' => '1.23'}, 'Venus::Float')

$float->int;

# 1

Venus::Hash - The Hash class provides methods for manipulating hash references.

use Venus::Hash;

my $hash = Venus::Hash->new({1..8});

# bless({'value' => {'1' => 2, '3' => 4, '5' => 6, '7' => 8}}, 'Venus::Hash')

$hash->count;

# 4

# $hash->all(sub{ $_ > 0 });
# $hash->any(sub{ $_ > 0 });
# $hash->each(sub{ $_ > 0 });
# $hash->grep(sub{ $_ > 0 });
# $hash->map(sub{ $_ > 0 });
# $hash->none(sub{ $_ < 0 });
# $hash->one(sub{ $_ == 0 });
# $hash->random;

Venus::Number - The Number class provides methods for manipulating numeric values.

package main;

use Venus::Number;

my $number = Venus::Number->new(1_000);

# bless({'value' => 1000}, 'Venus::Number')

$number->abs;

# 1000

Venus::Regexp - The Regexp class provides methods for manipulating regular expression references.

package main;

use Venus::Regexp;

my $regexp = Venus::Regexp->new(
  qr/(?<greet>\w+) (?<username>\w+)/u,
);

# bless({'value' => qr/(?<greet>\w+) (?<username>\w+)/u}, 'Venus::Regexp')

$regexp->search('hello venus')->captures;

# ['hello', 'venus']

Venus::Scalar - The Scalar class provides methods for manipulating scalar references.

package main;

use Venus::Scalar;

my $scalar = Venus::Scalar->new;

# bless({'value' => \''}, 'Venus::Scalar')

${$scalar}

# ""

Venus::String - The String class provides methods for manipulating string values.

package main;

use Venus::String;

my $string = Venus::String->new('hello world');

# bless({'value' => 'hello world'}, 'Venus::String')

$string->camelcase;

# "helloWorld"

Venus::Undef - The Undef class provides methods for representing undefined values.

package main;

use Venus::Undef;

my $undef = Venus::Undef->new;

# bless({'value' => undef}, 'Venus::Undef')

$undef->defined;

# 0

UTILITY CLASSES

Venus::Args - The Args class provides methods for accessing and manipulating @ARGS values.

package main;

use Venus::Args;

my $args = Venus::Args->new(
  named => { flag => 0, command => 1 }, # optional
  value => ['--help', 'execute'],
);

# bless({....}, 'Venus::Args')

$args->flag;

# "--help"

# $args->get(0); # $ARGV[0]
# $args->get(1); # $ARGV[1]
# $args->action; # $ARGV[1]
# $args->exists(0); # exists $ARGV[0]
# $args->exists('flag'); # exists $ARGV[0]
# $args->get('flag'); # $ARGV[0]

Venus::Box - The Box class is a proxy class provides a mechanism for autoboxing the return values of its proxied objects.

package main;

use Venus::Box;

my $box = Venus::Box->new(
  value => {},
);

# bless({'value' => bless({'value' => {}}, 'Venus::Hash')}, 'Venus::Box')

$box->keys->count->unbox;

# 0

Venus::Data - The Data class provides methods for accessing and manipulating POD data (and data sections) in the file or the __DATA__ token.

package main;

use Venus::Data;

my $data = Venus::Data->new;

# bless({...}, 'Venus::Data')

$data->value($data->space->format('label', 't/%s.t'));

# /path/to/t/Venus_Data.t

$data->find(undef, 'name');

# {
#   'data' => ['Venus::Data'],
#   'index' => 1,
#   'list' => undef,
#   'name' => 'name'
# }

Venus::Date - The Date class provides methods for manipulating date and time values.

package main;

use Venus::Date;

my $date = Venus::Date->new(570672000);

# bless({...}, 'Venus::Date')

$date->string;

# '1988-02-01T00:00:00Z'

Venus::Error - The Error class provides methods for throwing exceptions.

package main;

use Venus::Error;

my $error = Venus::Error->new;

# bless({...}, 'Venus::Error')

$error->throw;

# Exception!

Venus::Json - The Json class provides methods for encoding and decoding JSON data.

package main;

use Venus::Json;

my $json = Venus::Json->new(
  value => { name => ['Ready', 'Robot'], version => 0.12, stable => !!1, }
);

# bless({...}, 'Venus::Json')

$json->encode;

# {"name": ["Ready", "Robot"], "stable": true, "version": 0.12}

Venus::Match - The Match class provides an object-oriented switch mechanism (or dispatch table).

package main;

use Venus::Match;

my $match = Venus::Match->new(5);

# bless({...}, 'Venus::Match')

$match->when(sub{$_ < 5})->then(sub{"< 5"});
$match->when(sub{$_ > 5})->then(sub{"> 5"});
$match->none(sub{"?"});

my $result = $match->result;

# "?"

Venus::Name - The Name class provides methods for parsing and formatting package namespace strings.

package main;

use Venus::Name;

my $name = Venus::Name->new('Foo/Bar');

# bless({'value' => 'Foo/Bar'}, 'Venus::Name')

$name->package;

# "Foo::Bar"

Venus::Opts - The Opts class provides methods for accessing and manipulating @ARGS values that are passed as command-line options.

package main;

use Venus::Opts;

my $opts = Venus::Opts->new(
  value => ['--resource', 'users', '--help'],
  specs => ['resource|r=s', 'help|h'],
  named => { method => 'resource' } # optional
);

# bless({...}, 'Venus::Opts')

$opts->method;

# "users"

# $opts->method; # $resource
# $opts->get('resource'); # $resource

# $opts->help; # $help
# $opts->get('help'); # $help

Venus::Path - The Path class provides methods for operating on paths.

package main;

use Venus::Path;

my $path = Venus::Path->new('t/data/planets');

# bless({'value' => 't/data/planets'}, 'Venus::Path')

my $planets = $path->files;

# [
#   bless({...}, 'Venus::Path'),
#   bless({...}, 'Venus::Path'),
#   ...,
# ]

# my $mercury = $path->child('mercury');
# my $content = $mercury->read;

Venus::Process - The Process class provides methods for forking and managing processes.

package main;

use Venus::Process;

my $parent = Venus::Process->new;

# bless({'value' => 2179356}, 'Venus::Process')

my $process = $parent->fork;

if ($process) {
  # do something in child process ...
  $process->exit;
}
else {
  # do something in parent process ...
  $parent->wait(-1);
}

# $parent->exit;

Venus::Random - The Random class provides an object-oriented interface for Perl's pseudo-random number generator (or PRNG) which produces a deterministic sequence of bits which approximate true randomness.

package main;

use Venus::Random;

my $random = Venus::Random->new(42);

# bless({'value' => 42}, 'Venus::Random')

my $bit = $random->bit;

# 1

my $number = $random->range(10, 50);

# 24

Venus::Replace - The Replace class provides methods for manipulating regular expression replacement data.

package main;

use Venus::Replace;

my $replace = Venus::Replace->new(
  string => 'hello world',
  regexp => '(world)',
  substr => 'universe',
);

# bless({...}, 'Venus::Replace')

$replace->captures;

# "world"

Venus::Search - The Search class provides methods for manipulating regexp search data.

package main;

use Venus::Search;

my $search = Venus::Search->new(
  string => 'hello world',
  regexp => '(hello)',
);

# bless({...}, 'Venus::Search')

$search->captures;

# "hello"

Venus::Space - The Space class provides methods for parsing and manipulating package namespaces.

package main;

use Venus::Space;

my $space = Venus::Space->new('foo/bar');

# bless({'value' => 'Foo::Bar'}, 'Venus::Space')

$space->package;

# Foo::Bar

Venus::Template - The Template class provides minimalist template rendering functionality.

package main;

use Venus::Template;

my $template = Venus::Template->new(
  'From: <{{ email }}>',
);

# bless({...}, 'Venus::Template')

$template->render;

# "From: <>"

Venus::Throw - The Throw class provides a mechanism for generating and raising errors (exception objects).

package main;

use Venus::Throw;

my $throw = Venus::Throw->new;

# bless({'parent' => 'Venus::Error'}, 'Venus::Throw')

$throw->error;

# Exception!

Venus::Try - The Try class provides an object-oriented interface for performing complex try/catch operations.

package main;

use Venus::Try;

my $try = Venus::Try->new;

# bless({...}, 'Venus::Try')

$try->call(sub {
  my (@args) = @_;

  # try something

  return 2 * 3 * 4;
});

$try->catch('Example::Error', sub {
  my ($caught) = @_;

  # caught an error (exception)

  return;
});

$try->default(sub {
  my ($caught) = @_;

  # catch the uncaught

  return;
});

$try->finally(sub {
  my (@args) = @_;

  # always run after try/catch

  return;
});

my @args;

my $result = $try->result(@args);

# 24

Venus::Type - The Type class provides methods for casting native data types to objects and the reverse.

package main;

use Venus::Type;

my $type = Venus::Type->new([]);

# bless({'value' => []}, 'Venus::Type')

my $object = $type->deduce;

# bless({'value' => []}, 'Venus::Array')

my $code = $type->code;

# "ARRAY"

Venus::Vars - The Vars class provides methods for accessing %ENV items.

package main;

use Venus::Vars;

my $vars = Venus::Vars->new(
  value => { USER => 'awncorp', HOME => '/home/awncorp', },
  named => { iam => 'USER', root => 'HOME', },
);

# bless({....}, 'Venus::Vars')

$vars->iam;

# "awncorp"

# $vars->root; # $ENV{HOME}
# $vars->home; # $ENV{HOME}
# $vars->get('home'); # $ENV{HOME}
# $vars->get('HOME'); # $ENV{HOME}

# $vars->iam; # $ENV{USER}
# $vars->user; # $ENV{USER}
# $vars->get('user'); # $ENV{USER}
# $vars->get('USER'); # $ENV{USER}

Venus::Yaml - The Yaml class provides methods for encoding and decoding YAML data.

package main;

use Venus::Yaml;

my $yaml = Venus::Yaml->new(
  value => { name => ['Ready', 'Robot'], version => 0.12, stable => !!1, }
);

# bless({...}, 'Venus::Yaml')

$yaml->encode;

# "---\nname:\n- Ready\n- Robot\nstable: true\nversion: 0.12\n"

Why Try Venus

  • You're doing OO in Perl 5
  • You're interested in modern idiomatic Perl
  • You're looking for an architectural standard
  • You're partial to convention over configuration
  • You're in need of power and performance

Inspiration

  • https://github.com/ruby/ruby
  • https://github.com/crystal-lang/crystal
  • https://github.com/elixir-lang/elixir
  • https://github.com/python/cpython

Sources

Venus: OO Standard Library for Perl 5 - Github

To learn a new language, read its standard library - Hacker News

Is the standard library of any major programming language a liability? - ProgrammingLanguages

End Quote

"Most software today is very much like an Egyptian pyramid with millions of bricks piled on top of each other, with no structural integrity, but just done by brute force and thousands of slaves." - Alan Kay

About awncorp

user-pic