Functional Programming via Haskell

02 Jun 2017

Some notes on functional programming using Haskell.


Over the past year I’ve started picking up programming languages with an eye towards the world of app development. I sharpened my skills in Java to develop an Android app; started learning Swift to tackle an iOS version of said app; studied C# to gain some cross-mobile app coding experience; and I’ve been refining my Python knowledge to navigate Django to write web apps (I’ll have a future post on this).

But one language I started playing with purely for curiosity’s sake is Haskell. Up until now, I have dealt with languages that are primarily imperative in nature. This is in contrast to a purely functional programming language like Haskell.

What do these terms mean, and what is the difference between them? One of the reasons I started learning Haskell is to help me answer this question! So I’ll summarize my findings.

Imperative vs. Functional Programming

If you look up the Wikipedia entry for Programming Paradigm, you will see contrasting characteristics for the imperative and functional styles:

  • imperative which allows side effects
  • functional which disallows side effects

So what are side effects? It means that functions are allowed to modify the state of a program beyond their scope, and may not exhibit the same behavior on different executions. In other words, executing a function creates side effects within other parts of the program. State variables that can be changed are called mutable, and imperative languages support mutable state. In functional programming, the opposite is true: running a function with the same parameters should produce the result every time. Most data structures in Haskell are immutable, so once they are set to a value, that value cannot be changed. There are ways to handle mutable states in Haskell via a construct called a monad, but that is beyond the scope of this post. Haskell is, at its heart, grounded in immutability.

To illustrate the differences between imperative and functional programming styles, first consider the following class definition for a container of liquid in Python:

class LiquidContainer(object):
  def __init__(self, amount, size):
    if amount > size: # can't store more liquid than you can fit...
      raise ValueError
    else:
      self.amount = amount
      self.size = size

  # add to container
  def add(self,amount):
    if amount + self.amount > self.size:
      print("Can't add {}! You can add at most {}!".format(amount, self.size - self.amount))
    else:
      self.amount += amount

  # pour out of container
  def pour(self,amount):
    if amount > self.amount:
      print("Can't pour {}! You only have {} left!".format(amount, self.amount))
    else:
      self.amount -= amount

The concept is simple: you have a container of water, juice, etc., and you can either pour some out or add some to the container. But you cannot add more liquid than you can store, nor can you pour out more liquid than you have available. This is why we have the variable self.amount, which keeps track of the state of the container. If I were to run the pour function with the same parameters every time:

container = LiquidContainer(200,300) # create a new container object
container.pour(100) # we have 100 (mL, let's say?) left
container.pour(100) # we're out of liquid
container.pour(100) # whoops! we'll get a message that we can't do this..

I’d get a different result once I ran out of liquid. This is because the pour function changes the state of the LiquidContainer object, and the function’s behavior depends on the state of the object.

In functional programming, this is not allowed. Under this paradigm, functions are analogous to functions in mathematics: every input maps to an output, and the output does not change if the parameters are fixed. For example, consider the function

\(f(x,y) = x^2 + y^2 \)

Then , no matter how many times I evaluate it.

Also note that under the imperative programming paradigm, the pour and add functions simply change the amount in the container. They are written as statements that do something to the object. In contrast, everything in functional programming returns a value. Again, like mathematical functions, they produce one output for every input, which means something must be returned.

So how would I implement the equivalent of the LiquidContainer under a functional programming paradigm? Simple. The pour and add functions would return a new LiquidContainer with the updated contents. Whereas the Python code takes an object oriented approach, Haskell uses what are called type classes.

Here is how I would implement a liquid container in Haskell:

module Container(amount,size,liquidContainer, addLiquid, pourLiquid)
where

-- Generic container, no constraints on size/amount
data Container = Container {  amount :: Float,
                              size :: Float
  }

-- liquidContainer derives from Container with constraints
liquidContainer :: Float -> Float -> Maybe Container
liquidContainer amount' size' | amount' > size' = Nothing
                              | otherwise       = Just $ Container {amount=amount', size=size'}

-- Now create some functions that adjust amount in containers
addLiquid :: Float -> Container -> Maybe Container
addLiquid amount' container | amount container + amount' > size container = Nothing
                            | otherwise = liquidContainer newamount $ size container
                            where newamount = amount container + amount'

