fpkata-relex/ep0001/Solution.md
2018-01-27 16:43:16 +02:00

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.

Epilogue