This post first appeared on my blog.
I've been a long time PHP / JavaScript developer by 2013 when I first started toying with Go. It's been almost 5 years by now and I've been an advocate of the language ever since. I was one of the many programmers who tried it out for the concurency primitives but stayed for it's simplicity. It was with Go that I realised how much nicer life is without inheritance and the rest of the OOP shenenigans. On the other hand Go also gave me a push to experiment with even more languages. It turned out to be my gateway drug to learning programing languages.
Of course, I played with some languages like Java, Python or Ruby before but honestly they didn't provide me anything that would be that much more than I had before. I was not interested in writing games, desktop or mobile apps at the time, I was happy as webdeveloper. To me Java felt too clumsy, Ruby felt too much magic and Python felt lacking the momentum at the time. (Not to mention the 2.x vs 3.x confusion.) Learning either of these languages felt too much work for the potential gain. With Go it was different, practically a love at first sight. It was so easy to learn and promised (and fulfilled) so much that I couldn't even stop learning more. Once I mastered it though, I realized that my old OOP languages feel verbose and my dynamically typed languages feel fragile. I was however feeling that Python was nicer to read than Go and this made me looking at further languages, mostly new ones like Rust and Elixir.
There are a few ways to try out new languages and I usually prefer doing a small project in them even if I never actually finish those projects. This past two weeks however I went back to an old favorite: exercism.io. What's really nice about this service is that you can check how others solved a particular problem and with a bit of extra effort it's also possible to find or construct a "perfect" solution.
To showcase what I mean I'll add a few solutions for a simple problem called the Hamming problem throughout the exercism.io. The problem is simply finding the "distance" between two strings meaning the number of characters being different.
PHP
Here's my PHP solution from 2015:
<?php
function distance($a, $b)
{
if (strlen($a) != strlen($b)) {
throw new \InvalidArgumentException('DNA strands must be of equal length.');
}
$aSplit = str_split($a);
$bSplit = str_split($b);
$diff = array_diff_assoc($aSplit, $bSplit);
return count($diff);
}
It's actually quite simple, there's just one if for checking the string lengths. On the other hand there are a few strange things about PHP already showing:
- Notice how str_split has a '_' character to separate the parts but strlen is written as one word? Typical PHP...
- Notice that array_diff_assoc is a rather specific function. It shows me that there is a very large standard library to learn.
- Throwing an exception is okay. If you're a consumer of this function, you better catch the exception being thrown here.
- The lack of types mean you can always call this function with whatever values you want from
null
to any type ofobject
s, up to you. (Yes, this can be optionally fixed in PHP 7.0+, kind of.)
Note 1.
Hopefully no one would try to get everything done in one line, because it can seriously hurt anyone looking at it:
<?php
function distance($a, $b)
{
if (strlen($a) != strlen($b)) {
throw new \InvalidArgumentException('DNA strands must be of equal length.');
}
return count(array_diff_assoc(str_split($a), str_split($b)));
}
Note 2.
One could also write the following using PHP 7 making some of the issues go away. Typing is still optional though therefore there are still no general guarantees. Given that it's a scripting language, braking changes aren't necessary obvious before you deploy them unless your test suite has you covered.
<?php
/**
* @throws \InvalidArgumentException
*/
function distance(string $a, string $b): string
{
if (strlen($a) != strlen($b)) {
throw new \InvalidArgumentException('DNA strands must be of equal length.');
}
$aSplit = str_split($a);
$bSplit = str_split($b);
$diff = array_diff_assoc($aSplit, $bSplit);
return count($diff);
}
Go
My Go solution from 2017:
package hamming
import "errors"
const testVersion = 6
func Distance(a, b string) (int, error) {
if len(a) != len(b) {
return -1, errors.New("Lengths of the provided strings are not equal.")
}
aBytes := []byte(a)
bBytes := []byte(b)
dist := 0
for i, ch := range aBytes {
ch2 := bBytes[i]
if ch != ch2 {
dist += 1
}
}
return dist, nil
}
OK, what's the takeaway here?
- This is considerably longer and more complex than the PHP solution.
- On the other hand it requires much less prior knowledge from the user to understand, we don't even use the standard library. (To be fair it could also be avoided in PHP)
- There's no way this solution could be written on one line without breaking readability completely.
- Notice how the strong typing makes it impossible to call this method with weird data and how predictable the returned data is, except that error can also be a
nil
. - Notice how errors handled by just forcebly returning an error type, "never" throwing something unexpected. (In reality there is a panic which can be raised, but it's usage is different from your typical exceptions)
- Make sure you understand that the error can be
nil
. Warning: There will be dragons! - It might be strange that the function starts with a capital letter whereas the other identifiers start with a smaller case. This makes the code public / exported in Go.
Note: I tried to find a more functional or considerably simpler solution but I didn't find any.
UPDATE: Benjamin Cable pointed out a few issues with the above code and suggested a better alternative. For issues described check out the comment section, the code is as follows:
func distance(a, b []byte) (dist int, err error) {
if len(a) != len(b) {
return dist, errors.New("length of provided strings is not equal")
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
dist++
}
}
return dist, nil
}
Python
My Python solution again from 2015:
"""Python distance exercise solution"""
def distance(text_one, text_two):
"""Calculate distance between two strings"""
count = 0
for (char1, char2) in zip(text_one, text_two):
if char1 != char2:
count += 1
return count
Takeaway:
- The cyclomatic complexity is a bit larger here than with PHP, even without the error checking, but I do find it easier to read.
- Python's support for tuples and the
zip
function makes it kind of functional-like, although thefor
loop isn't all that usual in FP. - There's nothing preventing us from calling this function with strange data.
- There's little we can expect from this function to be returned. If we change the return type to a string, the callers won't know until they get a runtime error.
- We are using the standard library here but
zip
feels much less esoteric thanarray_diff_assoc
was for PHP.
Note: My Python is roughly on advanced beginner level, Christoph Schindler appears to be more experienced, his solution reads even more functional-like:
def distance(sequence, other):
if len(sequence) != len(other):
raise ValueError('Sequences must be of same length.')
return sum(1 for a, b in zip(sequence, other) if a!=b)
Additional takeaway:
- By raising a value error we can cause even more havoc on the caller's side.
F-sharp
and finally here's the same thing I wrote in F# this week having about 2 days worth of experience:
module Hamming
let distance (strand1: string) (strand2: string): int option =
match strand1.Length, strand2.Length with
| x, y when x = y ->
let seq1, seq2 = (List.ofSeq strand1), (List.ofSeq strand2)
Some(List.fold2 (fun acc a b -> if a = b then acc; else acc + 1) 0 seq1 seq2)
| _, _ -> None
What can we take away here?
- It is slightly longer than the Python solution.
- You may find the
match
syntax strange but you can think of it as anif
on steroid for now. - There's no way you could call this function with wrong types.
- The response is even more obvious than Go's is as the
null
part is not a surprise here, it's expressed by theoption
type annotation. - We are also using the standard library here but, much like
zip
,List.fold2
andList.ofSeq
also feel much less esoteric thanarray_diff_assoc
in the PHP solution.
It is not obvious from the above example but explicit type definitions are optional in F# unless the compiler tells you that you should provide them. Types however aren't optional like they are in PHP and partially in Python and since it's a compiled language there's much less space for mistakes too. Not having to deal with types in the first place can make coding feek much more like a scripting language when compared to C# for example. I only wish the compiler was faster, much faster if possible. (Go's compiler feels about 100x faster.)
UPDATE: As I mentioned above I just started to get deeper into FP languages and F#, here's a better solution from the comments:
let distance s1 s2 =
if String.length s1 <> String.length s2 then None
else
Seq.zip s1 s2
|> Seq.sumBy (fun (c1, c2) -> if c1 <> c2 then 1 else 0)
|> Some
It's hard to argue that this is a very elegant solution.
Conclusions
I guess it's no surprise that I find the F# solution the best here, but of course I do not mean to say that F# is the best language out of the four. I don't even think it's fair to say such a thing in general. I also don't think that this example proves in any way that FP is better than OOP. Obviously these examples provide no good insight into how hard these languages are to learn, how usable they are in the wild and many other important aspect of the languages. They do prove however, at least to me, that it's worth learning other languages as not all languages are equal.
Also exercism.io is an awesome dealer.
Top comments (9)
The Go implementation has some issues which I'd like to point out, and propose fixes for.
Firstly, you use
len()
on strings.len
returns the byte size rather than the character (or rune) length.If that's your intent in the first place, then your function should accept
a, b []byte
directly and not bother with strings at all.This removes the need for the type conversion inside the function.
Secondly, you're using a range over the byte slice for a, when a simple for loop would be simpler to read and write, given you've already asserted that
a
andb
are of the same size.Thirdly, named return parameters would enable you to write slightly more concise and obvious code.
Please also note that for local slices (that is to say, slices contained within the scope of a call-tree, and not defined globally), their length is cached, and calling
len()
therefore does not incur any overhead.With that in mind, here is my proposed amended solution:
Thanks for the feedback, I updated the post with your solution.
You are welcome, and thanks a lot for including my answer!
Here's an improved version from the F# community:
We can do even better :)
1) Strings implement IEnumerable, so they can be treated as a sequence of characters
2) Zip functions like Python's exist in F#'s Seq module as well
3) 'Count how many elements of a sequence fulfill this condition' can be done in easier ways than with an explicit folding. Seq.length, Seq.countBy, Seq.sumBy are all functions that can do the job.
Put it together, and you can do it like this:
I'll update the post with this, thanks a lot! Would you sharing your thoughts on some more code here? dev.to/peteraba/fp-public-review-1...
In Erlang:
Both the Go and first Python solutions are purely imperative. Java, C++ ... solutions would be almost identical. No need to inherit anything, no need to blame anything. Use OOP or functional whenever is needed, don't use a hammer for everything. johndcook.com/blog/2009/03/23/func...
I'm not sure what your point is, but I agree I guess.