13

So I'm trying to teach myself Haskell. I am currently on the 11th chapter of Learn You a Haskell for Great Good and am doing the 99 Haskell Problems as well as the Project Euler Problems.

Things are going alright, but I find myself constantly doing something whenever I need to keep track of "variables". I just create another function that accepts those "variables" as parameters and recursively feed it different values depending on the situation. To illustrate with an example, here's my solution to Problem 7 of Project Euler, Find the 10001st prime:

answer :: Integer
answer = nthPrime 10001

nthPrime :: Integer -> Integer
nthPrime n
  | n < 1 = -1
  | otherwise = nthPrime' n 1 2 []


nthPrime' :: Integer -> Integer -> Integer -> [Integer] -> Integer
nthPrime' n currentIndex possiblePrime previousPrimes
  | isFactorOfAnyInThisList possiblePrime previousPrimes = nthPrime' n currentIndex theNextPossiblePrime previousPrimes
  | otherwise = 
    if currentIndex == n 
      then possiblePrime 
      else nthPrime' n currentIndexPlusOne theNextPossiblePrime previousPrimesPlusCurrentPrime
        where currentIndexPlusOne = currentIndex + 1
              theNextPossiblePrime = nextPossiblePrime possiblePrime
              previousPrimesPlusCurrentPrime = possiblePrime : previousPrimes

I think you get the idea. Let's also just ignore the fact that this solution can be made to be more efficient, I'm aware of this.

So my question is kind of a two-part question. First, am I going about Haskell all wrong? Am I stuck in the imperative programming mindset and not embracing Haskell as I should? And if so, as I feel I am, how do avoid this? Is there a book or source you can point me to that might help me think more Haskell-like?

Your help is much appreciated,

-Asaf

