Goal of Part Three

Process Hash (Key-Value Pair) with Haskell Function

We have seen an introduction of handling map with function in the previous lesson. Since combining map with function is tricky, and also contain many forms of syntatic sugar. This deserve this long explanation article.


Example of Doing Loop in Haskell With Map

This tutorial/ guidance/ article is one of three parts. These three combined is going to be a long article. So I won’t speak too much. More on codes, than just words.

The first two parts discuss the most common loop, array in part one and hash in part two. Considering that combining map with function is tricky, This deserve this an article of its own in part three. Part three also contains comparation with other Languages, using Real World Function.


Data Type Naming

Considering we might use a lot of (String, String) in our code. It is a good idea to use synonim, to avoid repetitive typing. This Pair type define tuples with two elements.

type Pair = (String, String)

pair :: Pair
pair = ("key", "value")

Passing Arguments to Action

Think Action in Haskell as Procedure, Sequence of Command

Now we can apply our Pair to new function. This function has two arguments.

  • First, a text argument, with String type.

  • Second, a tuples, with Pair type.

Since it is an IO action procedure, it must return IO (). This action looks like a void function, but it is actually not.

dumpPair :: String -> Pair -> IO ()
dumpPair text (key, value) = do
    putStrLn(text ++ ": " ++ key ++ " | " ++ value)

main = do
    dumpPair "Test" ("Key", "Value")

This will do display:

Test: Key | Value

Reintroduce Data Structure

Considering our material color again, just in case we forget, or too lazy to scroll to previous part.

colorSchemes :: [Pair]
colorSchemes =
    [("blue50",     "#e3f2fd")
    ,("blue100",    "#bbdefb")
    ,("blue200",    "#90caf9")
    ,("blue300",    "#64b5f6")
    ,("blue400",    "#42a5f5")
    ,("blue500",    "#2196f3")
    ,("blue600",    "#1e88e5")
    ,("blue700",    "#1976d2")
    ,("blue800",    "#1565c0")
    ,("blue900",    "#0d47a1")
    ]

Iterate with mapM_ using Curry Function

Now we can do iterate our latest function. Doing mapM_ inside a function.

This function has two arguments.

  • First, a text argument, with String type.

  • Second, a dictionary, with Pair type.

dumpHash1 :: String -> [Pair] -> IO ()
dumpHash1 text dictionary = do
    mapM_ (dumpPair text) dictionary
    
main = do
    dumpHash1 "Name" colorSchemes

Wait … !??*@…?? Doesn’t it defined earlier, that dumpPair has two arguments ?

The trick in passing argument rely in the closing bracket. (dumpPair text). It is called Curry Function. Based on mathematical Lambda Calculus. Since I’m a just another beginner, I suggest you to read about Haskell Curry Function somewhere else.

However, the result will echo as below:

Name: blue50 | #e3f2fd
Name: blue100 | #bbdefb
Name: blue200 | #90caf9
Name: blue300 | #64b5f6
Name: blue400 | #42a5f5
Name: blue500 | #2196f3
Name: blue600 | #1e88e5
Name: blue700 | #1976d2
Name: blue800 | #1565c0
Name: blue900 | #0d47a1

It works. And plain simple.

Using Lambda with mapM_

This is the trickiest part for beginner. But I must go on, because we will likely to see, a bunch of lambda everywhere, randomly marching, in any Haskell source code we meet. It is because lambda oftenly used as a wrapper of building block.

We can move above function dumpPair inside dumpHash2 function, using where clause. This way dumpPair won’t pollute global namespace.

