Scoring Scrabble Words with Elm
Tags: functional, elm, beginners, humor
Published: 2017-10-10 13:42:41

---
title: Scoring Scrabble Words with Elm
published: true
description: "For ages, man has been vexxed by this simple problem. Today, I attempt a solution." 
tags: functional, elm, beginner, humor
canonical_url: http://www.vincecampanale.com/blog/2017/10/10/scrabble-words-elm/
---

<a title="By Gortyna (Own work) [CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File%3ACleobulus_Rhodes.JPG"><img width="256" alt="Cleobulus Rhodes" src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Cleobulus_Rhodes.JPG/256px-Cleobulus_Rhodes.JPG"/></a>

### The Problem

In approximately 6th century B.C., Cleobulus of Lindos, one of the Seven Sages of Greece, presented to the world a word-based mind game<sup>1</sup>. He called it "Scrabble". When outlining his game, he omitted an important part, perhaps on purpose -- how to compute the score of a word. A universal solution has yet to arise. 

### Assumptions

In order to provide a reasonable solution that will hold in this dimension, we must first make some assumptions.  
```
1) The letters A, E, I, O, U, L, N, R, S, and T have the value 1.
2) The letters D and G have the value 2.  
3) The letters B, C, M, and P have the value 3.  
4) The letters F, H, V, W, and Y have the value 4.  
5) The letter K has the value 5.  
6) The letters J and X have the value 8.  
7) The letters Q and Z have the value 10.  
```

### A Solution

Let us begin.  

Breaking the problem down and outlining a solution in pseudocode is good for simple scripting problems like this one, so let's start there.

```
-- given a word
-- split the word up into letters
-- get the points value for each letter
-- store / record the point values for each letter
-- add up all the point values
-- return the sum
```

I find it helpful to start with the type signature when constructing functions in Elm.  The function we want to build will take a word and return a score. In terms the compiler can understand, it will take a `String` and return an `Int`.
```
scoreWord: String -> Int
```

Next, we can start building our function.
```
scoreWord: String -> Int
scoreWord word = 
   word
```

Okay, so splitting the word up into letters is easy:

```
scoreWord: String -> Int
scoreWord word = 
  String.split "" word
```

Now we need to get the points value for each letter. This introduces a bit of complexity into our problem -- where do the point values come from? Elm doesn't know what a letter is worth. One way to tell it is to make a record, a series of key-value pairs.


```
pointsCategories =
    { one = [ "A", "E", "I", "O", "U", "L", "N", "R", "S", "T" ]
    , two = [ "D", "G" ]
    , three = [ "B", "C", "M", "P" ]
    , four = [ "F", "H", "V", "W", "Y" ]
    , five = [ "K" ]
    , eight = [ "J", "X" ]
    , ten = [ "Q", "Z" ]
    }
```

Each key of our record is the word version of the point value. Each value is a list containing every letter that has that point value. Records in Elm cannot have number literals for keys, so this is the closest we can get.

Now, we need a function that will score a letter by looking up it's point value in the `pointsCategories` record and converting that to a number.

```
getPointsForLetter : String -> Int
getPointsForLetter letter =
    if List.member letter pointsCategories.one then
        1
    else if List.member letter pointsCategories.two then
        2
    else if List.member letter pointsCategories.three then
        3
    else if List.member letter pointsCategories.four then
        4
    else if List.member letter pointsCategories.five then
        5
    else if List.member letter pointsCategories.eight then
        8
    else if List.member letter pointsCategories.ten then
        10
    else
        0
```

Okay, this function is a little (very) redundant. I'm starting to get the feeling that this program is a wee bit smelly and could use some tidying up, but let's get this solution working before we refactor.

This would be a good time to note that all the letters in our `pointsCategories` record are uppercase, but we do not have that guarantee for a given word, unless you're playing a very loud version of Cleobulus's game.

In order to address this, we might as well make all of our letters uppercase, since that's how they appear in the `pointsCategories` record -- WITH ENERGY! 

```
scoreWord : String -> Int
scoreWord word =
  List.map String.toUpper (String.split "" word)
```

Now we have a list of uppercase letters to work with so we can use our `getPointsValue` function to convert letters to points.

```
scoreWord : String -> Int
scoreWord word = 
  List.map getPointsForLetter (List.map String.toUpper (String.split "" word))
```

These parentheses are getting out of hand -- time to take advantage of the Elm's brilliant pipe operator to make our solution readable.

The pipe operator is nice because it turns 
```
f(g(h(i(x))))
```
into
```
x 
  |> i 
  |> h 
  |> g 
  |> f
```
where `x` is some value and `i`, `h`, `g`, and `f` are functions.

Applying this nifty tool to our solution, we get:

```
scoreWord : String -> Int
scoreWord word =
  word 
    |> String.split ""
    |> List.map String.toUpper
    |> List.map getPointsForLetter
```

The astute reader will notice we are not yet returning an `Int`, but rather a list of `Int`'s (or `List Int` using Elm syntax). Let's fix that.

