Skip to content

Instantly share code, notes, and snippets.

@simonhf
Created May 30, 2020 01:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simonhf/078f0874c622ef9b276fa58554fcd1fc to your computer and use it in GitHub Desktop.
Save simonhf/078f0874c622ef9b276fa58554fcd1fc to your computer and use it in GitHub Desktop.
Intro to Crystal Lang for Perl developers

Intro to Crystal Lang for Perl developers

Background

I've used Perl for years for all the quick and dirty programs where it's much faster to develop in Perl than e.g. C/C++ or another language like Java which is very verbose and you end up writing tons of source code.

And although I do write Perl scripts, I mostly use so-called Perl one-liners in order to extend the command line and act as glue for other command line run scripts and programs, e.g.:

$ perl -e 'foreach(1..3){printf qq[$_\n];}' | \
  perl -lane '
  printf qq[>%s\n], $_; $l++; $t+= $_;
  sub BEGIN{ printf qq[-start\n]; }
  sub END{ printf qq[-end // l=$l t=$t\n]; }'
-start
>1
>2
>3
-end // l=3 t=6

And the other reason why I like Perl one-liners is because if you get into the habit of just writing them on-the-fly, then it's lower friction than having to think up a file name, and think up where to store that file. And it's easier to copy and paste the one-liner into notes or a wiki page, or post it to a colleague via slack, etc.

My one-liner language wish-list

I've been looking for a more elegant / faster alternative language to aging Perl for writing command line one-liners for many years and have a list of language features that I don't want to do without:

  • Execute function before program starts, i.e. sub BEGIN {...}.
  • Execute function before program terminates, i.e. sub END {...}.
  • Handle implicit STDIN loop, e.g. cat foo.txt | perl -lane 'printf qq[>%s\n], $_;'.
  • Handle alternative quoting, e.g. qq[...] means "..." and q[...] means '...'.
  • Handle nested hash table with concise syntax, e.g. $h{foo}{bar}{baz}.
  • Handle constant folding, e.g. if(0){...} is compiled away with no run-time overhead.
  • Handle backticks, e.g. @lines = `...` .
  • Handle regular expression with concise syntax, e.g. if(m~(...)(...)~){($match1, $m2)=($1,$2); ...}.
  • Handle if suffix, e.g. next if(...);.
  • Handle switching off line output buffering, i.e. $|++.
  • Ideally one-liner should be same or shorter as Perl one-liner length.
  • Must be at least faster than Perl at run-time.

With this minimal list of Perl one-liner features, I've been very productive over the years with my Perl one-liners, but failed miserably to find any suitable replacement... up until now when Crystal Lang came onto my radar...

Crystal Lang equivalents to Perl

Crystal: Execute function before program starts

Perl seems to have this feature because of its implicit STDIN loop command line option. If you specify on the command line to have the implicit loop, then how would you execute any program before the implicit loop?

Crystal has no implicit loop but never the less has an extremely compact loop anyway:

Perl with implicit loop:

$ perl -e 'foreach(1..3){printf qq[$_\n];}' | perl -lane 'printf qq[>%s\n], $_;'
>1
>2
>3

Perl with explicit loop:

$ perl -e 'foreach(1..3){printf qq[$_\n];}' | perl -e 'while(<STDIN>){ printf qq[>%s], $_; }'
>1
>2
>3

Crystal only has explicit loop: Note: .each_line is a method of the STDIN object. Note: {|t|...} is an anonymous function called by .each_line and |t| is the local function variable name.

$ perl -e 'foreach(1..3){printf qq[$_\n];}' | crystal eval 'STDIN.each_line {|t| printf %[>%s\n], t; }'
>1
>2
>3

Or alternatively Crystal can interpolate the variable l into the string:

$ perl -e 'foreach(1..3){printf qq[$_\n];}' | crystal eval 'STDIN.each_line {|t| puts %[>#{t}]; }'
>1
>2
>3

Crystal: Execute function before program terminates

Perl:

$ perl -e 'sub END{ printf qq[bar\n]; } printf qq[foo\n];'
foo
bar

Crystal: Note: The semi colon needed after the closing curly braces of at_exit{...}.

$ crystal eval 'at_exit{ printf %[bar\n]; }; printf %[foo\n];'
foo
bar

Crystal: Handle implicit STDIN loop

See answer to 'Crystal: Execute function before program starts' above.

Crystal: Handle alternative quoting

Perl:

$ perl -e 'printf qq[%s %s %s\n], "foo", qq(bar), q<baz>;'
foo bar baz

Crystal:

$ crystal eval 'printf %[%s %s %s\n], "foo", %Q(bar), %q<baz>;'
foo bar baz

Crystal: Handle nested hash table with concise syntax

Things definitely are a little more forgiving in Perl than in Crystal when it comes to concise syntax for handling nested hash tables.

Consider this Crystal nested hash example: Note: In Perl the code would look something like $h{two}{foo} to access the nested hash table. Note: However, in Crystal the code is h["two"].as(Hash)["foo"].

$ crystal eval 'h=Hash{"one" => 1, "two" => Hash{"foo" => 3}}; printf %[debug: >#{h}< >%s< >%s< >%s<\n], h["one"], h["two"], h["two"].as(Hash)["foo"];'
debug: >{"one" => 1, "two" => {"foo" => 3}}< >1< >{"foo" => 3}< >3<

The .as(Hash) is only necessary because the first level of the hash table has two different types of values, numeric "one" => 1 and hash table "two" => Hash{"foo" => 3}.

With Crystal, if we keep the value type of the hash table value consistent, then the .as(Hash) is no longer necessary to differentiate between the value type:

$ crystal eval 'h=Hash{"one" => Hash{"baz" => 3}, "two" => Hash{"foo" => 3}}; printf %[debug: >#{h}< >%s< >%s< >%s<\n], h["one"], h["two"], h["two"]["foo"];'
debug: >{"one" => {"baz" => 3}, "two" => {"foo" => 3}}< >{"baz" => 3}< >{"foo" => 3}< >3<

But there's more: In Perl you might be tempted to have n keys in your hash table but also have two types of values per key, e.g. $h{$key}{sub_key_1} and $h{$key}{sub_key_2}. In this case, we're using a hash key for the 2nd level of the nested hash to emulate a kind of C struct with two struct members. It works but it's not good for performance in Perl because we end up doing one hash table for each hash table level.

Crystal offers something called Named Tuples to avoid the second level hash table lookup in this scenario described:

$ crystal eval 'h=Hash{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"][:foo]; p! h;'
debug: >{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}}< >{foo: 3}< >3<
h # => {"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}}

But, again, if you mix hash table value types, then you'll have to explicitly specify the type you're after, i.e. .as(NamedTuple): Note: You can also 'dump' a variable with p! h;.

$ crystal eval 'h=Hash{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"].as(NamedTuple)[:foo]; p! h;'
debug: >{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}< >{foo: 3}< >3<
h # => {"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}

And finally, if you want to create a 'real' Crystal record for the next level of your hash table then that's possible too, but the record must be declared causing a longer one-liner:

$ crystal eval 'record A, foo : Int32; h=Hash{"two" => A.new(foo: 3), "three" => A.new(foo: 4)}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"].foo; p! h;'
debug: >{"two" => A(@foo=3), "three" => A(@foo=4)}< >A(@foo=3)< >3<
h # => {"two" => A(@foo=3), "three" => A(@foo=4)}

So in summary, Crystal does offer nested hash tables and with more options and with concise syntax if you pay attention to consistent hash table value types.

Crystal: Handle constant folding

Perl:

$ perl -e 'if(0){ printf qq[foo ]; } printf qq[bar\n];'
bar

Crystal using its constant folding:

$ crystal eval 'if false; printf %[foo ]; end; printf %[bar\n];'
bar

Crystal using its macro mechanism:

$ crystal eval '{% if flag?(:mydebug) %} printf %[foo ]; {% end %}; printf %[bar\n];'
bar

$ crystal eval --define=mydebug '{% if flag?(:mydebug) %} printf %[foo ]; {% end %}; printf %[bar\n];'
foo bar

Crystal: Handle backticks

Perl: Note: Perl fails to chomp the backticks itself, and needs a scalar variable to chomp.

$ perl -e 'printf qq[>%s<\n], `echo hello`'
>hello
<

$ perl -e 'printf qq[>%s<\n], chomp(`echo hello`)'
Can't modify quoted execution (``, qx) in chomp at -e line 1, near "`echo hello`)
Execution of -e aborted due to compilation errors.

$ perl -e '$t=`echo hello`; chomp $t; printf qq[>%s<\n], $t'
>hello<

Crystal: Note: Crystal is more flexible than Perl regarding chomp.

$ crystal eval 'printf %[>%s<\n], `echo hello`'
>hello
<

$ crystal eval 'printf %[>%s<\n], `echo hello`.chomp'
>hello<

Crystal: Handle regular expression with concise syntax

Perl:

$ perl -e '$x = "abcdefg"; $x =~ m/((.*)(c.e).*)/; (undef,$a,$b) = ($1,$2,$3); printf qq[%s %s\n], $a, $b;'
ab cde

Crystal:

$ crystal eval 'x = "abcdefg"; x =~ /(.*)(c.e).*/; _,a,b = $~; printf %[%s %s\n], a, b;'
ab cde

Also with Crystal you can kind of name your captures although the syntax might be a tick longer:

$ crystal eval 'x = "abcdefg"; x =~ /(?<a>.*)(?<b>c.e).*/; printf %[%s %s\n], $~["a"], $~["b"];'
ab cde

Crystal: Handle if suffix

Perl:

$ perl -e '$x = 1; printf qq[foo\n] if ($x);'
foo

$ perl -e '$x = 0; printf qq[foo\n] if ($x);'
$

Crystal:

$ crystal eval 'x = true; printf %[foo\n] if x;'
foo

$ crystal eval 'x = false; printf %[foo\n] if x;'
$

Crystal: Handle switching off line output buffering

Perl:

$ perl -e '$|++;'

Crystal:

$ crystal eval 'STDOUT.flush_on_newline=true;'

Crystal: Ideally one-liner should be same or shorter as Perl one-liner length

As can be seen from the above examples, Crystal is kind of similar to Perl in terms of one-liner length.

Crystal: Must be at least faster than Perl at run-time

This one is kind of tricky. Once compiled the Crystal binary is by all accounts a tick faster than golang, and nearly as fast as C. However, Crystal compilation is not the speediest:

$ rm -rf ~/.cache/crystal/

$ time crystal eval 'printf %[foo\n];'
foo
real    0m1.187s

$ time crystal eval 'printf %[foo\n];'
foo
real    0m0.760s

$ rm -rf ~/.cache/crystal/

$ time crystal eval 'printf %[foo\n];'
foo
real    0m1.176s

$ time crystal eval 'printf %[foo\n];'
foo
real    0m0.760s

Although some amount of caching occurs, even a 'cached' build of printf %[foo\n]; takes 760ms... which his not quick. In comparison Perl manages to compile and run in only 3ms:

$ time perl -e 'printf qq[foo\n]'
foo
real    0m0.003s

The Crystal build is caching a lot of files even for the simple printf %[foo\n]; build:

$ find ~/.cache/crystal/eval/ -type f | wc -l
493

$ wc --bytes ~/.cache/crystal/eval/* | tail -1
4040225 total

However, the Crystal development road-map says they are working on an incremental build system which will hopefully speed this up in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment