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.