This is a The Weekly Challenge #239. An exercise. Practice.
There’s a fine line between Best Practices and Let’s Push This As Far As We Can.
To be honest, I’m still not sure if I went too far with this one, or not far enough.
This is an exercise that’s made for Perl. It’s string manipulation. It’s perfect for the Practical Extraction and Report Language. As I wrote it, though, I also noticed that it’s a great bit of functional coding — extract the data, transform it into something else, and report the results. Use a series of functions to morph the beginning data into the end data you want.
That meant that I had to do it in Elixir, also.
I thought I’d just write the Perl script and then transform it into Elixir, but then things took on a life of their own.
“Same String”: Perl Edition
You are given two arrays of strings.
Write a script to find out if the word created by concatenating the array elements is the same.
For example, is an array of “ab” and “c” the same as “a” and “bc” after their parts have been smashed together? Yes, both are “abc”.
You could program this straight through. Wouldn’t need a single subroutine in the code at all. For the purposes of testing this, though, I put everything into a main() that we could call with all three sets of arrays provided in the challenge. And I re-used my pretty_print_array() subroutine from the last challenge.
Here’s the code to run the first test, minus pretty_print_array() which you can find in last week’s writeup at the link in the last paragraph:
my @arr1 = ("ab", "c");
my @arr2 = ("a", "bc");
main(\@arr1, \@arr2);
sub main {
my @arr1 = @{ shift() };
my @arr2 = @{ shift() };
print 'Input: @arr1 = ' . ( pretty_print_array(\@arr1) ) . "\n";
print 'Input: @arr2 = ' . ( pretty_print_array(\@arr2) ) . "\n";
my $combined_string1 = '';
my $combined_string2 = '';
map{ $combined_string1 .= $_; } @arr1;
map{ $combined_string2 .= $_; } @arr2;
print "Output: ";
if ( $combined_string1 eq $combined_string2 ) {
print "true\n";
} else {
print "false\n";
}
}
Is it elegant and reusable and the kind of thing I’d put up on CPAN? Nah.
Does it solve the challenge? Yes.
However, I saw what was happening here:
- Print the array to the screen
- Combine the parts of the arrays into strings
- Compare those two strings I just created.
- Print out the results of that comparison
One bullet point basically feeds into the next. We’re taking a piece of data, transforming it in some way, and then sending it to the next step to transform it again and pass it along.
This is perfect for Elixir’s pipe operator.
More Functions, More Fun
First, though, let’s functionalist the &$%# out of this in Perl:
#!/usr/bin/perl
use warnings;
use strict;
my @arr1 = ("ab", "c");
my @arr2 = ("a", "bc");
main(\@arr1, \@arr2);
sub main {
my @arr1 = @{ shift() };
my @arr2 = @{ shift() };
show_inputs(\@arr1, \@arr2);
my $combined1 = combine_arrays(\@arr1);
my $combined2 = combine_arrays(\@arr2);
my $results = compare_strings($combined1, $combined2);
show_output( $results );
}
sub show_inputs {
my @arr1 = @{ shift() };
my @arr2 = @{ shift() };
print "Input: \@arr1 = " . ( pretty_print_array(\@arr1) ) . "\n";
print "Input: \@arr2 = " . ( pretty_print_array(\@arr2) ) . "\n";
return;
}
sub show_output {
my $final_results = shift();
print "Output: " . $final_results . "\n\n";
}
sub combine_arrays {
my @arr = @{ shift() };
my $combined_string = '';
map{ $combined_string .= $_; } @arr;
return $combined_string;
}
sub compare_strings {
my ($str1, $str2) = @_;
return 'true' if $str1 eq $str2;
return 'false';
# Or, in ternary fashion:
# return $str eq $str2 ? "true" : "false";
}
This will work whether there is one value in each array, or an insanely large number that fits in memory. It’s still only going to work with two arrays, but I’m not going to worry about that bit of craziness.
But each step from that bullet point list I wrote out above is now solved with a subroutine. One feeds into the next. Send data in, get data in, pass that new data onto the next one. Some of it is purely functional and some of it is just for displaying information to the screen.
Here’s how it breaks down:
- Print the array to the screen ( show_inputs() )
- Combine the parts of the arrays into strings ( combine_arrays() )
- Compare the strings ( compare_strings() )
- Print out the results of that comparison ( show_output() )
It’s also super easy to test this now. Every step can be tested independently.
This kind of programming of chaining functions together and letting one feed the next is the perfect program to write in a functional language. Let’s look at this in Elixir.
“Same String”: Elixir Edition
Take the same bullet points. Run a data point through them. See what happens.
I’ll create a module named SameString to put all the functions in. I’ll start with the brains behind the whole operation and then add the individual functions above it as we go.
defmodule SameString do
def run_everything(tuple_of_lists) do
tuple_of_lists
|> show_inputs()
|> combine_lists()
|> compare_strings()
|> show_output()
end
end
That might look a little familiar already. I did a little renaming, since Elixir uses lists instead of arrays, but it’s the same function (née subroutine) names.
If you’re not familiar with Elixir, I’ll try to explain how the pipe operator works: The expression or output of the function just before it is passed as the first argument to the function after it. I’ve constructed these functions to only require one parameter, so you never have to see anything besides the function names. The output of tuple_of_lists/1 is sent directly into show_inputs/1, then the output from that function is sent as the first argument to the next function, combine_lists/1, etc.
The pipe operator is super cool. The only other Elixir feature in the core language that comes close is pattern matching. We’ll be using that, too. Stick around.
To make this more concrete, here’s how we’ll be calling this function:
SameString.run_everything( {["ab", "c"], ["a", "bc"]} )
The data structure I’ve chosen is a tuple with two lists in it. You could just as easily make it a list of two lists, but I liked adding in an extra data structure for the exercise of it all.
The run_everything/1 function receives it as tuple_of_lists and then pipes it into show_inputs. As you might imagine, that function shows the inputs to the screen:
def show_inputs({list1, list2}) do
IO.puts "Input: List1 = "
IO.inspect list1
IO.puts "Input: List2 = "
IO.inspect list2
{list1, list2}
end
Three things to look out for here: I’m cheating a bit by returning the tuple of lists back out of this function so we can continue the pipe chain.
Second, in my attempt to be descriptive of the function by listing out the parameters the function takes, I’ve created a pattern-matching element. If I tried to pass in a single string or a number or even a map, this function would never run and the whole program would die then and there. Anything other than a tuple (indicated with curly braces) with two variables of some kind in it will fail to match and this function would never run.
Third, you need to use IO.inspect to print out an array, rather than IO.puts. It’s a long story, but trust me on that one.
From there, the tuple of lists gets passed to the next function:
def combine_lists({list1, list2}) do
string1 = reduce_string(list1)
string2 = reduce_string(list2)
{string1, string2}
end
Here’s where things shift a bit from Perl. Rather than mapping over the array and concatenating the pieces into a string, I want to run a reducer to boil the array down to one long string.
def reduce_string( list_of_strings ) do
Enum.reduce(list_of_strings, "", fn characters, acc ->
acc <> characters
end)
end
This takes each element of the array and concatenates it to an accumulator. Enum.reduce naturally returns that last generated value.
And then combine_lists/1 sends back out a tuple of the two new strings.
The pipe operator neatly sends that tuple over to compare_strings/1. If you’re not used to pattern matching, this is where things get interesting. There are two different functions named compare_strings/1 here:
def compare_strings({string1, string1}) do
true
end
def compare_strings({_, _}) do
false
end
Elixir is doing pattern matching. If the two values inside the tuple being passed into the function are the same, it returns true. Otherwise, it returns false.
In the second function, you replace the parameters with underscores to indicate that you’re not going to do anything with the values that are coming in here. Elixir is serious about this – it’ll give you a warning if you run the script with named variables you never use.
There’s one more way to shorten this up even more:
def compare_strings({string1, string1}), do: true
def compare_strings({_, _}), do: false
Slightly different syntax, but it’s well-suited for the one-liner structure for short functions.
That true or false value then gets passed into show_output/1, and I’ll do some one-liners and pattern matching here, too:
def show_output(true), do: IO.puts "Output: True"
def show_output(false), do: IO.puts "Output: False"
Part of the fun of Elixir is writing complete programs without ever touching an if statement. Use pattern matching and the rest will follow.
And done. That’s the end of the pipeline.
It’s a nifty bit of code that feels right, reads easily, and (again) could be easily tested.
To see the final script, check out my Github repo.
But, wait! Let’s program this in two more languages! Stay tuned….
Other Solutions
I’m not good at Perl Golf. It’s not something I’ve practiced or worked on, either, but it’s always entertaining to watch someone who is skilled at it. Check out W. Luis Mochán’s one-liner solution on his blog.
1 Comment
"Same String" Part Deux: Now In Javascript and Python - Various and Sundry · October 22, 2023 at 10:01 pm
[…] or you start nesting ternaries, but for a simple true/false test like this, it makes perfect sense. I didn’t need one in Elixir because I was using pattern matching. I used it as an option in Perl, but I should have committed […]
Comments are closed.