F# For Dummys - Day 14 Collections Map
Tags: fsharp
Published: 2024-05-25 08:25:32

Today we learn Map, an immutable collection that stores key-value pairs</br>

Map is immutable, it cannot be changed after created. Adding or removing elements return a new Map instead of modifying the existing one
#### Create Map
- Explicitly specifying elements
```f#
let notEmptyMap = Map [ (1, "a"); (2, "b") ]

printfn "%A" notEmptyMap // map [(1, a); (2, b)]
```
- Map.ofList
```f#
let map = 
    Map.ofList [
        (3, "three")
        (1, "one")
        (2, "two")
    ]

printfn "map: %A" map // map [(1, one); (2, two); (3, three)]
```
we can see the key is sorted from 3,1,2 to 1,2,3</br>
what if the key is dulpicate
```f#
let map = 
    Map.ofList [
        (3, "three")
        (1, "one")
        (2, "two") // first key 2
        (2, "overwrite") // second key 2
    ]

printfn "%A" map // map [(1, one); (2, overwrite); (3, three)]
```
the value of the same key 2 is the last one, it overwrite the first one
- Map.empty

create an empty map with int keys and string values
```f#
let emptyMap = Map.empty<int, string>
printfn "%A" emptyMap // map []
```
#### Access element
- ContainsKey

Tests if an element is in the domain of the map
```f#
let sample = Map [ (1, "a"); (2, "b") ]

printfn "sample contain key 1: %b" (sample.ContainsKey 1) // sample contain key 1: true
printfn "sample contain key 3: %b" (sample.ContainsKey 3) // sample contain key 3: false
```
- map.[key]

access by key in the domain will return the value, raise Exception if key not exists
```f#
let sample = Map [ (1, "a"); (2, "b") ]

printfn "access by key 1: %s" sample.[1] // access by key 1: a
printfn "access by key 3: %s" sample.[3] // Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary
```
if you use browser environment, you may not see this Exception
- TryFind

Lookup an element in the map, returning a `Some` value if the element is in the domain of the map and `None` if not
```f#
let sample = Map [ (1, "a"); (2, "b") ]

printfn "TryFind key 1: %A" (sample.TryFind 1) // evaluates to Some "a"
printfn "TryFind key 3: %A" (sample.TryFind 3) // evaluates to None
```
- TryGetValue

Lookup an element in the map, assigning to `value` if the element is in the domain of the map and returning `false` if not
```f#
let sample = Map [ (1, "a"); (2, "b") ]

printfn "access by key 1: %A" (sample.TryGetValue 1) // evaluates to (true, "a")
printfn "access by key 3: %A" (sample.TryGetValue 3) // evaluates to (false, null)
```
`TryGetValue` is added in F# 6.0</br>

Conclusion: `TryFind` `TryGetValueThe` provide a safer way to access elements, and `option` type returned by `TryFind` `TryGetValueThe` fits naturally with F#'s pattern matching
#### Pattern Matching with Map
- TryFind match
```f#
let mp = Map.ofList [("doot", 1); ("beef", 2); ("hoopty", 3)]

match Map.tryFind "doot" mp with
| Some value -> printfn "Value: %A" value
| None -> printfn "No value found!"

match Map.tryFind "goot" mp with
| Some value -> printfn "Value: %A" value
| None -> printfn "No value found!"
```
- TryGetValue match
```f#
let mp = Map.ofList [("doot", 1); ("beef", 2); ("hoopty", 3)]

match mp.TryGetValue "doot" with
| (true, value) -> printfn "Value: %A" value
| (false, _) -> printfn "No value found!" // Value: 1

match mp.TryGetValue "goot" with
| (true, value) -> printfn "Value: %A" value
| (false, _) -> printfn "No value found!" // No value found!
```
#### Loop Map
- for KeyValue(key, value) in
```f#
let map = Map.ofList [("doot", 1); ("beef", 2); ("hoopty", 3)]

for KeyValue(key, value) in map do
    printfn "Key: %s, Value: %d" key value
```
- Map.iter
```f#
let map = Map.ofList [("doot", 1); ("beef", 2); ("hoopty", 3)]
map |> Map.iter (fun key value -> printfn "Key: %s, Value: %d" key value)
```
#### Modify element of Map
- Add & Remove
```f#
let map = Map.ofList [("doot", 1); ("beef", 2); ("hoopty", 3)]

let mapWithAddedElement = map.Add("pork", 4)
printfn "Map after adding (4, \"four\"): %A" mapWithAddedElement

let mapWithRemovedElement = mapWithAddedElement.Remove("beef")
printfn "Map after removing key 2: %A" mapWithRemovedElement
```
#### Practice
Counting Words in text: "hello world hello map in F#"
```f#
let text = "hello world hello map in F#"
let words = text.Split(' ') // ["hello", "world", "hello", "map", "in", "F#"]

let wordCountMap = 
    words 
    |> Array.fold (fun acc word ->
        if Map.containsKey word acc then // Static Member Function
            let count = acc.[word]
            acc.Add(word, count + 1)
        else
            acc.Add(word, 1)
    ) Map.empty

printfn "Word count map: %A" wordCountMap // Word count map: map [(F#, 1); (hello, 2); (in, 1); (map, 1); (world, 1)]
```
the solution use Map.containsKey which is *Static Member Function*, we can use acc *Instance Member Function* like this:
```f#
let text = "hello world hello map in F#"
let words = text.Split(' ')
printfn "words: %A" words

let wordCountMap = 
    words 
    |> Array.fold (fun (acc: Map<string, int>) word ->
        if acc.ContainsKey word then // Instance Member Function
            let count = acc.[word]
            acc.Add(word, count + 1)
        else
            acc.Add(word, 1)
    ) Map.empty

printfn "Word count map: %A" wordCountMap
```
*Static Member Function* and *Instance Member Function* will be introduced in OOP