Comparison of Class::Tiny and Object::Simple

Yuki Kimoto recently posted about the latest release of Object::Simple, billed as "the simplest class builder". Since I've also written a "simple" OO framework called Class::Tiny, I thought I'd point out similarities and differences.

(I'm not going to address Object::Simple's origins from or differences from Mojo::Base.)

Similarities

Single file, minimal dependencies

Both Object::Simple and Class::Tiny are single-file OO frameworks with no no-core dependencies on recent perls. According to the "sloccount" tool, Object::Simple is 98 lines. Class::Tiny is 135.

Class::Tiny does require some dependencies on older Perls for deep @ISA introspection and global destruction detection.

Accessor generation with lazy defaults

Both frameworks allow you to specify accessors and provide either scalar or code-reference defaults for them. Defaults are evaluated on first use. The underlying generated code is extraordinarily similar and accessor speeds are generally comparable (at least with Class-Tiny-1.05 which has some optimizations to remove scopes).

Read-write accessors

Both offer read-write accessors, which I think is the only sensible choice when providing only a single style.

Differences

Mutator return style

Class::Tiny mutators return the value just set, which is consistent with the values returned by accessors. Object::Simple mutators return the invocant, which allows chaining.

BUILD/BUILDARGS/DEMOLISH

Class::Tiny supports the BUILD/BUILDARGS/DEMOLISH methods just like Moose and Moo do. Object::Simple does not.

Notably, Class::Tiny supports an interoperability convention that allows Moo or Moose classes to inherit from a Class::Tiny class without calling BUILD methods more than once.

Constructor speed

Because Class::Tiny does some extra validation, plus provides BUILD/BUILDARGS support, its constructor is about 3x slower than Object::Simple, which has a two-line constructor.

Extraneous methods in @ISA

Class::Tiny classes inherit from Class::Tiny::Object, which provides only new, BUILDALL and DESTROY methods. Object::Simple classes typically inherit from Object::Simple, which provides import, new, attr, class_attr and dual_attr methods.

Unknown constructor arguments

Class::Tiny ignores unknown attributes in constructor arguments (without error, just like Moose/Moo). Object::Simple will include them in the constructed object.

Subclassing

Class::Tiny relies on users to set inheritance with @ISA or base/super/parent pragmas. Object::Simple additionally offers an import flag "-base" which sets the superclass. If the superclass is not Object::Simple, the superclass is loaded.

Introspection

Class::Tiny provides a mechanism for getting a list of class attributes and default values for attributes. Object::Simple does not.

strict/warnings export

Object::Simple turns on strict and warnings in the caller when the "-base" flag is used. Class::Tiny does not.

Closing thoughts

Object::Simple is, indeed, simple. It's not much more than syntactic sugar for generating accessors with defaults.

That said, I think it's too simple. If you really need minimal overhead and maximum speed just bless a hash reference into a class and directly access the members. If you want minimalism and default values, you can get there with eager defaults like this:

sub new {
    my $class = shift;
    return bless { name => "Jane", data => {}, @_ }, $class;
}

Once you start subclassing, I think you'll want BUILD/DEMOLISH support to properly order construction and teardown and Object::Simple doesn't give it to you.

Even if you don't plan to subclass, might that be something your downstream users might want to do? Providing BUILD/DEMOLISH support makes it easy for downstream users to have well-structured construction and teardown.

Yes, you can create custom constructors, but that defeats the syntactic simplicity of Object::Simple. Plus, if you have custom constructors, you'll need custom destructors and a mechanism for ensuring they get called in order. Very soon, you'll have re-invented the semantics of BUILD/DEMOLISH. So why not start with a framework that already provides that for you?

I think Object::Simple fits a very narrow use-case: people who want lazy defaults, don't want to subclass and are willing to add a dependency to avoid some typing.

For general use, I still think Moo is the best all-around choice unless you know for sure that you need the introspection and meta-class hackery that Moose offers.

If Moo is too "heavy" for me for some project, I'll use Class::Tiny. If Class::Tiny is too "heavy" (?!?), then I'll just roll my own class and avoid the dependency entirely.