Asaf
  • 815
  • 1
  • 9
  • 25
  • 4
    Something that will help a lot is wrapping your head around laziness. For example, for this problem I just created an infinite list of primes, and then indexed into that list for the 10001st element. – Lily Ballard Dec 09 '11 at 01:01
  • @KevinBallard But wait, if you mean that you used a sieve to create that list, then how would that work? Because doesn't the sieve remove multiples through the rest of the list... which means you have to traverse the whole list? – Asaf Dec 09 '11 at 01:11
  • No, you only have to traverse the list up to the square root of the number you're evaluating. If nothing up to the square root divides into your number, nothing higher than the square root will either. – Lily Ballard Dec 09 '11 at 01:23
  • @Asaf try to avoid thinking of the steps you would take, such as "removing the multiples". Instead think of defining an element using the other elements. Don't worry if you haven't calculated them yet - you are merely defining and the calculation will happen when needed. – llayland Dec 09 '11 at 01:34
  • @KevinBallard I'm sorry, I'm a bit confused. The way I'm imagining the sieve, you start out with a list [2..], another words 2 to infinity, and then you traverse the list, removing all multiples of 2. When you finish removing all multiples of 2 (which you can't ever finish doing) you go to the next number left, 3, and remove all multiples of 3. I feel like you're suggesting something else, something closer to the way my function works, but then where does the infinite list come into play? – Asaf Dec 09 '11 at 01:42
  • @llayland fair enough, but how do you consider the efficiency of a program if you don't consider the "steps" that would be required for the calculation? Specifically in a question like this it applies, where if you provide too trivial a solution your computer will take close to forever to provide you with an answer. – Asaf Dec 09 '11 at 01:46
  • @Asaf: Something like `primes = 2 : 3 : filter isPrime [5,7..] where isPrime n = not $ any ((==0) . mod n) $ takeWhile (( – Lily Ballard Dec 09 '11 at 05:38
  • @Asaf: The complexity of a given algorithm should be something you can figure out without knowing the precise order of operations. – Lily Ballard Dec 09 '11 at 05:57
  • @KevinBallard yup that would do it very well (responding to the first comment). As for the complexity, would you mind elaborating? I define complexity by roughly calculating how much time something would take to complete using Big O Notation. How do you define "complexity"? Just curious – Asaf Dec 09 '11 at 12:19
  • Asaf, once you realise that lazy evaluation is a certain precisely defined evaluation order, reasoning about complexity becomes possible in the same way -- you can estimate the worst-case number of reductions. – Rotsor Dec 09 '11 at 14:24
  • @Rotsor Yeah, I guess that kind of reasoning will take time. Lazy evaluation doesn't sound complicated, but it's very hard to visualize in my mind as things become complicated. – Asaf Dec 09 '11 at 15:52
  • @Asaf: You can usually know the complexity of an algorithm merely by knowing the work it must perform. For example, if I implement quicksort in Haskell, it's going to be O(nlog(n)) even if I'm not absolutely certain the precise order of operations (though as Rotsor says, lazy evaluation still has a precisely defined evaluation order that you can work through if you wish). – Lily Ballard Dec 09 '11 at 19:49

6 Answers6

14

Am I stuck in the imperative programming mindset and not embracing Haskell as I should?

You are not stuck, at least I don't hope so. What you experience is absolutely normal. While you were working with imperative languages you learned (maybe without knowing) to see programming problems from a very specific perspective - namely in terms of the van Neumann machine.

If you have the problem of, say, making a list that contains some sequence of numbers (lets say we want the first 1000 even numbers), you immediately think of: a linked list implementation (perhaps from the standard library of your programming language), a loop and a variable that you'd set to a starting value and then you would loop for a while, updating the variable by adding 2 and putting it to the end of the list.

See how you mostly think to serve the machine? Memory locations, loops, etc.! In imperative programming, one thinks about how to manipulate certain memory cells in a certain order to arrive at the solution all the time. (This is, btw, one reason why beginners find learning (imperative) programming hard. Non programmers are simply not used to solve problems by reducing it to a sequence of memory operations. Why should they? But once you've learned that, you have the power - in the imperative world. For functional programming you need to unlearn that.)

In functional programming, and especially in Haskell, you merely state the construction law of the list. Because a list is a recursive data structure, this law is of course also recursive. In our case, we could, for example say the following:

constructStartingWith n = n : constructStartingWith (n+2)

And almost done! To arrive at our final list we only have to say where to start and how many we want:

result = take 1000 (constructStartingWith 0)

Note that a more general version of constructStartingWith is available in the library, it is called iterate and it takes not only the starting value but also the function that makes the next list element from the current one:

iterate f n = n : iterate f (f n)
constructStartingWith = iterate (2+)   -- defined in terms of iterate

Another approach is to assume that we had another list our list could be made from easily. For example, if we had the list of the first n integers we could make it easily into the list of even integers by multiplying each element with 2. Now, the list of the first 1000 (non-negative) integers in Haskell is simply

[0..999]

And there is a function map that transforms lists by applying a given function to each argument. The function we want is to double the elements:

double n = 2*n

Hence:

result = map double [0..999]

Later you'll learn more shortcuts. For example, we don't need to define double, but can use a section: (2*) or we could write our list directly as a sequence [0,2..1998]

But not knowing these tricks yet should not make you feel bad! The main challenge you are facing now is to develop a mentality where you see that the problem of constructing the list of the first 1000 even numbers is a two staged one: a) define how the list of all even numbers looks like and b) take a certain portion of that list. Once you start thinking that way you're done even if you still use hand written versions of iterate and take.

Back to the Euler problem: Here we can use the top down method (and a few basic list manipulation functions one should indeed know about: head, drop, filter, any). First, if we had the list of primes already, we can just drop the first 1000 and take the head of the rest to get the 1001th one:

result = head (drop 1000 primes)

We know that after dropping any number of elements form an infinite list, there will still remain a nonempty list to pick the head from, hence, the use of head is justified here. When you're unsure if there are more than 1000 primes, you should write something like:

result = case drop 1000 primes of
    [] -> error "The ancient greeks were wrong! There are less than 1001 primes!"
    (r:_) -> r

Now for the hard part. Not knowing how to proceed, we could write some pseudo code:

primes = 2 : {-an infinite list of numbers that are prime-}

We know for sure that 2 is the first prime, the base case, so to speak, thus we can write it down. The unfilled part gives us something to think about. For example, the list should start at some value that is greater 2 for obvious reason. Hence, refined:

primes = 2 : {- something like [3..] but only the ones that are prime -}

Now, this is the point where there emerges a pattern that one needs to learn to recognize. This is surely a list filtered by a predicate, namely prime-ness (it does not matter that we don't know yet how to check prime-ness, the logical structure is the important point. (And, we can be sure that a test for prime-ness is possible!)). This allows us to write more code:

primes = 2 : filter isPrime [3..]

See? We are almost done. In 3 steps, we have reduced a fairly complex problem in such a way that all that is left to write is a quite simple predicate. Again, we can write in pseudocode:

isPrime n = {- false if any number in 2..n-1 divides n, otherwise true -}

and can refine that. Since this is almost haskell already, it is too easy:

isPrime n = not (any (divides n) [2..n-1])
divides n p = n `rem` p == 0

Note that we did not do optimization yet. For example we can construct the list to be filtered right away to contain only odd numbers, since we know that even ones are not prime. More important, we want to reduce the number of candidates we have to try in isPrime. And here, some mathematical knowledge is needed (the same would be true if you programmed this in C++ or Java, of course), that tells us that it suffices to check if the n we are testing is divisible by any prime number, and that we do not need to check divisibility by prime numbers whose square is greater than n. Fortunately, we have already defined the list of prime numbers and can pick the set of candidates from there! I leave this as exercise.

You'll learn later how to use the standard library and the syntactic sugar like sections, list comprehensions, etc. and you will gradually give up to write your own basic functions.

Even later, when you have to do something in an imperative programming language again, you'll find it very hard to live without infinte lists, higher order functions, immutable data etc. This will be as hard as going back from C to Assembler.

Have fun!

Ingo
  • 36,037
  • 5
  • 53
  • 100
  • 2
    Thank you for such a well-thought out post. I will definitely try to mimic that structure of thought when I'm having trouble. And I hope that one day I will find it "find it very hard to live without...". I decided to learn Haskell to become a better programmer, as it was clear it would force me to think in an entirely new way. The challenge is probably the most enjoyable part. It's why I didn't just go with another imperative language, it probably would have been too easy. – Asaf Dec 09 '11 at 12:43
  • It is not explained why `2 :` is needed. It can be removed without making anything less correct. – Rotsor Dec 09 '11 at 15:01
  • Rotsor: The post was long enough anyway :) Yet I think it is a good idea to start with something you know. For example, we know that primes is a list that starts with 2. One could also write 2:3:5:7: .... As you say, in the end it makes no difference. – Ingo Dec 10 '11 at 10:48
  • OTOH, in the optimized version, where we divide only by prime numbers we pick from `primes`, we will need the 2 to be there, when `isPrime n = not (any (divides n) (takeWhile (\p -> p*p <= n) primes))`, i.e. prime-ness of 2 can be established with the unoptimized version only. – Ingo Dec 10 '11 at 11:03
12

It's ok to have an imperative mindset at first. With time you will get more used to things and start seeing the places where you can have more functional programs. Practice makes perfect.

As for working with mutable variables you can kind of keep them for now if you follow the rule of thumb of converting variables into function parameters and iteration into tail recursion.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
hugomg
  • 68,213
  • 24
  • 160
  • 246
  • That makes me feel better. I guess I'll just keep reading and practicing and not worry too much about how "Haskell-like" I'm being for now. – Asaf Dec 09 '11 at 02:31
4

Off the top of my head:

Community
  • 1
  • 1
edwardw
  • 12,652
  • 3
  • 40
  • 51
  • 2
    Not sure this is kind of thing OP was looking for. These are all the abstract concepts, but don't really address thinking purely and with laziness. It is something you kind of have to get used to, but I think this is answering at the wrong level. – luqui Dec 09 '11 at 02:05
  • Thank you very much for all those sources. I am going to read through them eventually (and watch/chat). Your answer certainly answers my last question, but not the earlier questions. That being said I'm sorry I have to choose between your answer and the other. – Asaf Dec 09 '11 at 02:29
3

I think the big change from your code to more haskell like code is using higher order functions, pattern matching and laziness better. For example, you could write the nthPrime function like this (using a similar algorithm to what you did, again ignoring efficiency):

nthPrime n = primes !! (n - 1) where
  primes = filter isPrime [2..]
  isPrime p = isPrime' p [2..p - 1]
  isPrime' p [] = True
  isPrime' p (x:xs) 
    | (p `mod` x == 0) = False
    | otherwise = isPrime' p xs

Eg nthPrime 4 returns 7. A few things to note:

  • The isPrime' function uses pattern matching to implement the function, rather than relying on if statements.
  • the primes value is an infinite list of all primes. Since haskell is lazy, this is perfectly acceptable.
  • filter is used rather than reimplemented that behaviour using recursion.

With more experience you will find you will write more idiomatic haskell code - it sortof happens automatically with experience. So don't worry about it, just keep practicing, and reading other people's code.

David Miani
  • 14,518
  • 2
  • 47
  • 66
  • 1
    Using more higher-order functions: `isPrime p = all ((/= 0) . mod p) [2..p-1]` – hammar Dec 09 '11 at 06:47
  • I agree with you, and I think the solution might be to try using Hoogle all the time, and keep comparing with other people's solutions so I get more familiar with the higher order functions. Many of them seem strange to me at the moment. I find I almost never use map/all and while I use the $ symbol to avoid parenthesis I have a lot of trouble with point free style (utilizing the "." symbol). – Asaf Dec 09 '11 at 12:35
2

Another approach, just for variety! Strong use of laziness...

module Main where

nonmults :: Int -> Int -> [Int] -> [Int]
nonmults n next [] = []
nonmults n next l@(x:xs)
   | x < next = x : nonmults n next xs
   | x == next = nonmults n (next + n) xs
   | otherwise = nonmults n (next + n) l

select_primes :: [Int] -> [Int]
select_primes [] = []
select_primes (x:xs) = 
  x : (select_primes $ nonmults x (x + x) xs)

main :: IO ()
main = do
  let primes = select_primes [2 ..]
  putStrLn $ show $ primes !! 10000 -- the first prime is index 0 ...
DanK
  • 21
  • 1
1

I want to try to answer your question without using ANY functional programming or math, not because I don't think you will understand it, but because your question is very common and maybe others will benefit from the mindset I will try to describe. I'll preface this by saying I an not a Haskell expert by any means, but I have gotten past the mental block you have described by realizing the following:

1. Haskell is simple

Haskell, and other functional languages that I'm not so familiar with, are certainly very different from your 'normal' languages, like C, Java, Python, etc. Unfortunately, the way our psyche works, humans prematurely conclude that if something is different, then A) they don't understand it, and B) it's more complicated than what they already know. If we look at Haskell very objectively, we will see that these two conjectures are totally false:

"But I don't understand it :("

Actually you do. Everything in Haskell and other functional languages is defined in terms of logic and patterns. If you can answer a question as simple as "If all Meeps are Moops, and all Moops are Moors, are all Meeps Moors?", then you could probably write the Haskell Prelude yourself. To further support this point, consider that Haskell lists are defined in Haskell terms, and are not special voodoo magic.

"But it's complicated"

It's actually the opposite. It's simplicity is so naked and bare that our brains have trouble figuring out what to do with it at first. Compared to other languages, Haskell actually has considerably fewer "features" and much less syntax. When you read through Haskell code, you'll notice that almost all the function definitions look the same stylistically. This is very different than say Java for example, which has constructs like Classes, Interfaces, for loops, try/catch blocks, anonymous functions, etc... each with their own syntax and idioms.

You mentioned $ and ., again, just remember they are defined just like any other Haskell function and don't necessarily ever need to be used. However, if you didn't have these available to you, over time, you would likely implement these functions yourself when you notice how convenient they can be.

2. There is no Haskell version of anything

This is actually a great thing, because in Haskell, we have the freedom to define things exactly how we want them. Most other languages provide building blocks that people string together into a program. Haskell leaves it up to you to first define what a building block is, before building with it.

Many beginners ask questions like "How do I do a For loop in Haskell?" and innocent people who are just trying to help will give an unfortunate answer, probably involving a helper function, and extra Int parameter, and tail recursing until you get to 0. Sure, this construct can compute something like a for loop, but in no way is it a for loop, it's not a replacement for a for loop, and in no way is it really even similar to a for loop if you consider the flow of execution. Similar is the State monad for simulating state. It can be used to accomplish similar things as static variables do in other languages, but in no way is it the same thing. Most people leave off the last tidbit about it not being the same when they answer these kinds of questions and I think that only confuses people more until they realize it on their own.

3. Haskell is a logic engine, not a programming language

This is probably least true point I'm trying to make, but hear me out. In imperative programming languages, we are concerned with making our machines do stuff, perform actions, change state, and so on. In Haskell, we try to define what things are, and how are they supposed to behave. We are usually not concerned with what something is doing at any particular time. This certainly has benefits and drawbacks, but that's just how it is. This is very different than what most people think of when you say "programming language".

So that's my take how how to leave an imperative mindset and move to a more functional mindset. Realizing how sensible Haskell is will help you not look at your own code funny anymore. Hopefully thinking about Haskell in these ways will help you become a more productive Haskeller.

Community
  • 1
  • 1
parker.sikand
  • 1,371
  • 2
  • 15
  • 32