```
scoreWord : String -> Int
scoreWord word = 
  word 
    |> String.split ""
    |> List.map String.toUpper
    |> List.map getPointsForLetter
    |> List.sum
```

This solution works, but I detest the repetitive chain of `if else if`'s in the `getPointsForLetter` function. Unfortunately, this is the crux of the solution, so this refactor is going to be a big one!

### All Aboard the Refactor Tractor

The main issue with the above solution is the fact that we can't use a number literal as the key in our `pointsCategories` record. This means we are separating looking up the value and converting it to an `Int`. We will need to reach into our toolbox and find a new data structure to use in order to solve this. I propose we use a dictionary (`Dict`) to tie a number to a character. We need to import `Dict` to use it.

```
import Dict exposing (..)

pointValues : Dict Char Int
pointValues =
    Dict.fromList
        [ ( 'A', 1 )
        , ( 'E', 1 )
        , ( 'I', 1 )
        , ( 'O', 1 )
        , ( 'U', 1 )
        , ( 'L', 1 )
        , ( 'N', 1 )
        , ( 'R', 1 )
        , ( 'S', 1 )
        , ( 'T', 1 )
        , ( 'D', 2 )
        , ( 'G', 2 )
        , ( 'B', 3 )
        , ( 'C', 3 )
        , ( 'M', 3 )
        , ( 'P', 3 )
        , ( 'F', 4 )
        , ( 'H', 4 )
        , ( 'V', 4 )
        , ( 'M', 4 )
        , ( 'Y', 4 )
        , ( 'K', 5 )
        , ( 'J', 8 )
        , ( 'X', 8 )
        , ( 'Q', 10 )
        , ( 'Z', 10 )
        ]
```
Now we can access the point value for a letter using the `Dict.get` method. This change of data structure is crucial because we have moved the redundancy out of our program and into the data itself. Redundancy in data is gravy, redundancy in code is gunk.

Let's write a function that takes a character and returns it's point value.

```
getPointValue : Char -> Int
getPointValue letter = 
    let
        letterScore =
            Dict.get letter pointValues
    in
    case letterScore of
        Just score ->
            score

        Nothing ->
            0
```

`Dict.get` is not guaranteed to return something since it can be called with a value that may not correspond to a key in the dictionary, like if we tried to get the point value of a Chinese character. In terms that Elm's compiler can understand, `Dict.get` returns a `Maybe` type, so we need to tell the program how to handle both cases of the `Maybe` type -- `Just` (we got something) and `Nothing` (no luck).

If we get a score back, then return that score. If not, default to 0.

Time to rewrite `scoreWord` so it splits our word into a list of `Char`'s (`String.toList`) and uses our new `getPointValue` function to get the value of each character in the list.

```
scoreWord : String -> Int
scoreWord word =
    word
        |> String.toUpper
        |> String.toList
        |> List.map getPointValue
        |> List.sum
```

This works, but I still think `getPointValue` is a bit verbose. Let's use a couple tricks to make it easier to understand.

`flip` is a neat tool that lets you flip the order of arguments to a function. `Maybe.withDefault` is a shorthand way to do what we did in our `case` statement. Taking advantage of these two handy mechanisms and the pipe operator, we can write `getPointValue` like this:

```
getPointValue : Char -> Int
getPointValue letter =
  letter 
    |> flip Dict.get pointValues
    |> Maybe.withDefault 0
```
Ah, much better.

To cap off the post, here's the entire script:
```
module ScrabbleScore exposing (..)

import Dict exposing (..)


pointValues : Dict Char Int
pointValues =
    Dict.fromList
        [ ( 'A', 1 )
        , ( 'E', 1 )
        , ( 'I', 1 )
        , ( 'O', 1 )
        , ( 'U', 1 )
        , ( 'L', 1 )
        , ( 'N', 1 )
        , ( 'R', 1 )
        , ( 'S', 1 )
        , ( 'T', 1 )
        , ( 'D', 2 )
        , ( 'G', 2 )
        , ( 'B', 3 )
        , ( 'C', 3 )
        , ( 'M', 3 )
        , ( 'P', 3 )
        , ( 'F', 4 )
        , ( 'H', 4 )
        , ( 'V', 4 )
        , ( 'M', 4 )
        , ( 'Y', 4 )
        , ( 'K', 5 )
        , ( 'J', 8 )
        , ( 'X', 8 )
        , ( 'Q', 10 )
        , ( 'Z', 10 )
        ]


getPointValue : Char -> Int
getPointValue letter =
    letter
        |> flip Dict.get pointValues
        |> Maybe.withDefault 0


scoreWord : String -> Int
scoreWord word =
    word
        |> String.toUpper
        |> String.toList
        |> List.map getPointValue
        |> List.sum
```


While we can never know exactly how Cleobulus intended for a word to be scored, we can give our best guess and this is mine.

----------------------------------------------------------------------------------

<em>If you like my style, [check out my blog](http://www.vincecampanale.com/blog/) or follow me on twitter at [@_vincecampanale](https://twitter.com/_vincecampanale) for occasional web development goodies in between memes and retweets :)</em>


<sup>1</sup> I made this up.