Marshall Bowers

Conjurer of code. Devourer of art. Pursuer of æsthetics.

Everything is a Value

Tuesday, November 12, 2019
1812 words
10 minute read

What is a value? We're going to avoid the philosophical kind of values for now and focus on the computer science kind.

According to Wikipedia:

In computer science, a value is the representation of some entity that can be manipulated by a program.

Normally when we think of values we think of primitive values, like numbers or strings. We can perform operations on these values, such as adding two numbers together or concatenating two strings together to produce a new value:

ghci> 1 + 1
2

ghci> 2 + 2
4

ghci> "Hello" ++ ", world!"
"Hello, world!"

Numbers, in particular, have some interesting properties afforded to them by the laws of mathematics, such as the associative property and the commutative property:

ghci> (1 + 1) + 2 == 1 + (1 + 2)
True

ghci> 3 + 5 == 5 + 3
True

In this post we're going to explore treating complex types the same way we treat values, and what happens when we do so.

MMO-tivating Example

Let's pretend we're creating a fantasy MMORPG and we want to track some basic statistics about each player. We'll define a record to hold the stats that we're interested in tracking:

data PlayerStats =
  PlayerStats
    { hoursPlayed :: Float
    , monstersKilled :: Int
    , goldEarned :: Int
    }
  deriving (Eq, Show)

At some point we'll end up with a list of PlayerStats, probably retrieved from a database or some other means of persistence. That might look something like this:

allPlayerStats :: [PlayerStats]
allPlayerStats =
  [ PlayerStats {hoursPlayed = 2.5, monstersKilled = 25, goldEarned = 39}
  , PlayerStats {hoursPlayed = 14, monstersKilled = 167, goldEarned = 543}
  , PlayerStats {hoursPlayed = 7.75, monstersKilled = 125, goldEarned = 234}
  -- ...
  ]

Now suppose we want to aggregate these statistics to get the totals across all players. One approach to this might be to write a function to sum each stat individually:

totalHoursPlayed :: [PlayerStats] -> Float
totalHoursPlayed = foldr (+) 0. map hoursPlayed

totalMonstersKilled :: [PlayerStats] -> Int
totalMonstersKilled = foldr (+) 0 . map monstersKilled

While this approach works, it isn't exactly ideal. For one, we need to define a new function each time we have a new stat that we want to aggregate. Notice how we don't have a totalGoldEarned function defined. If we wanted to get the total amount of gold earned across all players we would need to write a totalGoldEarned function.

Another shortcoming of this approach is that each time we calculate one of these stats we need to traverse the entire list of player stats.

We can do better!

Let's write a function that will sum all of our player stats:

sumPlayerStats :: [PlayerStats] -> PlayerStats
sumPlayerStats =
  foldr
    (\a b ->
       PlayerStats
         { hoursPlayed = hoursPlayed a + hoursPlayed b
         , monstersKilled = monstersKilled a + monstersKilled b
         , goldEarned = goldEarned a + goldEarned b
         })
    PlayerStats {hoursPlayed = 0, monstersKilled = 0, goldEarned = 0}

This looks much better. Now we can use sumPlayerStats to run through the list of stats once and then pull out whichever of the accumulative stats that we want:

ghci> sumPlayerStats $ allPlayerStats
PlayerStats {hoursPlayed = 24.25, monstersKilled = 317, goldEarned = 816}

ghci> hoursPlayed . sumPlayerStats $ allPlayerStats
24.25

While this approach is perfectly fine, it turns out we can still do better.

Let's take a step back and return to the concept of values.

Say we want to sum a list of numbers. For example, here's how we could sum the numbers from one to ten:

ghci> foldr (+) 0 $ [1..10]
55

To sum a list of numbers we're still using (+), just like when we performed 1 + 1 or 2 + 2. Can we make summing our PlayerStats just as simple as 1 + 1?

Turns out, we can!

The first thing we'll need to do is define an instance of Num for PlayerStats:

