100 Languages Speedrun: Episode 54: Haskell

Haskell is a language that took the ideas of functional programming very seriously. Instead of being halfhearted like OCaml or various Lisps, Haskell took the most principled functional choice every single time.

Before I get to the code, there are a f…


This content originally appeared on DEV Community and was authored by Tomasz Wegrzanowski

Haskell is a language that took the ideas of functional programming very seriously. Instead of being halfhearted like OCaml or various Lisps, Haskell took the most principled functional choice every single time.

Before I get to the code, there are a few serious issues with Haskell I want to address up front.

Haskell demands full commitment from you and has brutal learning curve. You can't slowly ease into functional style, like you could with a functional-flavored language Ruby, OCaml, Racket, or Clojure - starting with a style you're already familiar with, and gradually going "more functional" as you get comfortable with it. Nope, you start functional, remain functional, and there are pretty much no escape hatches.

Among all the languages that see any nontrivial use, Haskell likely has the most sophisticated type system, and they are no opt-outs. You will need to learn how to make the type checker happy, and a lot of perfectly reasonable code will require some serious hoops to jump through to make it type check, if it is at all possible. Haskell also pretty much expects that if you want to code properly, you'll need to use various language extensions, most of which are type system extensions. Type checker error messages often literally ask you if you want to enable some language extension or another.

Maybe Scala and Crystal get even close to that level of type system sophistication, and both are orders of magnitude less demanding, and with far more escape hatches available.

And the worst thing about Haskell is the kind of people it attracts. For some reason, when any other kinds programmers are given a choice of a "clever" more concise code, and a simpler more readable code, even Perl programmers tend to choose the simpler one. For whichever reason Haskell programmers rarely show any such restraint. You can go through some Haskell tutorials to learn the basics, then try to read some real Haskell code, and you'll feel like you accidentally opened obfuscated code golf website. Here's a joke version of this problem, but it's very real.

Hello, World!

Normally we'd start with a Hello, World!, but that's not a function. Functions take arguments, and return results, and printing stuff to the terminal is not a result.