Admittedly, I'm biased, but I can't think of a situation where I'd actually use Object::Simple as it stands.

If Object::Simple added BUILD/DEMOLISH support, then it might be a decent alternative – a different flavor of simple class builder for those who like its particular API choices (e.g. mutator chaining). Until then, I think it's too niche to put in my toolbox.

Posted in perl programming | Tagged , , | Comments closed

Stand up and be counted: Annual MongoDB Developer Survey

If you use Perl and MongoDB, I need your help. Every year, we put out a survey to developers to find out what languages they use, what features they need, what problems they have, and so on.

We have very few Perl responses. ☹️

Be an ally! Take the MongoDB Developer Experience Survey.

Camel

Posted in perl programming | Tagged , , , | Comments closed

When RFCs attack: HTTP::Tiny is getting stricter

The problem with standards are that there are too many standards. When RFC-2616 – defining HTTP/1.1 – was updated, the IETF spread the details across six RFCs: RFC-7230 to RFC-7235. Most of the changes appear to be server side, but in some areas, particularly around header formatting, the rules are getting tighter.

I was lucky to have a hackathon day at the DC-Baltimore Perl Workshop, so I implemented some of the stricter rules as well as fixing a number of other bugs (and even adding a few features!) See the HTTP::Tiny Changes file for details.

I still need to review more of the RFCs to see if there are other changes that impact HTTP::Tiny.

Meanwhile, HTTP-Tiny-0.057-TRIAL.tar.gz is on CPAN. Please, please, please install it and use it in your day-to-day work to see if there are any problems.

Here is how you can install it with cpanm or cpan:

$ cpanm --dev HTTP::Tiny
$ cpan DAGOLDEN/HTTP-Tiny-0.057-TRIAL.tar.gz

If you find any problems, please open a ticket on the HTTP::Tiny Github issue tracker.

Thanks!

Posted in perl programming | Tagged , , | Comments closed

At-sign nick completion for Weechat and Slack

I like connecting to chat apps like Slack and Flowdock using an IRC client, as that keeps all my chats in one window, right in my terminal.

I've been frustrated that the IRC gateways for Slack and Flowdock don't highlight people based just on their nick the way a typical IRC client does. For example, my IRC nick is "xdg". In an IRC channel, just saying "xdg: ping" will highlight and beep my terminal. But if I want to highlight "bob" who uses the Slack app, I need to say "@bob: ping".

Usually, when I highlight someone, I can use tab completion to finish the nick. That's really handy when I'm talking to my friend "BinGOs". But if I want to @-highlight him and I type "@Bi<TAB>", completion fails.

Since I use Weechat as my IRC client, I whipped up a plugin called atcomplete, which adds an @-prefixed nick for every nick in the nicklist.

With that, I can do "Bi<TAB>" and get "BinGOs", or "<TAB>" again to get "@BinGOs". And if I do "@Bi<TAB>", I get "@BinGOs" right away.

I've submitted it to the Weechat scripts page and it might eventually get approved, but it's available on github now as weechat-atcomplete if people want to try it out. Feedback and patches welcome!

Posted in hacks | Tagged , , , | Comments closed

No more dirty reads with MongoDB

If you're reading this blog, it's a good bet that sometime in your life you've had a computer freeze or crash on you. You know that crashes happen.

If it's your laptop, you restart and hope for the best. When it's your database, things are a bit more complicated.

Historically, a database lived on a single machine. Writes are considered "committed" when they are written to a journal file and flushed to disk. Until then, they are "dirty". If the database crashes, only the committed changes are recovered from disk.

So far, so good. While originally MongoDB didn't journal by default, it has been the default since version 2.0 in 2011.

As you can imagine, the problem with having a database on a single machine is that when the system is down, you can't use the database.

Trading consistency for availability

While MongoDB can run as a single server, it was designed as a distributed database for redundancy. If one server fails, there are others that can continue.

But once you have more than server, you have a distributed system, which means you need to consider consistency between parts of the system. (See Eight Fallacies of Distributed Computing).

