5.8 KiB
FPKATA #01: In the library…
(Soundtrack: Arch Enemy — War Eternal)
(Inspired from this exercice: http://www.codewars.com/kata/54dc6f5a224c26032800005c/train/haskell)
REPL compatible copy/paste (TL;DR)
import Dict
type Stock = Stock { name : String, count : Int }
library1 = [ Stock { name = "ABART", count = 20 } , Stock { name = "CDXEF", count = 50 } , Stock { name = "BKWRK", count = 25 } , Stock { name = "BTSQZ", count = 89 } , Stock { name = "DRTYM", count = 60 } ]
categories1 = [ 'A', 'B', 'C', 'W' ]
toUplet : List Repl.Stock -> List ( String, Int )
toUplet = List.map (\x -> case x of Stock v -> (v.name,v.count))
onlyInitials : List (String, Int) -> List (Char, Int)
onlyInitials = List.map (\x -> case x of (s, i) -> ((Maybe.withDefault '\0' << List.head << String.toList) s,i) )
howMany : Char -> List Char Int -> Int
howMany c = (List.foldl (\t acc -> acc + Tuple.second t) 0) << List.filter (\t -> (Tuple.first t) == c)
stockList : List Stock -> List Char -> List ( Char, Int )
stockList sl = List.map (\c -> (c, sl |> toUplet >> onlyInitials >> howMany c))
Prologue
You are stuck in a library with a very talkative and experimental science-minded IA. It keeps repeating the following:
— Pleeease, can you help me?
— …
— Pleaaase, can you help?
— …
— Hellooooo?
— …
Since you would really like to have peace, you dare ask:
— What's the matter?
— My code is incomplete, I've lost my code, please help!
— What code? What are you talking about?
— I really need to sort the library index and count how many books have the same first-letter reference, but only in from a list…
— Wait!
— … of characters that my master will provide.
This makes no sense, can't you write it down?
A few seconds pass in total silence. Then, there's a sound coming from your right, something like an inkjet printer work at a fast pace. It's actually a printer. It throws the printed sheet of paper at you in a furious “TWEEEEEEEEEEEP”. You pick up the paper and read:
INPUT................:
LIBRARY COUNT......: [("ABOOK", 12), ("BPDEXU", 45), ("BJLDRSA", 15), ("CLDAVB", 98), ("DAJLDI", 32)]
INDICES............: ['A','B','C','W']
OUTPUT...............:
INDEX COUNT........: [('A',12),('B',60),('C',98),('W',0)]
TIME TO GOOD ANSWER..: 203 years
You frown:
— It took you so long to find this?
— Can you help me? I've lost my code, my dedicated code for this task.
— Well, you managed to get this one right, didn't you?
— Nooooo! Master is going to unplug me if I use bruteforce again! Master was very unhappy with my performance! I want to live! Please help me!
— Ok, all right, calmn down, there, there… What are programmed with?
— Elm.
Of course it's Elm…
Outline
module Main exposing (..)
import Html exposing (text)
type Stock
= Stock String Int
library1 : List Stock
library1 =
[ Stock "ABART" 20
, Stock "CDXEF" 50
, Stock "BKWRK" 25
, Stock "BTSQZ" 89
, Stock "DRTYM" 60
]
categories1 : List Char
categories1 =
[ 'A'
, 'B'
, 'C'
, 'W'
]
stockList : List Stock -> List Char -> List ( Char, Int )
stockList st cs =
<THIS IS BLANK, FILL ME>
main =
text (toString (stockList library1 categories1))
Walkthrough
All is needed is to fill the gaps from library1
, categories1
to stockList
.
So, what is needed for successfully building a stockList? Let's look at the types:
We have a List Stock
but we don't have any function to work on it. We could
make some functions for this, but we'll just be lazy and turn a List Stock into
a much lower level List (String, Int)
:
toUplet : List Stock -> List ( String, Int )
toUplet = List.map (\x -> case x of Stock v -> (v.name,v.count))
We'll need to count stock by initials, so we write a function for that:
onlyInitials : List (String, Int) -> List (Char, Int)
onlyInitials = List.map (\x -> case x of (s, i) -> ((Maybe.withDefault '\0' << List.head << String.toList) s,i) )
So, for every tuple, we operate of the left part. There is some strange
machinery for turning a String -> Char
involving a Maybe value, because
List.head
may fail. Of course in our case, no book should ever have an empty
title, but Elm doesn't know that. We mitigate this by setting a default null
char if the title is empty.
Good! We can already chain the two preceding functions since the output type
of toUplet
matches the input type of onlyInitials
. Ain't that a coincidence?
> library1 |> toUplet >> onlyInitials
[('A',20),('C',50),('B',25),('B',89),('D',60)] : List ( Char, Int )
Oh, but oh. There are two Bs in there! Those should go together and their counts added together, and for that, nothing is better than a fold. Let's write a function that answers the question: “How many X is there?” where X is a Char.
howMany : Char -> List Char Int -> Int
howMany c = List.filter (\t -> (Tuple.first t) == c)
>> (List.foldl (\t acc -> acc + Tuple.second t) 0)
howMany
is made of two functions: the first is one that only keeps tuples
of interest (they match our Char c) and then we blindly sum the count of the
resulting list. We always start the count from 0, so that if we don't find
anything, it's still correct.
Let's try to find how may books that have a title starting with B in the library:
> library1 |> toUplet >> onlyInitials >> howMany 'B'
114 : Int
Nice! Well, we're almost done actually. We just need to do that for every letter
we were requested in categories1
. Let's use a map for that:
stockList sl = List.map (\c -> (c, sl |> toUplet >> onlyInitials >> howMany c) )
And voilà! Let's run this:
> stockList library1 categories1
[('A',20),('B',114),('C',50),('W',0)] : List ( Char, Int )
Mission accomplished.