Haskell for Python Programmers
Python is a functional programming language like Haskell.
Haskell is one of my favorite functional languages. I learned it in college and have continued using it occasionally. It’s common in the academic, scientific, and sometimes financial world. However, outside of these realms, it’s rarely used. I hope that changes over time since Haskell code is useful, beautiful, and easy. Python, my language of choice in work, is very similar to Haskell in many ways; one could even say that Haskell is the most Pythonic functional language. Python took many concepts from languages like Haskell and Lisp which will be shown later.
There are some great books on Haskell, but I want to focus on transitioning from Python in particular because Python is very similar to Haskell in certain ways. You will probably notice similarities, like from the functools module in Python, for example. Haskell is also more readable than Lisp, with a nearly equivalent expressive power which is why it is often preferred.
Haskell is in the functional paradigm, in which functionality is built up from composing functions together, similar to the Unix philosophy of using small scripts and tools together. Python is an imperative language with functional aspects, which means that it executes code line by line to modify the state of the program. This distinction means Haskell code is implemented differently than Python and other imperative languages in many cases.
Functions
Let’s start with the basis of Haskell; since Haskell is a functional language, everything in Haskell uses functions. Functions in Haskell are different from functions in Python. In Haskell, a function is immutable and must always return a value. In addition, it must return the same value for the same input, a property called idempotency. Whereas in Python, a function can return any value, and it doesn’t even have to return anything. Let’s compare a function in Python and Haskell that returns a value of 1.
Python
def return1() -> int:
return 1
Haskell
let return1 = 1
The let can be removed to make the definition global. In Haskell, all we have to say is that our function name equals 1. This binds the name return1 to the function body.
Recursion
In Haskell, there are no loops, only recursion; the compiler optimizes tail-recursive calls. In Python, this doesn’t happen, making recursion in Python much less efficient. Let’s implement a function in Python using loops to reverse a string and then see the same function in Haskell.
def reverse(s: str) -> str:
revs = ""
for i in range(len(s) - 1, -1, -1):
revs += s[i] # alternatively we could have just done s[::-1]
return revs
The same function in Haskell requires recursion.
reverse' :: String -> String
reverse' "" = ""
reverse' (x : xs) = (reverse xs) ++ [x]
In Haskell, we can provide multiple definitions of a function with different literal values, as we did with the empty quotes. The colon operator defines a type, and the type String -> String is a function that takes a string and returns a String. In the last definition, our argument is (x : xs); this pattern-matching technique splits the string into the first character and then the rest of the string. Then we recursively call reverse on the rest and append our first character to the end.
Unlike Python, Haskell is strongly typed, meaning that everything has to have a type. This comes from ML and other functional languages with a strong type system. Haskell has automatic type inference, so we could have removed the first line; however, providing the type definition is often beneficial as it helps document the code, helping us understand what the input and output required are.
Conditionals
In Haskell, we can also have cases in a function like this.
canDrink :: Int -> String
canDrink age
| age < 21 = "Cannot drink"
| age == 21 = "Just able to drink"
| otherwise = "Can buy alcohol anywhere"
Notice how each case checks for the age and then, depending on the condition, does something based on it. The equivalent in Python would require a bunch of if-else statements.
def canDrink(age: int) -> bool:
if age < 21:
return "Cannot drink"
elif age == 21:
return "Just able to drink"
else:
return "Can buy alcohol anywhere"
We can do something similar with if-else in Haskell, but it’s uglier.
canDrink age =
if age < 21 then "Cannot drink"
else if age == 21 then "Just able to drink"
else "Can buy alcohol anywhere"
Notice the indention; like Python, Haskell uses indention to signify blocks of code that should be grouped together.
List Comprehensions
Python took list comprehensions from languages like Haskell and Lisp, and we will see how right now. In Python, we can find all the Pythagorean triples with a sum of less than 100 like this. We can use the walrus operator, :=, to assign c inside the loop.
[(a, b, c) for a in range(100) for b in range(100) \
if a + b + (c := math.sqrt(a**2+b**2)) < 100]
We can do the same thing in Haskell like this.
[(a, b, c)| a <- [1..100], b <- [1..100],
let c = sqrt (a^2 + b^2), a + b + c < 100]
Notice how similar the two code blocks are. The only real difference is the syntactic choices, like using the arrow operator instead of for.
Haskell has lists, tuples, and most of the other types Python has and a few more. In addition to the other types, Haskell has a record type that lets you define your own data. This is similar to a struct in C rather than an entire class. In Python, you define a dataclass like this.
@dataclass
class Person:
firstname: str
lastname: str
age: int
In Haskell, you can define a new record data type like this; record data types are nearly identical to dataclasses in Python.
data Person = Person {
firstname :: String,
lastname :: String,
age :: Int
} deriving(Show, Eq)
let ron = Person {firstname="Ron", lastname="Burgundy", age=46}
The deriving expression derives certain typeclasses; typeclasses are similar to interfaces in Java. Python has no equivalent for them other than an abstract class with unimplemented methods. Some typeclasses like Show provide a __str__ or toString like functionality, letting us print out the data structure. Others like eq provide an __eq__ functionality, letting us use the equality operator.
IO and Monads
One thing that is more complex in Haskell than in Python is output. Since Python allows side effects, we can use the print function wherever we want to print. In Haskell, on the other, we have to use monadic notation. Haskell functions are pure, meaning that for the same input, they must return the same output. This means Haskell doesn’t allow side effects or computations that affect the outside state of the system since that might return a different value for the same input. Because of this, we must use a special syntax derived from monads to make it work.
Monads are a special topic that deserves their own article; a monad in Haskell is like chaining together a series of functions with a context; that allows for stateful computation and modifying the outside environment. In Haskell, we use the IO monad to affect the state of the IO device, which can both read and output information. We will define a main function that returns a type of IO().
main :: IO ()
main = do
putStrLn "This is a print statement."
putStrLn "This is another print statement."
The do notation chains a series of function calls with the IO context. This allows for something resembling imperative programming in Haskell despite Haskell not supporting that paradigm.
Another example of a Monad is the Maybe Monad. This is similar to Promises in Javascript, which lets us carry out computation and then fail if it doesn’t work. For example, let’s try creating a function doSomething that fails if its input is less than 0.
doSomething :: Maybe Int -> Maybe Int
doSomething (Just n)
| n >= 0 = Just (n * n)
| otherwise = Nothing
doSomething Nothing = Nothing
Reversing Words
As an example of how the two are different, let’s try reimplementing a Leetcode problem, reversing the words in a string from Python to Haskell. My shortest solution for this was done in almost one line.
import re
class Solution:
def reverseWords(self, s: str) -> str:
return " ".join(re.split(r"\s+", s)[::-1]).strip()
The Haskell version is almost as short using function composition.
reverseWords :: String -> String
reverseWords = unwords . reverse . words
We don’t have to declare the parameter because we’re merely redefining reverseWords as a composition of existing functions. The words function splits a string into a series of words, then reverse reverses that list and unwords joins them by spaces again. This is the simplest Haskell solution I could find, and it works. I find this solution more explainable than the Python solution, which looks strange and ugly a bit. Notice that the two solutions aren’t all that different except in syntax; the structure is the same. Words instead of split, reverse instead of [::-1], and unwords instead of join.
Functional Programming
Python has taken a lot from functional programming, and you can write entire programs in Python now using functional techniques. Two key functions in functional programming are map and reduce; in Python, we can combine both like this to find the total ordinal value of a string.
def totalOrdValue(s: str) -> int:
return reduce(lambda n, s: n + s, map(lambda c: ord(c), s))
# we can also write it like this sum(ord(c) for c in s)
In Haskell, the equivalent code would look like this.
import Data.Char (ord)
totalOrdValue :: String -> Int
totalOrdValue = foldl (+) 0 . map ord
-- It could also have been totalOrdValue = sum . map ord
The import statement imports ord from Data.Char, this is syntactically similar to a Python import. Then we use foldl, which in Haskell is the equivalent of reduce. We apply this using composition with map, to convert the string to ordinal values. This would have taken at least a few lines of code and an entire loop in C or Java, but in Haskell and Python, we can nearly do it in one line.
In Haskell, we can do much of the same functionality we can do in Python. The syntax is slightly different, but the verbosity is nearly the same. It seems modern Python is becoming more like Haskell with its new type system, list comprehensions, dataclasses, and functools. They might merge eventually as the overlap grows. Unlike Python, Haskell compiles to optimized machine code which can improve performance but can have a higher learning curve if you come from an imperative language.
In fact, it helps when learning Haskell to unlearn everything else you’ve learned. This might make it a bigger jump for Python programmers since you can no longer store previous computations or use a mutable list to store data. Thinking in this way requires more thought and attention than is otherwise necessary. Hopefully, however, this article helped; there are some good books on Haskell for further reading, but trying it out is the best way.