pourLiquid :: Float -> Container -> Maybe Container
pourLiquid amount' container  | amount' > amount container = Nothing
                              | otherwise = liquidContainer newamount $ size container
                              where newamount = amount container - amount'

This creates a module that is meant to be imported and used by a program with a main function. There are several things happening here:

  • module Container(...) is specifying which functions and properties are meant to be accessed outside the scope of this file. We have the amount and size properties, as well as the functions to pour and add the liquid. We also have a function which serves as a constructor called liquidContainer. It takes in the amount and size and returns a value of type Maybe Container. This means it is either of type Just Container, which indicates success, or Nothing, which indicates that something went wrong (i.e. that our liquid exceeded the size of the container).
  • data Container = ... is the true constructor of the Container datatype, but it does not know how to distinguish between valid and invalid parameter values. This is why we added the liquidContainer function, and we do not allow outside access to the true constructor.
  • The addLiquid function takes a Float and a Container as arguments and returns type Maybe Container. Remember that liquidContainer returns a Maybe Container as well, so we must verify that the construction happened successfully before we pass it to addLiquid (i.e. make sure its value is not Nothing). The addLiquid function will return Nothing if we try adding too much liquid. If it is successful, it returns a value of type Just Container with the updated amount.
  • The pourLiquid function works the same way, but it will return Nothing if we try to remove more than we have.
  • See all those vertical lines? These are called guards, and they are used to control the flow of execution. Following each guard, the expression on the right hand side of the equals sign will be evaluated if the condition on the left hand side is met. They are similar to if-else statements. In fact, here is the liquidContainer definition rewritten as an if-then-else expression in Haskell:
-- liquidContainer derives from Container with constraints
liquidContainer :: Float -> Float -> Maybe Container
liquidContainer amount' size' = if amount' > size'
                                  then Nothing
                                else
                                  Just $ Container {amount=amount', size=size'}

Another important point is that the Haskell’s control-flow mechanisms return a value that is assigned to the output of the function as it acts on its arguments. Contrast this with the example in Python, where the if and else statements themselves produce no output and are only used to organize commands that depend on one or more conditions being satisfied.

One last thing to address: what is the need for Maybe types? Well, everything in Haskell must return a value. If I’m pouring water from my container, I need to return a container with the updated amount. But what if I try to pour out too much? I can throw an error, but maybe I don’t want to. Maybe I want this attempt at over-pouring to trigger some other function that tells me I should add more water. Furthermore, I still need to return a container to satisfy my type requirements. The Maybe type addresses both concerns. I either get a Just Container, which indicates success, or a Nothing, which indicates something went wrong. So if I specify my return type as Maybe Container, I satisfy my type requirements and can decide what to do next.

Just for completeness, here is a program that utilizes the Container module and uses its functions:

import Container
import Data.Maybe

main = do

  -- Initialize container, pour liquid if everything went smoothly
  let jar = liquidContainer 200 300

  let emptiedjar = case jar of
                    Nothing -> Nothing
                    jar -> pourLiquid 200 $ fromJust jar

  -- Initialize another container, add liquid if everything went smoothly
  let newjar = liquidContainer 100 300

  let fulljar = case newjar of
                  Nothing -> Nothing
                  newjar -> addLiquid 200 $ fromJust newjar

  -- Get amount left in emptied jar
  let emptiedamount = show . amount $ fromJust emptiedjar

  putStrLn $ "The emptied jar has " ++ emptiedamount ++ " left!"

  let fullamount = show . amount $ fromJust fulljar

  putStrLn $ "The full jar has " ++ fullamount ++ " left!"

Two comments on this:

  1. The case ... of ... statements are similar to switch statements in C-style languages, for example.
  2. The fromJust function from the Data.Maybe module takes a value of type Just a and removes the Maybe wrapper, returning a value of type a. Recall that we need pass a Container, not a Just Container, to the pourLiquid and addLiquid functions. But if the value we’re trying to unwrap is a Nothing value, then this will throw an error, so we must be certain things went smoothly before we do this!

So that’s all I have for now! We’ve seen that Haskell shares common ground with mathematics in how functions are treated. In fact, it’s an inherently mathematical language, borrowing terms like functors, monads, and applicatives from category theory. I dare not make bold claims about the language until I spend enough time with these constructs. Here’s a nice blog entry that attempts to explain these concepts in a graphical, intuitive way.

Leave a Comment