instance Num PlayerStats where
  a + b =
    PlayerStats
      { hoursPlayed = hoursPlayed a + hoursPlayed b
      , monstersKilled = monstersKilled a + monstersKilled b
      , goldEarned = goldEarned a + goldEarned b
      }
  a - b =
    PlayerStats
      { hoursPlayed = hoursPlayed a - hoursPlayed b
      , monstersKilled = monstersKilled a - monstersKilled b
      , goldEarned = goldEarned a - goldEarned b
      }
  a * b =
    PlayerStats
      { hoursPlayed = hoursPlayed a * hoursPlayed b
      , monstersKilled = monstersKilled a * monstersKilled b
      , goldEarned = goldEarned a * goldEarned b
      }
  abs stats =
    PlayerStats
      { hoursPlayed = abs $ hoursPlayed stats
      , monstersKilled = abs $ monstersKilled stats
      , goldEarned = abs $ goldEarned stats
      }
  signum stats =
    PlayerStats
      { hoursPlayed = signum $ hoursPlayed stats
      , monstersKilled = signum $ monstersKilled stats
      , goldEarned = signum $ goldEarned stats
      }
  fromInteger i =
    PlayerStats
      { hoursPlayed = fromInteger i
      , monstersKilled = fromInteger i
      , goldEarned = fromInteger i
      }

While this might seem like a lot of code at first, I promise it is all worth it in the end. By defining Num for our PlayerStats record we've given it the ability to behave just like a number.

With that in place, look at what our sumPlayerStats from before turns into:

ghci> foldr (+) 0 $ allPlayerStats
PlayerStats {hoursPlayed = 24.25, monstersKilled = 317, goldEarned = 816}

It's identical to when we summed the numbers from one to ten! By treating PlayerStats like a number we get all the benefits of a number, like being able to combine two instances using (+).

One thing to note is that this does not always work. The reason we can do this with PlayerStats is because it is comprised solely of numeric fields.

So now we know how to treat records as values. Are there other things that we can treat as values?

Fun with Functions

Let's define an isDivisibleBy function that checks whether some integer is divisible by another integer:

isDivisibleBy :: Int -> Int -> Bool
isDivisibleBy divisor n = n `mod` divisor == 0

We can then implement isEven in terms of isDivisibleBy, like so:

isEven :: Int -> Bool
isEven = isDivisibleBy 2

With these two functions we can implement an isDivisibleBy6 function using the divisibility rules:

isDivisibleBy6 :: Int -> Bool
isDivisibleBy6 n = isEven n && isDivisibleBy 3 n

We could also have implemented isDivisibleBy6 just by doing isDivisibleBy 6, but we're doing it this way in order to identify a more general pattern.

Let's examine isDivisibleBy6 a little more closely. Notice how we have two terms: isEven n and isDivisibleBy 3 n. When the function is executed we'll evaluate one or both terms ((&&) in Haskell short-circuits if the first term is False) and then use (&&) to compute the result.

Notice how we have an n on both sides of the "equation". On one side we take n as an argument to the function, and on the other we apply n to both terms.

Time for a brief detour. If we were to put this into mathematical terms, it's like having:

4 + x = (1 + x) + (3 + x)

We can remove all of the xs from the equation and the equation will still hold true.

Returning to our isDivisibleBy6 function, can we perform the same simplification here?

Let's try just removing n entirely and (&&)ing our two functions together:

ghci> isEven && isDivisibleBy 3 $ 6
<interactive>:12:1: error:
    • Couldn't match expected type ‘Bool’
                  with actual type ‘Int -> Bool’
    • Probable cause: ‘isEven’ is applied to too few arguments
      In the first argument of ‘(&&)’, namely ‘isEven’
      In the expression: isEven && isDivisibleBy 3
      In the expression: isEven && isDivisibleBy 3 $ 6

