I kept doing learning Rust via exercism.org. and I learnt some basic usage of functions, but still don’t have enough concrete understandings.. However, I found something similar in syntax in rust to haskell or elm. let’s look at the problem first.
Introductions
Your task is to convert a number into a string that contains raindrop sounds corresponding to certain potential factors. A factor is a number that evenly divides into another number, leaving no remainder. The simplest way to test if a one number is a factor of another is to use the modulo operation.
The rules of raindrops are that if a given number:
has 3 as a factor, add 'Pling' to the result. has 5 as a factor, add 'Plang' to the result. has 7 as a factor, add 'Plong' to the result. does not have any of 3, 5, or 7 as a factor, the result should be the digits of the number.
Examples
28 has 7 as a factor, but not 3 or 5, so the result would be "Plong".
30 has both 3 and 5 as factors, but not 7, so the result would be "PlingPlang".
34 is not factored by 3, 5, or 7, so the result would be "34".
First Iteration
This is categorised as an easy problem. and I solve the problem by using .map()
and .join()
.
pub fn raindrops(n: u32) -> String {
let divisible_by = |k| -> bool { n % k == 0 };
let sound = [(3, "Pling"), (5, "Plang"), (7, "Plong")]
.map(|(k, sound)| if divisible_by(k as u32) { sound } else { "" })
.join("");
if sound.is_empty() {
.to_string()
n} else {
sound}
}
divisible_by
this is not a function but it is called closure. Rust allows us to declare a function inside another function but the local function is unabled to access local variables, which is uncomfortable for me because In haskell, we could make a local function which can access the variables in current scope:
raindrops :: Int -> String
=
raindrops n let divisible_by k = n `rem` k == 0
-- snip ..
So, I tried to use closure here. And it is great to see it is working as I wanted.
filter_map is more efficient
I could have used filter_map()
instead of map()
. so if I revise the iteration 1, it would be like:
// .. snip ..
let sound = [(3, "Pling"), (5, "Plang"), (7, "Plong")]
.iter()
.filter_map(|(k, sound)| {
if divisible_by(*k as u32) {
Some(sound.to_string())
} else {
None
}
})
.collect::<String>();
// .. snip ..
And filter_map in rust is actually efficient than filter().map()
combination.
Second Iteration
And I found some other’s iteration using fold
. In functional programming, this is kind of
folding problem. which sounds familiar to me, so I tried to figure out how to use it.
And this is raindrops
function by using fold:
// .. some structure and implementation ..
// snip ..
pub fn raindrops(n: u32) -> String {
let divisible_by = |k| -> bool { n % k == 0 };
// fold_helper will be used later
let fold_helper = |acc: Option<String>, fac_sound: &FacToSound| {
if divisible_by(fac_sound.get_factor()) {
let new_sound = fac_sound.get_sound();
acc.map(|s| s + new_sound)
.or_else(|| Some(String::from(new_sound)))
} else {
acc}
};
RAIN_DROP_SOUND_MAP.iter()
.fold(None, fold_helper)
.unwrap_or_else(|| n.to_string())
// I think unwrap_or_else is cool stuff
}
So fold in rust is basically foldl in haskell. The accumulator is the first argument as same as in haskell as well. I enjoyed the struggling to reach this solution. but I didn’t like the part shown below:
acc.map(|s| s + new_sound)
.or_else(|| Some(String::from(new_sound)))
because .or_else
will always check the result of acc.map()
is Some (Just in Haskell) or
None (Nothing in Haskell).
If I use the pattern matching, It runs either .map()
part or .or_else
part.
Even if it looks fancy or expressive, it can lead us to write less efficient code
and also make the codes for reviewer who is not familiar with extra syntax to understand.
leave it as basic
In Elm lanauge, Maybe-Extra package exisits where orElse
exists as well.
Just 5
|> orElse (Just 4)
--> Just 5
Nothing
|> orElse (Just 7)
--> Just 7
But that function comes from an extra package! Because elm language encourages the user use simpler syntax, which is pattern matching in this case. (this quite strong point of view, but I agree that it is not good idea to write many different version of function for an open source project which is involved by many people.)
case Just 5 of
Nothing ->
Just 4
x ->
x
It results in a bit longer codes than one with helper package, nevertheless the user still
understand the code easier with plain syntax background.
No need to look up the extra documentation! even though your IDE will teach you how to use it
thesedays. But reading a documentation is still stressful.
so finally I made another iteration.
Third Iteration
Note: I add more types and implentation for study purpose.
struct FacToSound(u32, &'static str);
impl FacToSound {
pub fn get_factor(&self) -> u32 {
self.0
}
pub fn get_sound(&self) -> &str {
self.1
}
}
const RAIN_DROP_SOUND_MAP: [FacToSound; 3] = [
3, "Pling"),
FacToSound(5, "Plang"),
FacToSound(7, "Plong"),
FacToSound(;
]
pub fn raindrops(n: u32) -> String {
let divisible_by = |k| -> bool { n % k == 0 };
let fold_helper = |acc: Option<String>, fac_sound: &FacToSound| {
if divisible_by(fac_sound.get_factor()) {
let new_sound = fac_sound.get_sound();
Some(match acc {
Some(sound_so_far) => sound_so_far + new_sound,
None => String::from(new_sound),
})
} else {
acc}
};
RAIN_DROP_SOUND_MAP.iter()
.fold(None, fold_helper)
.unwrap_or_else(|| n.to_string())
}
Haskell Version
I didn’t expect the rust code can be easily tranlated into haskell code. Actually it was!
{-# LANGUAGE OverloadedStrings #-}
module Raindrops (convert) where
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.Maybe as MB
convert :: Int -> Text
=
convert n . show $ n) . (foldr helper Nothing) $
MB.fromMaybe (T.pack 3, "Pling")
([ (5, "Plang")
, (7, "Plong")
, ( :: [ (Int, Text) ])
]
where
helper (k, sound) acc | n `rem` k == 0 =
case acc of
Nothing ->
Just sound
->
_ <>) <$> acc
(sound | otherwise = acc
Data.Text is used here, so code is less straightforward. and foldr
is used here instead of
fold
in Rust (which is actually foldl
in haskell). The order of catenating texts is
different from the one in fold. i.e:
// append new sound to acc
// in fold in rust
Some(sound_so_far) => sound_so_far + new_sound,
vs
-- preppend new sound to acc. in foldr in haskell
->
_ <>) <$> acc (sound
If you are not comfortable with <$>
operator, you can use fmap
. and I put a lamda variable
to how it really works. (Frankly speaking, I wish elm
had this kind of feature but it is not
supported on purose!!)
-- preppend new sound to acc. in foldr in haskell
->
_ fmap (\acc' -> sound <> acc') acc
Wrapping Up
- In Rust, it is possible to solve a problem in a functional programming way.
- The simpler syntax we write, The easier code we read!