Haskell supports IO through "monads", which are convoluted enough that explaining them through analogies was a meme a while back, including the controversial big argument if jQuery is a monad or not (to spoil it - technically it isn't, but there are some similarities).

The code below might looks fairly conventional but it's just a fake syntax hiding some alien concepts:

main = do
  putStrLn "Hello, World!"
$ ghc hello.hs
$ ./hello
Hello, World!

Fibonacci

Let's do this a functional way, constructing the output in one go, and just printing it whole once it's done.

fib :: Int -> Int
fib 1 = 1
fib 2 = 1
fib n = fib (n-1) + fib (n-2)

fibStr :: Int -> String
fibStr n = "fib(" ++ (show n) ++ ")=" ++ show (fib n) ++ "\n"

fibLoop :: Int -> String
fibLoop 0 = ""
fibLoop n = fibLoop (n-1) ++ fibStr n

main = do
  putStr (fibLoop 20)
$ ghc fib.hs
$ ./fib
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34
fib(10)=55
fib(11)=89
fib(12)=144
fib(13)=233
fib(14)=377
fib(15)=610
fib(16)=987
fib(17)=1597
fib(18)=2584
fib(19)=4181
fib(20)=6765

Let's go through it step by step:

  • we define fib recursively as expected. Haskell supports pattern matching including on numbers, so we can write separate definitions for base cases, then for recursive case (all definitions need to be all in one place)
  • we define fibStr to return a string like "fib(8)=21\n" or such - Haskell doesn't support string interpolation, so we need to ++ a bunch of strings together.
  • show might look like a function, but it's not, I'll get to it in a moment
  • fibLoop is another recursive function for building up the whole answer string
  • and finally a little IO monad code to do the printing

Type Classes

Haskell has a really strict type system, so you might be worried that each type would get its own function like intToString, the way it works in OCaml.

Fortunately this is a problem Haskell managed to solve, using "type classes". The price is a lot of additional type system complexity, but that's the price Haskell is always happy to pay. Let's see how they work.

At first we might want to write code like this:

add :: t -> t -> String
add x y = (show x) ++ " + " ++ (show y) ++ " = " ++ (show (x + y))

main = do
  putStrLn (add 40 380)
  putStrLn (add 6 0.9)

That should work right? Both + and show are defined for integers and for floats, and that's the arguments we're passing. Unfortunately Haskell type checker will not allow this. add :: t -> t -> String would mean we can pass anything to add - including strings, functions, and who knows what else, and these types might not have the right functionality available. It doesn't matter to Haskell that we never actually pass such arguments. Type signature says that that we might, and that's enough to reject this code.

Let's add some additional annotations, to specify exactly which types are allowed for add:

add :: (Show t, Num t) => t -> t -> String
add x y = (show x) ++ " + " ++ (show y) ++ " = " ++ (show (x + y))

main = do
  putStrLn (add 40 380)
  putStrLn (add 6 0.9)
$ ./add
40 + 380 = 420
6.0 + 0.9 = 6.9

It means add has type t -> t -> String, but not for any t, only for t which is an instance of type classes Show (types with show) and Num (types with + and a few other things).

By the way Haskell figured out that the 6 was actually 6.0, something OCaml would not do.

Defining own types

Well, let's follow along and define our own type, a 2-element vector, with a pair of arbitrary types for elements.

data Vec t = Vec t t

instance (Show t) => Show (Vec t) where
  show (Vec a b) = "<" ++ show a ++ "," ++ show b ++ ">"

instance (Num t) => Num (Vec t) where
  (+) (Vec ax ay) (Vec bx by) = Vec (ax + ay) (by + by)
  (-) (Vec ax ay) (Vec bx by) = Vec (ax - ay) (by - by)
  (*) (Vec ax ay) (Vec bx by) = Vec (ax * ay) (by * by)
  abs (Vec ax ay) = Vec (abs ax) (abs ay)
  signum (Vec ax ay) = Vec (signum ax) (signum ay)
  fromInteger n = Vec (fromInteger n) (fromInteger n)

add :: (Show t, Num t) => t -> t -> String
add x y = (show x) ++ " + " ++ (show y) ++ " = " ++ (show (x + y))

main = do
  putStrLn (add (Vec 20 30) (Vec 40 50))
  putStrLn (add (Vec 55.5 14.5) (Vec 210 210))
  putStrLn (show (Vec "foo" "bar"))
$ ./vec
<20,30> + <40,50> = <50,100>
<55.5,14.5> + <210.0,210.0> = <70.0,420.0>
<"foo","bar">

What's going on:

  • Show type class is simple enough, anything that can be converted to a String just needs to define its type-specific show. Any vector, if its elements are Showable, is Showable as well. Vector of non-Showable elements like a vector of functions would not work here.
  • Num is much harder - we only really want + and -, but Num demands that we define a bunch of other things, which we just define elementwise. If we want to use + we need to do all those things (or redefine some things).
  • Vector Integer is both in type classes Show (Vector Integer) and type class Num (Vector Integer)
  • Vector String is in type class Show (Vector String), but not in Num (Vector String)

As you can see Haskell demands high degree of precision. For this simple example we were able to satisfy the type checker, but at cost of a lot of complexity.

What if we wanted to do a totally reasonable putStrLn (add "foo" "bar")? Well, the short story is that you simply can't, and I don't think that's possible even with any of the available type system extension.

I'm giving this example mostly to show contrast with two other languages.

In OCaml, you can't do any of such things at all, you'll need separate addIntVectors, addFloatVectors, intVecToString, floatVectorToString, stringVectorToString etc. functions, which is just unbelievably tedious and ugly.

In Crystal you can do absolutely every thing I just mentioned, including the impossible to Haskell putStrLn (add "foo" "bar"), and it statically type checks everything, all without a single type annotation (except for Vector constructor).

FizzBuzz

Let's write a FizzBuzz. This is a very straightforward solution:

import Data.List

-- FizzBuzz in Haskell
fizzbuzz :: Int -> String
fizzbuzz n
  | n `mod` 15 == 0 = "FizzBuzz"
  | n `mod` 5 == 0 = "Buzz"
  | n `mod` 3 == 0 = "Fizz"
  | otherwise = show n

fizzBuzzLoop :: Int -> String
fizzBuzzLoop n =
  intercalate "\n" (map fizzbuzz [1..n])

main =
  putStrLn (fizzBuzzLoop 100)
  • there are many ways to define a function, we can do it by defining a list of cases with | condition = result and | otherwise = default, which is a fairly unusual use of =
  • (map f [1..n]) to apply f to every number from 1 to n.
  • intercalate (which we need to import from Data.List) is a really weird name for a String.join. That's because Haskell doesn't really have strings as a first class concept, they're just lists of characters, and intercalate attempts to be a more general name. Mainly it's a really weird naming choice, and Haskell has a lot of such cases.

You might already notice something. Haskell code contains a lot of f (...), and Haskell programmers are not in love with all those parentheses...

$

And so they defined $. Which does nothing, except it syntactically applies parentheses to everything to its right side. So the equivalent code with some $s could be:

import Data.List

-- FizzBuzz in Haskell
fizzbuzz :: Int -> String
fizzbuzz n
  | n `mod` 15 == 0 = "FizzBuzz"
  | n `mod` 5 == 0 = "Buzz"
  | n `mod` 3 == 0 = "Fizz"
  | otherwise = show n

fizzBuzzLoop :: Int -> String
fizzBuzzLoop n =
  intercalate "\n" $ map fizzbuzz [1..n]

main =
  putStrLn $ fizzBuzzLoop 100

Pointfree Fizzbuzz

This is where we should probably stop. The code is perfectly readable if you're used to the $ operator.

Unfortunately it does not stop here. As I mentioned, Haskell attracts people who really love feeling clever, a lot more than they love writing readable code.

There are many ways they express their cleverness, but the main tool is the "pointfree" style - just never explicitly saying what the function operates on, and using various ways of composing functions with combinators.

To have full Haskell experience, I gave it a go too. Defining fizzBuzzLoop and divBy pointfree was simple enough, but the fizzbuzz really took me a while. And I'm sure there are far more "clever" ways to write this:

import Data.List
import Data.Bool
import Control.Monad

divBy :: Int -> Int -> Bool
divBy = ((0 ==) .) . flip mod

fizz :: Int -> String
fizz = liftM3 bool show (const "Fizz") $ divBy 3

buzz :: Int -> String
buzz = liftM3 bool fizz (const "Buzz") $ divBy 5

fizzbuzz :: Int -> String
fizzbuzz = liftM3 bool buzz (const "FizzBuzz") $ divBy 15

fizzBuzzLoop :: Int -> String
fizzBuzzLoop =
  intercalate "\n" . map fizzbuzz . flip take [1..]

main =
  putStrLn $ fizzBuzzLoop 100

For just some partial explanations:

  • you can apply only some of the arguments to a function - a function that takes two arguments like mod x y is really a function that takes one argument (number we'll divide by) and then returns a function that takes the other argument (divisor) and returns the actual result
  • flip f x y is same as f y x - if you want to do partial application of a function, but that function takes the arguments in wrong order, you can use flip on it - for example if we want to create a function for "remainder of dividing by 3" (_ mod 3), we can flip mod, then apply 3 to it
  • $ just saves some parentheses and you never really need it
  • . is function composition (f . g) x is f (g x)
  • const x is a function that takes any argument and ignores it and returns x
  • bool a b c is a function that takes c and returns either a or b depending on c
  • liftM3 applies same argument to 3 functions
  • [1..] is infinite range starting from 1
  • take n list takes first n elements of a list, which we flip to take list first and n last so we can partially apply it
  • operators like + or == are generally placed between arguments like x + y or x == y, but if you parenthesize them one or both sides missing like (+) or (0 ==), they become functions that take the missing side or sides as arguments

This pointfree style is a fun puzzle. If you're looking for some esoteric programming challenge, Haskell definitely provides one.

Unfortunately for some reason, this style of highly obfuscated Haskell is also a huge share of Haskell you'll find in the wild.

Anyway, I'll just stop here, or I'll need to explain how monads works, and many have tried and failed that task before me.

Should you use Haskell?

No.

Haskell had a very ambitious goal, and I'm really impressed by how far they managed to get without making compromises along the way. In many ways, Haskell is also a much higher quality language than let's say OCaml. They really paid attention to the details, and Haskell issues are more often result of conscious design choices, not just random things which could have easily been done properly, but they never bothered, like so many of OCaml's issues.

On the other hand, the thing they've been trying to achieve, perhaps it should have never been attempted. Also while some Haskell developers, I assume, are good people, it really tends to attract the kind of people who value being "clever" a bit too much. Of course, if you're exactly this kind of person, Haskell will be great for you!

As for playing with Haskell as an esoteric language, I definitely recommend that.

Code

All code examples for the series will be in this repository.

Code for the Haskell episode is available here.


This content originally appeared on DEV Community and was authored by Tomasz Wegrzanowski


Print Share Comment Cite Upload Translate Updates
APA

Tomasz Wegrzanowski | Sciencx (2022-01-11T23:35:50+00:00) 100 Languages Speedrun: Episode 54: Haskell. Retrieved from https://www.scien.cx/2022/01/11/100-languages-speedrun-episode-54-haskell/

MLA
" » 100 Languages Speedrun: Episode 54: Haskell." Tomasz Wegrzanowski | Sciencx - Tuesday January 11, 2022, https://www.scien.cx/2022/01/11/100-languages-speedrun-episode-54-haskell/
HARVARD
Tomasz Wegrzanowski | Sciencx Tuesday January 11, 2022 » 100 Languages Speedrun: Episode 54: Haskell., viewed ,<https://www.scien.cx/2022/01/11/100-languages-speedrun-episode-54-haskell/>
VANCOUVER
Tomasz Wegrzanowski | Sciencx - » 100 Languages Speedrun: Episode 54: Haskell. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/01/11/100-languages-speedrun-episode-54-haskell/
CHICAGO
" » 100 Languages Speedrun: Episode 54: Haskell." Tomasz Wegrzanowski | Sciencx - Accessed . https://www.scien.cx/2022/01/11/100-languages-speedrun-episode-54-haskell/
IEEE
" » 100 Languages Speedrun: Episode 54: Haskell." Tomasz Wegrzanowski | Sciencx [Online]. Available: https://www.scien.cx/2022/01/11/100-languages-speedrun-episode-54-haskell/. [Accessed: ]
rf:citation
» 100 Languages Speedrun: Episode 54: Haskell | Tomasz Wegrzanowski | Sciencx | https://www.scien.cx/2022/01/11/100-languages-speedrun-episode-54-haskell/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.