DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Al Newkirk
Al Newkirk

Posted on

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
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;
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

Venus::Mixin - The mixin builder module which uses the Mars architecture and 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
Enter fullscreen mode Exit fullscreen mode

Venus::Role - The role (or trait) 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"
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

package main;

use Venus::Code;

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

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

$code->call;

# 1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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

package main;

use Venus::Scalar;

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

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

${$scalar}

# ""
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Venus::Data - The Data class provides methods for accessing and manipulating POD data (and data sections) in the underlying file or __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'
# }
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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

package main;

use Venus::Error;

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

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

$error->throw;

# Exception!
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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;

# "?"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 approximates 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
Enter fullscreen mode Exit fullscreen mode

Venus::Replace - The Replace class provides methods for operating on 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"
Enter fullscreen mode Exit fullscreen mode

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

package main;

use Venus::Search;

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

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

$search->captures;

# "hello"
Enter fullscreen mode Exit fullscreen mode

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

package main;

use Venus::Space;

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

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

$space->package;

# Foo::Bar
Enter fullscreen mode Exit fullscreen mode

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

package main;

use Venus::Template;

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

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

$template->render({
  name => 'awncorp',
  email => 'awncorp@cpan.org',
});

# 'From: "awncorp"<awncorp@cpan.org>'
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Why Try Venus

  • You're doing modern 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

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

Top comments (4)

Collapse
 
bbrtj profile image
bbrtj

Obviously you put a lot of effort into creating this. Looks impressive.

My problem with such systems though: to fully leverage its power, you will have to no longer code in Perl, but in Venus instead. I mean it is no longer familiar to programmers who only know Perl, not Venus.

I'm also worried about performance. You say I should try Venus if I'm in need of performance, but I see pure perl code only in its distribution. How does it achieve better speed? Do you have sample benchmarks?

Collapse
 
iamalnewkirk profile image
Al Newkirk

Hey @bbrtj. Thanks for the feedback and the kudos. I don't mean to be overly reductionist, but in order to use anything that's not part of the core language you're going to have to learn its API. I take your points that when some systems introduce a DSL it can begin to feel like you're programming in another language. This isn't the case with Venus. Yes, there are conventions (i.e. ways of doing things) that you may not be used to, but it's all really just packages and method calls.

Regarding performance, Venus isn't competing with Perl itself so we can't expect it to be faster (or even as fast) as native OO, but as compared to other object systems (e.g. Moose, Mouse, Moo, ...) the Mars architecture (which Venus is based on) is the fastest. Here's a report from a benchmark testing cold-starts.

Collapse
 
bbkr profile image
Pawel Pabian

you will have to no longer code in Perl, but in Venus instead

That is inevitable when base language is limited. For large codebase you have two choices in Perl: adopt OO framework or go crazy.

And to be honest - committing to specific framework syntax is not that big of a deal. I maintain 150K lines of Perl+Mouse, from which 30% are Moose syntax (attributes, builders, phasers). Even non Perl developer have no problems understanding it because syntax at this level is self-explanatory.

Collapse
 
bbkr profile image
Pawel Pabian

without years of navel-gazing, committee legislation, infighting, or stalling

Perl was too limited for too long. Multiple workarounds for lack of proper OO became integral parts of every bigger project. My $dayjob code currently slurps 5 different OO frameworks at once through CPAN module dependencies, starts slowly and consumes way too much memory. This process is impossible to reverse by introducing standard library so late into the game. Even if some OO framework could become part of Perl core distribution it will never completely overcome huge ecosystem inertia.

I'm not trying to spoil the fun. Cold blooded observation is that more convenient OO-oriented languages are within reach of hand. Closest competitor Raku is light years ahead of Perl in terms of boilerplate size, syntax consistency and base language feature set. It does not need "standard library" patching.

Your project reminds me of manufacturers who still make performance parts for 1970 Dodge Charger. They help to revive classics despite the fact their products will never get wide adoption due to shrinking user base. Glory to them.

Dream Big


Use any Linode offering to create something unique or silly in the DEV x Linode Hackathon 2022 and win the Wacky Wildcard category.

β†’ Join the Hackathon <-