<interactive>:12:1: error:
    • Couldn't match expected type ‘Integer -> t’
                  with actual type ‘Bool’
    • The first argument of ($) takes one argument,
      but its type ‘Bool’ has none
      In the expression: isEven && isDivisibleBy 3 $ 6
      In an equation for ‘it’: it = isEven && isDivisibleBy 3 $ 6
    • Relevant bindings include it :: t (bound at <interactive>:12:1)

<interactive>:12:11: error:
    • Couldn't match expected type ‘Bool’
                  with actual type ‘Int -> Bool’
    • Probable cause: ‘isDivisibleBy’ is applied to too few arguments
      In the second argument of ‘(&&)’, namely ‘isDivisibleBy 3’
      In the expression: isEven && isDivisibleBy 3
      In the expression: isEven && isDivisibleBy 3 $ 6

Well, that didn't work out so well. The compiler is complaining because (&&) only works on Bools, and we're trying to use it with an Int -> Bool.

But what if we could make a version of (&&) that knew how to work with an Int -> Bool?

We can define our own logical operators that work on unary functions that return Bool:

(&&&) :: (a -> Bool) -> (a -> Bool) -> (a -> Bool)
(&&&) f g = \a -> f a && g a

infixr 3 &&&

(|||) :: (a -> Bool) -> (a -> Bool) -> (a -> Bool)
(|||) f g = \a -> f a || g a

infixr 3 |||

I couldn't find a way to overload (&&) and (||) for functions, hence why we need to use slightly different operators for them.

Now let's try out the expression we tried before, but this time using our own (&&&) operator in place of (&&):

ghci> isEven &&& isDivisibleBy 3 $ 6
True

It works!

And here's what it would look like as a function declaration:

isDivisibleBy6' :: Int -> Bool
isDivisibleBy6' = isEven &&& isDivisibleBy 3

We've now simplified the "equation" by removing the redundant n from both sides. What enabled us to do this was treating our functions as values and being able to combine them using logical operators.

While this particular example might appear to be of limited use (after all, we can just do isDivisibleBy 6 instead), it does open the door to more possibilities.

It's time to use our imaginations again! This time pretend that we're running a streaming service. Our domain might look something like this:

data Viewer =
  Viewer
    { age :: Int
    , isSubscriber :: Bool
    , hasParentalApproval :: Bool
    }

is18OrOlder :: Viewer -> Bool
is18OrOlder viewer = age viewer >= 18

Let's say that we have the following business requirement:

"In order to be eligible for a giveaway a viewer must be a subscriber and either 1) be 18 or older or 2) have parental approval."

Using our (&&&) and (|||) operators we can define a function that expresses this business requirement just as clearly as it was stated above:

isEligibleForGiveaway :: Viewer -> Bool
isEligibleForGiveaway = isSubscriber &&& (is18OrOlder ||| hasParentalApproval)

We can also test out our isEligibleForGiveaway function to ensure it's working as expected:

ghci> isEligibleForGiveaway $ Viewer {age = 19, isSubscriber = True, hasParentalApproval = False}
True

ghci> isEligibleForGiveaway $ Viewer {age = 21, isSubscriber = False, hasParentalApproval = False}
False

ghci> isEligibleForGiveaway $ Viewer {age = 11, isSubscriber = True, hasParentalApproval = True}
True

You can see how as the business requirements change and new requirements are added it would be trivial to update our isEligibleForGiveaway function to incorporate the new requirements while still remaining readable and easy to reason about.

Wrapping Up

In this post we've explored how we can treat complex types—like records and functions—as values, and gain a number of benefits in doing so.

By treating our PlayerStats record like a number we were able to sum a list of stats just as easily as we could with a list of Ints.

And by treating our functions as values we discovered that we could combine functions together using logical operators in order to clearly express business requirements in code.

I hope you enjoyed reading this, and hopefully learned something new that you can use in your day-to-day programming.


If you liked this post and want to get notified about new ones like it, be sure to subscribe to my newsletter, Errata Exist.

And if you have any questions, comments, or other feedback, please get in touch with me. I'd love to hear from you!