I like the simplicity of the String Calculator kata. It's a typical example of the map-reduce algorithm, where the string has to be split by a delimiter, mapped into a list of integers and then reduced to their sum. I have often used it as an example to quickly evaluate engineering candidates, try new languages and tools. This was the first challenge I tried to solve in Haskell about a year ago. I found the code the other day, I wanted to see how I could improve upon it with everything I've learned so far.
This is what I found from a year before:
import Test.Hspec
import Data.List.Split (splitOn)
calculator :: String -> Integer
calculator x = sum $ coerceString x ","
coerceString :: String -> String -> [Integer]
coerceString x c = map (coerce) (splitOn c x)
where
coerce "" = 0
coerce a = read a :: Integer
main :: IO ()
main = hspec $ do
describe "String Calculator" $ do
it "returns 0 for empty string" $ do
calculator "" `shouldBe` 0
it "returns 1 for '1'" $ do
calculator "1" `shouldBe` 1
it "returns 3 for '1,2,3'" $ do
calculator "1,2,3" `shouldBe` 6
This works for simple cases, but what should I do when there is a non-numeric string in the input arguments?
it "returns 0 for '1,2,!'" $ do
calculator "1,2,!" `shouldBe` 0
The algorithm crashes as I suspected:
String Calculator
returns 0 for empty string
returns 1 for '1'
returns 3 for '1,2,3'
returns 0 for '1,2,!' FAILED [1]
Failures:
string_calculator_01.hs:22:
1) String Calculator returns 0 for '1,2,!'
uncaught exception: ErrorCall (Prelude.read: no parse)
Randomized with seed 1943023196
Finished in 0.0023 seconds
4 examples, 1 failure
I could easily wrap the entire logic and return zero when an exception occurs, however, Haskell can do better. Much better.
I can return a Maybe Int from the operation that parses the string to an integer: if the result is Nothing here, the overall result will be Nothing.
There is a string parser in the Text.Read module that does just that, it's called readMaybe.
Here is how it works:
% ghci GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/adomokos/.ghci λ> import Text.Read λ> (readMaybe "12") :: Maybe Int Just 12 λ> (readMaybe "!") :: Maybe Int Nothing λ>
I need to parse the list of strings, which is achieved by mapping over the values with readMaybe:
GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /Users/adomokos/.ghci
λ> import Text.Read (readMaybe)
λ> import Data.List.Split (splitOn)
λ> let xs = "1,2,3"
λ> splitOn (",") xs
["1","2","3"]
λ> map (\x -> readMaybe x :: Maybe Int) $ splitOn (",") xs
[Just 1,Just 2,Just 3]
I now have a list of Maybe Int values that can be reduced into a single Maybe Int value. Reducing an array of numbers would be super easy (foldl (+) 0 [1,2,3] or foldl1 (+) [1,2,3] or just simply sum [1,2,3]). It's obvious that I will need something similar.
Adding a number to a Maybe Int can be achieved with a Functor:
λ> fmap (+10) (Just 4) Just 14 λ> (+10) `fmap` (Just 4) Just 14 λ> (+10) <$> Just 4 Just 14
All three expressions mean the same thing. The first is using the fmap as a conventional function name and arguments style, the second one uses the infix version of fmap and the third one is using a symbol.
This works for adding a number to a Maybe Int, however, I need to use an Applicative Functor to calculate the sum of two Maybe Ints.
λ> (+) <$> Just 10 <*> Just 4 Just 14
Using Applicative Functors, folding the list of Maybe Ints happens like this:
λ> let xs = [Just 1, Just 2, Just 3] λ> foldl (\acc x -> (+) <$> x <*> acc) (Just 0) xs Just 6 λ> foldl1 (\acc x -> (+) <$> x <*> acc) xs Just 6
The solution now works, although a bit hard to read:
import Test.Hspec
import Text.Read (readMaybe)
import Data.List.Split (splitOn)
calculator :: String -> Maybe Int
calculator input =
foldr (\x acc -> (+) <$> x <*> acc) (Just 0) $
map (\x -> readMaybe x) $ splitOn "," input
main :: IO ()
main = hspec $ do
describe "String Calculator" $ do
it "returns Nothing for empty string" $ do
calculator "" `shouldBe` Nothing
it "returns Just 1 for '1'" $ do
calculator "1" `shouldBe` Just 1
it "returns Just 3 for '1,2,3'" $ do
calculator "1,2,3" `shouldBe` Just 6
it "returns Nothing for '1,2,3,!'" $ do
calculator "1,2,3,!" `shouldBe` Nothing
It's more meaningful after I've refactored it into chunks:
calculator :: String -> Maybe Int
calculator input =
let foldingFn acc x = (+) <$> acc <*> x
parsedInts = map (\x -> readMaybe x) . splitOn (",")
in foldr1 (foldingFn) (parsedInts input)
The parsedInts function can be further simplified:
calculator :: String -> Maybe Int
calculator input =
let foldingFn acc x = (+) <$> acc <*> x
parsedInts = map (readMaybe) . splitOn (",")
in foldr1 (foldingFn) (parsedInts input)
And finally, the sum of list of Maybe values can be calculated by using the sequence function like this:
calculator :: String -> Maybe Int
calculator input =
let parsedInts = map (readMaybe) . splitOn (",")
in fmap sum . sequence $ parsedInts input
I find this form to be the most readable, but I liked the journey of getting there through the Applicative Functors.