You might have read Seeing Java where I described my experience writing a project for a Java class as an exercise in learning some modern Java. Today I did a similar experiment but on a much smaller scale and with one of the polar opposites of the Java language: Haskell.
Haskell is something I’ve been thinking quite a bit about lately as I’ve been pushing myself to learn more about “functional programming.” Haskell and Clojure are two languages that break the normal paradigm I’m used to and encourage a dramatically different style of programming. They take very different approaches to that concept, though, and I’ll only be discussing Haskell today. By the way, today is the day I wrote my first program ever in Haskell.
Functional programming is a misnomer. Most software is functional. The term comes from a technical aspect about passing around functions whereas many languages only allow passing around data. Functional programming is basically the result of the worlds of mathematics and computer science crashing into each other. I’ll describe it more fully in a future post.
Java and Haskell have strong typing in common, but Java’s typing system feels like a bear compared to Haskell’s, which helps more than it annoys. Typing is lightweight in Haskell, which means that the language encourages creating all sorts of names for a certain type of data. Instead of a list of numbers in a certain form, for example, we might create a phoneNumber
type. This can be done in most programming languages but it can require more effort to do well.
Haskell is a purely functional language. This comes from the math side of things and that means we can make a bunch of assumptions about the code we write, which in turn makes code that might otherwise be very slow to run work fast. Usually when programming there is a way to write code that is simple to understand but slow and another way that is harder to read but runs faster. Because we “promise” not to do certain things, the Haskell compiler can translate our easier-to-read code into faster-to-run code for us.
Haskell has a bad reputation for being incredibly cryptic. My first bit of Haskell was pain-free, but I’m not doing anything particularly complicated. I enjoyed the language syntax (the format for how you are supposed to write the program) and the error messages were pretty good when I wrote incorrect code. As a programmer I didn’t find this any more cryptic or hard-to-read than just about any other language.
One of the things I really enjoy is the declarative nature of this code verses the imperative or procedural nature of what I’m used to. When we typically learn how to program, we are taught how to break complicated processes into smaller and smaller steps and then order those steps in code to accomplish our goals. The math-side of functional languages prefer to focus on the relationships between data (or input and output) and let the computer “figure out” how to make it work. Let me illustrate with a description of pattern matching.
Pattern matching is a way to specify different behaviors for input that is structurally distinct. A little code will hopefully make sense.
pluralize :: takes a word and a plurality indicator to make a new word noun 1 = noun noun 2 = "a couple {noun}s" noun n = "{noun}s"
Disclaimer: the above snippet isn’t real code from any language, but it’s similar to how Haskell makes functions to operate on data. We can see here in the second line that if we call pluralize
with a noun and a count of 1
, then we just return the noun. If, however, we want the noun for a count of 2
, the function pieces together a simple fragment, “a couple of things.” Finally, if we call it with any other number besides 1
or 2
, we simply add an s
on the end of the noun and call it a day. This crude pluralizer won’t get an A in grammar class, but it’s not a bad way to describe the behavior we want from pluralizing nouns.
This code snippet leaves much to be imagined on how to accomplish the pluralizing. In a traditional imperative language, we would have to construct it in a way that includes lots of “support” code, which figures out what circumstances we are in before giving a new value.
pluralize( noun, count ) if count is 1 then noun else if count is 2 then "a couple {noun}s" else "{noun}s"
The differences are subtle in a simple function like this, but in the first snippet we don’t have to perform checking on the input to see if it is a particular value and then to respond accordingly. Sometimes we end up with more code to check the input than we do to actually get.stuff.done. Again, this is a result of the mathematical focus on the algorithm in Haskell and other functional programming languages.
This was a small project but a fun one. Reasons for choosing Haskell for a project include the language’s ability to prove its behavior the way we construct mathematical proofs such as for the Pythagorean Theorem, its ability to concurrently run code safely, its type safety, and the love of the purity and expressiveness of the language.
The only real trip-up I had came from the fact that I was running my code through my text editor – a “set-it-and-forget-it” kind of deal. Unfortunately, a few of the errors I made in the process caused the code to run forever without ending, so I was surprised to see a popup telling me that my computer’s memory was full and I had to close some applications or it would crash. Whoops.
Want to see the code? Here it is. How much of what it is doing can you figure out? How would you attempt to add a new function in this code to compute the standard deviation?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Data.Monoid; | |
x1 = [1..100] | |
x2 = [2,4..100] | |
x3 = [3,6..100] | |
data Summary = Summary {µ::Float, n::Int} deriving (Show) | |
instance Monoid Summary where | |
mempty = Summary { µ = 0, n = 0 } | |
mappend s1 s2 = Summary { | |
µ = combinedMean s1 s2, | |
n = (n s1) + (n s2) | |
} | |
count :: [Float] -> Int | |
count xs = length xs | |
mean :: [Float] -> Float | |
mean xs = | |
let s = sum xs | |
n = fromIntegral $ count xs | |
in s / n | |
describe :: [Float] -> Summary | |
describe xs = Summary { µ = mean xs, n = count xs } | |
combinedMean :: Summary -> Summary -> Float | |
combinedMean s1 s2 = | |
let n1 = fromIntegral $ n s1 | |
n2 = fromIntegral $ n s2 | |
µ1 = µ s1 | |
µ2 = µ s2 | |
in (µ1 * n1 + µ2 * n2) / (n1 + n2) | |
main :: IO () | |
main = do | |
putStrLn $ " x1 – " ++ (show $ d1) | |
putStrLn $ " x2 – " ++ (show $ d2) | |
putStrLn $ " x3 – " ++ (show $ d3) | |
putStrLn $ " x1 + x2 – " ++ (show $ d1 <> d2) | |
putStrLn $ "x1 + x2 + x3 – " ++ (show $ mconcat [d1, d2, d3]) | |
where | |
[d1, d2, d3] = map describe [x1, x2, x3] |