You might have heard of the CAP Theorem. While there are excellent critques of its practical applicability – e.g. Thinking More Clearly About Consistency – it is sufficient to convey two key ideas:

  • because networks are unreliable, partitions happen, so for any real system, partition tolerance ("P") is a given.
  • because MongoDB uses redundant servers that continue to operate during a failure or partition, it gives up global consistency ("C") for availability ("A")

When we say that it gives up consistency, what we mean is that there is no single, up-to-date copy of the data visible across the entire distributed system.

Rethinking commmitment

A MongoDB replica set is structured with a single "primary" server that receives writes from clients and streams them to "secondary" servers. If a primary server fails or is partitioned from other servers, a secondary is promoted to primary and takes over servicing writes.

But what does it mean for a write to be "committed" in this model?

Consider the case where a write is committed to disk on the primary, but the primary fails before the write has replicated to a secondary. When a new secondary takes over as primary, it never saw the write, so that write is lost. Even if the old primary rejoins the replica set (now as a secondary), it synchronizes with the current primary, discarding the old write.

This means that commitment to disk on a primary doesn't matter for whether writes survives a failure. In MongoDB, writes are only truly commmitted when they have been replicated to a majority of servers. We can call this "majority-committed" (hereafter abbreviated "m-committed").

Commitment, latency and dirty reads

When replica sets were introduced in version 1.6, MongoDB offered a configuration option called write concern (abbreviated "w"), which controlled how long the server would wait before acknowledging a write. The option specifies the number of servers that need to have the write before the primary would unblock the client – e.g. w=1 (the default), w=2, etc. You could also specify to wait for a majority without knowing the exact number of servers (w='majority').

use MongoDB;
my $mc = MongoDB->connect(
    $uri,
    {
        w => 'majority'
    }
);

This means that a writing process can control how much latency it experiences waiting for levels of commitment. Set w=1 and the primary acknowledges the write when it is locally committed while replication continues in the background. Set w='majority' and you'll wait until the data is m-committed and thus safe from rollback in a failover.

You may spot a subtle problem in this approach.

A write concern only blocks the writing process! Reading processes can read the write from the primary even before it is m-committed. These are "dirty reads" – reads that might be rolled back if a failure occurs. Certainly, this is a rare case (compared to, say, transaction rollbacks), but it is a dirty read nonetheless.

Avoiding dirty reads

MongoDB 3.2 introduced a new configuration option called read concern, to express the commitment level desired by readers rather than writers.

Read concern is expressed as one of two values:

  • 'local' (the default), meaning the server should return the latest, locally committed data
  • 'majority', meaning the server should return the latest m-committed data

Using read concern requires v1.2.0 or later of the MongoDB Perl driver:

use MongoDB v1.2.0;
my $mc = MongoDB->connect(
    $uri,
    {
        read_concern_level => 'majority'
    }
);

With read concern, reading processes can finally avoid dirty reads.

However, this introduces yet another subtle problem. Consider what could happen if a process writes with w=1 and then immediately reads with read concern 'majority'?

Configured this way, a process might not read its own writes!

Recommendations for tunable consistency

I encourage MongoDB users to place themselves (or at least, their application activities) into one of the following groups:

  • "I want low latency" – Dirty reads are OK as long as things are fast. Use w=1 and read concern 'local'. (These are the default settings.)
  • "I want consistency" – Dirty reads are not OK, even at the cost of latency or slightly out of date data. Use w='majority' and read concern 'majority.
use MongoDB v1.2.0;
my $mc = MongoDB->connect(
    $uri,
    {
        read_concern_level => 'majority',
        w => 'majority',
    }
);

I haven't discussed yet another configuration option: read preference. A read preference indicates whether to read from the primary or from a secondary. The problem with reading from secondaries is that, by definition, they lag the primary. Worse, they lag the primary by different amounts, so reading from different secondaries over time pretty much guarantees inconsistent views of your data.

My opinion is that – unless you are a distributed systems expert – you should leave the default read preference alone and read only from the primary.

Posted in mongodb | Tagged , , , | Comments closed

© 2009-2016 David Golden All Rights Reserved