dumpHash2 :: String -> [Pair] -> IO ()
dumpHash2 text dictionary = do
    mapM_ (dumpPair' text) dictionary
    where
        dumpPair' text (key, value) = do
            putStrLn(text ++ ": " ++ key ++ " | " ++ value)

And convert it to lambda later on. Merge both above function dumpPair and dumpHash1 into one dumpHash3.

dumpHash3 :: String -> [Pair] -> IO ()
dumpHash3 text dictionary = do
    mapM_ (\(key, value) -> do 
            putStrLn(text ++ ": " ++ key ++ " | " ++ value)
        ) dictionary   

It looks exactly like foreach loop, with different syntax. Once we get it, it is more flexible.

Eta Reduction

And Hey, there is always a place for improvement. How about Eta Reduction. Ough.. Yeah…

dumpHash3 :: String -> [Pair] -> IO ()
dumpHash3 text = do
    mapM_ (\(key, value) -> do 
            putStrLn(text ++ ": " ++ key ++ " | " ++ value)
        )   

Does it look literally cryptic, with operator marching, scattered all over the place ? Not really, the most cryptic part is the function declaration. This function declaration part is not mandatory. You can safely remove in this situation. Or just comment it out to disable.

Side Effects: Debugging

I actually use this function as a based model, to read key-value pairs from config. Sometimes strange thing happen in my application, and I need too see what happened in the process of applying config.

So what if I want some kind IO operation inside, such debug debug for example. Well, here it is, how to do it. No need to worry about side efect, we are already in IO action mode.

dumpHash4 :: String -> [Pair] -> IO ()
dumpHash4 text dictionary = do
    -- loop over a hash dictionary of tuples
    mapM_ (\(key, value) -> do 
            let message = text ++ ": " ++ key ++ " | " ++ value
            
            putStrLn message

            -- uncomment to debug in terminal
            -- putStrLn ("Debug [" ++ message ++ "]")
        ) dictionary  

Note that in real application, I replace the line putStrLn message with my own IO action.


View Source File:


Passing Arguments to Function

Think Function in Haskell as Math Equation

Now we can apply our Pair to new function. This function has two arguments, and one returning value.

  • First, a text argument, with String type.

  • Second, a tuples, with Pair type.

  • Return String Type.

pairMessage :: String -> Pair -> String
pairMessage text (key, value) = 
    text ++ ": " ++ key ++ " | " ++ value

main = do
    putStrLn $ pairMessage "Test" ("Key", "Value")
    putStrLn ""

This will show display:

Test: Key | Value

The difference betwwen pairMessage with the function dumpPair is, we place IO operation such putStr outside the function.


Iterate with map using Curry Function

Now we can do iterate our latest function. Doing map inside a function.

This function has two arguments, and one returning value.

  • First, a text argument, with String type.

  • Second, a dictionary, with List of Pair.

  • Return List of String.

hashMessage1 :: String -> [Pair] -> [String]
hashMessage1 text dictionary = 
    map (pairMessage text) dictionary 
    
main = do
    mapM_ putStrLn (hashMessage3 "Name" colorSchemes)
    putStrLn ""

You should not be surprised with the curry function pairMessage text. This wil produce as below:

Name: blue50 | #e3f2fd
Name: blue100 | #bbdefb
Name: blue200 | #90caf9
Name: blue300 | #64b5f6
Name: blue400 | #42a5f5
Name: blue500 | #2196f3
Name: blue600 | #1e88e5
Name: blue700 | #1976d2
Name: blue800 | #1565c0
Name: blue900 | #0d47a1

It also works. And plain simple.


Using Lambda with map

This article is getting more and more repetitious, But I must go on for an extra mile.

We can move above function pairMessage inside hashMessage2 function, using where clause.

hashMessage2 :: String -> [Pair] -> [String]
hashMessage2 text dictionary = 
    map (pairMessage' text) dictionary 
    where
        pairMessage' text (key, value) = 
            text ++ ": " ++ key ++ " | " ++ value

And convert it to lambda later on. Merge both above function pairMessage and hashMessage1 into one hashMessage3.

hashMessage3 :: String -> [Pair] -> [String]
hashMessage3 text dictionary = 
    map ( \(key, value) ->
          text ++ ": " ++ key ++ " | " ++ value    
        ) dictionary 

main = do
    mapM_ putStrLn (hashMessage3 "Name" colorSchemes)
    putStrLn ""

It looks exactly like foreach loop, with different syntax. Once we get it, it is more flexible.


Go Further with Action

Considering of simpliying the main clause. How about moving mapM_ putStrLn inside an action? We should go back using IO action procedure, because main clause only accept IO sequence.

First we need to capture the value of newly produced list. Do block, contain only sequence of IO. We assign a variable in do block by using let clause.

-- function
dumpHash5 :: String -> [Pair] -> IO ()
dumpHash5 text dictionary = do
    -- loop over a hash dictionary of tuples
    let messages = map ( \(key, value) ->
            text ++ ": " ++ key ++ " | " ++ value
            ) dictionary      
    mapM_ putStrLn messages

main = do
    dumpHash5 "Name" colorSchemes
    putStrLn ""

How about the output ? The same as previous sir.

We can go further using where as usual. No need to covert into lambda. We can have another form of this function.

dumpHash6 :: String -> [Pair] -> IO ()
dumpHash6 text dictionary = do
    -- loop over a hash dictionary of tuples    
    forM_ messages putStrLn
    where messages = map ( \(key, value) ->
            text ++ ": " ++ key ++ " | " ++ value
            ) dictionary 

Side Effects: Debugging

Again, how if I want multiple IO operation inside ? Such as debugging capability, after applying config. We can do it by changing from standard IO putStrLn to custom IO action debugStrLn inside where clause.

dumpHash7 :: String -> [Pair] -> IO ()
dumpHash7 text = do
    -- loop over a hash dictionary of tuples    
    forM_ messages debugStrLn
    where 
        debugStrLn message = do 
            putStrLn message

            -- uncomment to debug in terminal
            -- putStrLn ("Debug [" ++ message ++ "]")

        messages = map ( \(key, value) ->
            text ++ ": " ++ key ++ " | " ++ value
            ) dictionary 

You can try it yourself in your terminal.


View Source File:


Conclusion

Coding in Haskell is fun. I love it.

Happy Coding.