Saturday, 29 March 2008

99 problems with Python (1-10)

No, this is not a rant about problems I have with Python. I actually quite like it. Rather, it's my attempt to complete some of the Dr Werner Hett's 99 Prolog logic problems using Python, as a way of getting to learn a bit about the language. I first heard about the 99 Problems from Joel and Ryan Lanciaux's efforts to solve the problems using F#, who were in turn inspired by the Ruby implementations on the Curious Coding blog.

Keep in mind that I'm new to Python (have jumped into this exercise straight after reading the excellent Dive into Python), so much of this might be pretty clumsy. I'd love to hear any suggestions for improvements. I am also not entirely keeping with the original spirit of the Prolog exercise in terms of logic programming, as my main purpose is familiarising myself with Python. I have, however, tried to do things in a fairly Pythonic, functional-style rather than C/C++/Java/C#, imperative-style. I'm also not concerned with error handling or edge cases, just in getting the basic syntax and approach right. I'm not sure I'll get through all 99 problems, but at the very least here's the first 10.

Implementation overview

I did the exercise in Eclipse with PyDev plugin, and used the Python unittest module to implement each solution as a test to make it easy to run and verify each solution. You can obviously run all this using IDLE or whatever Python runner you like. Here's the basic file structure I started off with:

import unittest

class NinetyNineProblemsFixture(unittest.TestCase):
    
  def testXX_Description(self):
    def solutionToProblem(input): pass          
    input = "some input"
    expected = "expected output for successful implementation"    
    self.assertEqual(expected, solutionToProblem(input))  
  
if __name__ == '__main__':
  unittest.main()

I wrote a test for each problem, then implemented the solution as an inner function within the test. The if __name__ == '__main__' line lets you run the tests by simply running your *.py file through Python. I tended to run using PyTest.py from within Eclipse/PyDev.

Back to table of contents...

P01 Find the last element of a list

  def test01_FindLastElementOfList(self):
    def getLast(list):
      return list[-1]
    list = [1, 3, 7, 14]
    self.assertEqual(14, getLast(list))

Lists are great in Python. They have lots of nice features that make them very pleasant to work with. This example shows one of these features, you can use a negative index to get list items from the end of the list. For example, list[-3] will get the third last item in the list. In this case, we want the last element, so we use list[-1].

Back to table of contents...

P02 Find the last but one element of a list

Same as P01, but we want the second last item:

  def test02_FindLastButOne(self):
    def getLastButOne(list):
      return list[-2]
    list = [1, 3, 7, 14]
    self.assertEqual(7, getLastButOne(list))

Back to table of contents...

P03 Find the K'th element of a list

Basic array index for this. Lists indicies are zero-based in Python (as they should be :)), but the problem definition wants to translate the reference to 1-based.

  def test03_FindKthElement(self):
    def getKth(list, k):
      return list[k-1]
    list = [1, 3, 7, 14]
    self.assertEqual(3, getKth(list, 2))

Back to table of contents...

P04 Find the number of elements of a list

Cheating for this one and just use the built in len function:

  def test04_NumberOfElementsInList(self):
    def getCount(list):
      return len(list)
    list = [1, 3, 7, 14]    
    self.assertEqual(4, getCount(list))   

Back to table of contents...

P05 Reverse a list

Here I played around with two different approaches so as to learn a bit about Python list slicing.

Update (2 April 2008): Initially I did not realise that there was a built-in reversed() function. I originally had something silly like this:

    def reverse(l):
      newList = list(l)      
      return newList.reverse() or newList

The list.reverse function does an in-place reversal of the list elements and returns None (the Python null value), so I had to clone the list first so as not to affect the original, then OR it to return the list reference itself. Thanks to Mark et al. on Reddit for showing me the light! :)

  def test05_ReverseAList(self):
    def reverse(l):
      return list(reversed(aList))
    def manualReverse(list):          
      return list[::-1]
    sampleList = [1, 3, 7, 14]
    self.assertEqual([14, 7, 3, 1], reverse(sampleList))
    self.assertEqual([14, 7, 3, 1], manualReverse(sampleList))

The reversed() function returns an iterator, so it is wrapped in a list() constructor to convert it to a new list. I used l for the method parameter, as I couldn't figure out how to appropriately qualify references to the list class when it was hidden by a parameter called list (cue embarrassed smiley).

List slicing

The second approach was to use Python's list slicing. A slice is a range of values copied from an original list. This means we have no side effects like we do using the in place reverse(). The basic syntax for a slice is:

list[indexOfFirstElementInSlice : indexOfFirstElement_Not_InSlice : optionalStep]

Omitting the first argument starts the slice from the first list element. Omitting the second argument takes the remainder of elements in the list. The step can be set to, say, 2 to take every second list element. It's worth noticing that the second argument is exclusive, so that we can the following from the Python interpreter:

>>> aList = range(0, 6)
>>> aList
[0, 1, 2, 3, 4, 5]
>>> #From index 1 up to, but excluding, index 5:
>>> aList[1:5]
[1, 2, 3, 4]
>>> aList[1::2]
[1, 3, 5]

In our case we want the slice to include all the elements, but we want to take them in reverse order, which is how we end up with list[::-1]. Right, that was all a lot of explanation for not-much-code, but I thought I'd point out some Python basics along the way.

Back to table of contents...

P06 Find out whether a list is a palindrome

A palindrome is something that reads the same forward as it does backwards, like "Go hang a salami, I'm a lasagna hog"* (well, ignoring punctuation and spaces). I tried two different approaches for this one too.

  def test06_IsListAPalindrome(self):
    def isPalindrome(aList):
      return aList == aList[::-1]
    def isPalindromeRecursive(aList):
      if (len(aList)<=1): return True
      top, tail = aList[0], aList[-1]          
      if (top != tail): return False
      return isPalindromeRecursive(aList[1:-1])
    palindromes = [
                   ['a', 'b', 'c', 'b', 'a'],
                   [1, 10, 20, 30, 30, 20, 10, 1],
                   ['a', 'a']                    
                   ]
    nonPalindromes = [
                      ['a', 'b', 'c', 'd'],
                      [1, 10, 20, 20, 10, 2],
                      ['a', 'v']
                      ]    
    self.assertTrue(all([isPalindrome(x) for x in palindromes]))
    self.assertTrue(all([not isPalindrome(x) for x in nonPalindromes]))
    
    self.assertTrue(all([isPalindromeRecursive(x) for x in palindromes]))
    self.assertTrue(all([not isPalindromeRecursive(x) for x in nonPalindromes]))

The first way is cheating, but effective. Simply use return aList == aList[::-1] to compare the list with a reverse slice, using the same slicing syntax used for P05. This will obviously ensure the list reads the same forwards as backwards.

The second way uses recursion, as well as logical list operations to get the same effect. Let's break it down:

if (len(aList)<=1): return True

A list of 0 or 1 elements will read the same forwards as backwards, right? So yes, we have a palindrome.

top, tail = aList[0], aList[-1]          
if (top != tail): return False

Ok, now we look at first and last elements of the list (the latter of which I inconveniently named tail, which is normally used to refer to the remainder of the list besides the first element, head/tail semantics). This line also shows how you can do multiple assignments over one line in Python. You can also use a similar syntax return multiple values from one function, which we'll see later if you make it that far without getting bored :). The second line of the fragment then compares these elements -- if they are not equal then the list can't be a palindrome (e.g. 1 2 3 2 7, the 1 and 7 don't match so the list isn't a palindrome).

If the first and last elements are equal, then we have a potential palindrome, and use tail recursion to check whether a slice of the list, excluding the first and last elements, is a palindrome. Which seems to work nicely.

* A fine lesson in palindromes is available from Mr Yankovic album. You might be able to find a version somewhere without too much trouble, strictly for educational purposes.

Back to table of contents...

P07 Flatten a nested list structure

This problem just wants us to take a nested list, like [1, [2, 3], [4, 5, [6, 7]]], and flatten it out to a 1-dimensional list, like [1, 2, 3, 4, 5, 6, 7]. After coming up with a fairly ugly solution I ended up peeking at the Curious Coding solution, and adapted it to Python. This is also the first problem in the set marked as a 2-star problem, or "intermediate difficulty" (the others have all been marked as 1-star, or "easy").

  def test07_FlattenAList(self):
    def flatten(aList):
      flatList = []      
      for item in aList:
        if (type(item)==list):
          flatList.extend(flatten(item))
        else:
          flatList.append(item)          
      return flatList
    self.assertEqual(
                     [1, 2, 3, 4, 5, 6, 7], 
                     flatten([1, [2, 3], [4, 5, [6, 7]]])
                     )

Points of note from this problem is the use of the type() method (well, type is actually a "type", which, like everything in Python, is an object). This is used to check if the current element is a list. If so, the flattened list is extended by the flattened version of that sub-list (via a recursive call to flatten()). If not, the element is appended to the flatList.

The extend() method adds the items from a list to another list, whereas append() adds the item from as a single element to the list. This is probably best shown with an example:

>>> aList = range(0,6)
>>> aList
[0, 1, 2, 3, 4, 5]
>>> bList = list(aList)
>>> bList
[0, 1, 2, 3, 4, 5]
>>> anotherList
[6, 7]
>>> aList.append(anotherList)
>>> bList.extend(anotherList)
>>> aList
[0, 1, 2, 3, 4, 5, [6, 7]]
>>> bList
[0, 1, 2, 3, 4, 5, 6, 7]

Back to table of contents...

P08 Eliminate consecutive duplicates of list elements

This is another 2-star, intermediate problem, but it was pretty easy to whip through using Python's list comprehensions. Let's have a quick look at the list comprehension syntax first:

[itemInNewList for itemInNewList in someList if someConditionIsMet]

The square braces ([, ]) indicate we are creating a new list. In the new list we will include itemInNewList as an element, for the itemInNewList values in someList (or other iterable), that meet someConditionIsMet (the if someConditionIsMet is optional). Quick example:

>>> aList = range(0,6)
>>> aList
[0, 1, 2, 3, 4, 5]
>>> [item for item in aList if item < 3]
[0, 1, 2]

All a bit LINQ-like to me (well, more correctly LINQ is inspired by features in functional programming, such as monads [PDF]).

Back to problem 8, the aim is to eliminate consecutive duplicates from a list. So we would like to create a new list by iterating over a source list, and only including elements that are not the same as the previous element.

  def test08_EliminateConsecutiveDuplicates(self):
    def compress(aList):
      return [item for index, item in enumerate(aList) if index==0 or item != aList[index-1]]
    sampleList = ['a','a','a','a','b','c','c','a','a','d','e','e','e','e']
    self.assertEqual(
                     ['a','b','c','a','d','e'],
                     compress(sampleList)                     
                     )

The only tricky bit with this list comprehension compared with the trivial example given above is the use of the enumerate(aList) function which returns two values per iteration, the item and the index of that item. We use these variables in the list comprehension condition to check whether this is the first list item (which we want to include), or if the list item duplicates the previous one (which we want to exclude).

I think that's awesome :)

Back to table of contents...

From comments: avoiding enumerate()

Arnar (Blogger profile link) left a nice solution for this problem in the comments that doesn't require the use of enumerate(). I've reproduced it here (updating the naming convention to match the example above):

    def compress(aList):
        return aList[:1] + [aList[i] for i in range(1, len(aList)) if aList[i-1] != aList[i]]

This first creates a list containing only the head element (aList[:1]), and concatenates it with a list comprehension that eliminates duplicates. Rather than using enumerate() to get the index and item, Arnar used range(1, len(aList)) (which I have blogged about before) to generate the relevant indexes and then accesses the items direct from the list.

The nice thing about this approach is that by starting with aList[:1], we simplify the list comprehension condition I originally had (if index==0 or item != aList[index-1]). I'm not sure if there is a clear advantage of using enumerate() or range() to iterate over, but it definitely gave me another way of thinking about the problem. Thanks Arnar! :)

From comments: using itertools.groupby

This example added 3 April 2008

I had a number of helpful comments suggesting I use the itertools standard Python library for a number of these examples. Thanks to Arnar, Niall, Mohammad and everyone else that suggested this and wrote in with examples. I originally intended to do this exercise without using any imports (well, except for unittest), but Arnar managed to convince me to stop being so silly. :)

The particular itertools function we are looking at is groupby(iterable[, key]). This function returns a list of tuples. The first item in the tuple is the key for a particular group, and the second is an iterator over the group itself (i.e. (key, group)). So how are keys specified? By default, it is the item's identity or value (so if you group [1, 1, 1, 2, 3], you will get a group of 1s, like this:

>>> from itertools import groupby
>>> aList = [1,1,1,2,3]
>>> [list(group) for key, group in groupby(aList)]
[[1, 1, 1], [2], [3]]
>>> [key for key, group in groupby(aList)]
[1, 2, 3]

Note we have to import the function from the itertools library first using from itertools import groupby (Dive Into Python has a great explanation about how to import stuff). As the group returned is an iterator over a group, we can turn it into a list using the constructor list(group). The last command issue also shows what keys groupby() finds when no key argument is supplied. We can also provide groupby() with a function used to calculate keys:

>>> aList
[1, 1, 1, 2, 3]
>>> [list(group) for key, group in groupby(aList, lambda x: x<3)]
[[1, 1, 1, 2], [3]]

Here we use a simple lambda function to sort the list into groups: those less than 3, and not. :) Armed with just enough knowledge to be dangerous, let's try and solve problem 8 again. To eliminate consecutive duplicates, all we really want to do is get the keys from the list:

  def test08_EliminateConsecutiveDuplicates(self):
    def compress(aList):        
        return [key for key, group in groupby(aList)]
    sampleList = ['a','a','a','a','b','c','c','a','a','d','e','e','e','e']
    self.assertEqual(['a','b','c','a','d','e'], compress(sampleList))

And this passes nicely. But hold on a minute! How come we have duplicated keys in there? There are to 'a' elements! I said the default keys were each item's identity, and they are both 'a'! Why are you lying Dave? WHY?!?!

Ok, I'm better now. The reason is that groupby() is implemented using an iterator. It goes through the list picking out items in the 'a' group, then hits 'b'. This has a different identity, and so starts a new group. The function's iterator has now moved past the initial group of 4 'a' elements, and has basically completely forgotten about them, so the next time it hits an 'a' it creates a new group. Clear as mud? Check out the documentation and it should clear things up ( worked for me :) )

Back to table of contents...

P09 Pack consecutive duplicates of list elements into sublists

Another 2 star problem, this one wants us to convert consecutive duplicates in a list into a sub-lists. The original problem says something about only packing duplicates if list contains duplicates, but the example and implementations I have seen do not seem to do this, so I'll follow convention of blindly packing each element into a sub-list.

  def test09_PackDuplicatesIntoSubLists(self):
    def pack(aList):
      packedList = []
      for index, item in enumerate(aList):
        if index==0 or item != aList[index-1]:
          packedList.append([item])
        else:  
          packedList[-1].append(item)
      return packedList
    
    sampleList = ['a','a','a','a','b','c','c','a','a','d','e','e','e','e']
    self.assertEqual(
                     [['a','a','a','a'],['b'],['c','c'],['a','a'],['d'],['e','e','e','e']],
                     pack(sampleList)                     
                     )

I couldn't find a nice way of doing this list comprehension style, so I resorted to a simple imperative operation. We start with an empty packedList, and use the nice enumerate() that was so useful in problem 8 to iterate over the source list. If this is the first element, or if this element is not duplicating the previous item, we will append a one element, sub-list to packedList (packedList.append([item])). This means that every element of packedList will be a list itself. This is important, because the else branch of this condition relies on this fact to simply add duplicate elements to the previous sub-list (packedList[-1].append(item)).

At this point I'm seriously loving how easy it is to use Python lists: slicing, list comprehensions, negative indexing etc. Test passes, so it's off to our final question for this set.

From comments: another itertools.groupby alternative

This example added 3 April 2008.

As noted in the follow ups to problem 8, a number of commenters pointed me to the groupby() function (see problem 8 for an explanation). We can use it here too, but this time we want the groups rather than the keys:

  def test09_PackDuplicatesIntoSubLists(self):
    def pack(aList):
        return [list(group) for key, group in groupby(aList)]
    sampleList = ['a', 'a', 'a', 'a', 'b', 'c', 'c', 'a', 'a', 'd', 'e', 'e', 'e', 'e']
    self.assertEqual(
                     [['a','a','a','a'], ['b'], ['c','c'], ['a','a'], ['d'], ['e','e','e','e']],
                     pack(sampleList)                     
                    )

Back to table of contents...

P10 Run-length encoding of a list

This problem relates to problem 9, but instead of showing multiple elements in each sub-list, we just want a count of how many duplicates there are. Let's have a look at the test assertion to make this clearer:

    ...
    sampleList = ['a','a','a','a','b','c','c','a','a','d','e','e','e','e']
    self.assertEqual(
                     [[4,'a'],[1,'b'],[2,'c'],[2,'a'],[1,'d'],[4,'e']],
                     encode(sampleList)
                     )

First I tackled this the easy way, using copy-and-paste reuse from problem 9 and changing the action on each branch. The implementation is show below, with differences from problem 9 emphasised:

    def encode(aList):
      encodedList = []
      for index, item in enumerate(aList):
        if index==0 or item != aList[index-1]:
          encodedList.append([1, item])
        else:  
          encodedList[-1][0] += 1
      return encodedList

Here, rather than appending to sub-lists all the time, we are keeping a structure that contains the number of duplicates and the item being duplicated in each list. That worked, passing the test show above, but I also tried a less-objectional form of reuse of the pack() function from P09 by calling it from a list comprehension:

    def encode2(aList):
      return [[len(packed), packed[0]] for packed in pack(aList)]    

This list comprehension is making a new list from every sub-list returned by the pack() function. Each item is a two item list ([len(packed), packed[0]]). The first item is the length of the sub-list (i.e. how many duplicates we have), and the second is the first item of the sub-list (i.e. the item being duplicated). Here is the complete test:

  def test10_RunLengthEncodeList(self):    
    def encode(aList):
      encodedList = []
      for index, item in enumerate(aList):
        if index==0 or item != aList[index-1]:
          encodedList.append([1, item])
        else:  
          encodedList[-1][0] += 1
      return encodedList
    def pack(aList):
      packedList = []
      for index, item in enumerate(aList):
        if index==0 or item != aList[index-1]:
          packedList.append([item])
        else:  
          packedList[-1].append(item)
      return packedList
    def encode2(aList):
      return [[len(packed), packed[0]] for packed in pack(aList)]
      
    sampleList = ['a','a','a','a','b','c','c','a','a','d','e','e','e','e']
    self.assertEqual(
                     [[4,'a'],[1,'b'],[2,'c'],[2,'a'],[1,'d'],[4,'e']],
                     encode(sampleList)
                     )
    self.assertEqual(
                     [[4,'a'],[1,'b'],[2,'c'],[2,'a'],[1,'d'],[4,'e']],
                     encode2(sampleList)
                     )   

From comments: yet another itertools.groupby alternative

This example added 3 April 2008.

As noted in the follow ups to problem 8 and problem 9, I really should have been using the groupby() function for some of these problems (see here for an explanation).

  def test10_RunLengthEncodeList(self):    
    def encode3(aList):
      return [[len(list(group)), key] for key, group in groupby(aList)]
    sampleList = ['a','a','a','a','b','c','c','a','a','d','e','e','e','e']
    self.assertEqual(
                     [[4,'a'], [1,'b'], [2,'c'], [2,'a'], [1,'d'], [4,'e']],
                     encode3(sampleList)
                    )

This is pretty similar in form to the second approach used (encode2()), but without having to call pack first.

Back to table of contents...

Conclusion

Because all the problems so far revolved around lists, an area in which Python excels, I think all of these implementations came out quite well, especially considering I have no idea what I am doing when it comes to Python (or at all, some might argue!).

Looking at the Ruby implementations, there are a number of similarities between the Python versions I came up with. To be fair, I did peek at some of the Ruby solutions for hints in keeping to a functional programming style, but my main point here is that the language differences for basic stuff seem fairly superficial. I found both the Ruby versions and the Python versions very easy to understand.

Perhaps because it has been many, many years since I worked with Haskell, I found the F# samples from the Lanciaux brothers much more difficult to understand. The language semantics are just so different. Although all of them seem more natural to me than the original Prolog solutions :-) Take a look at the solutions to problem 8 and see if you agree:

All in all I had a thoroughly good time using Python for this exercise, and will definitely be looking for any excuse to use it again in future. At least for this basic stuff, the language just seems so concise and easy express your intention through the syntax. If you have suggestions for improvements or find any bugs with this please leave a comment or send me an email (tchepak at gmail). [UPDATE: Thanks to all those who have helped out with comments so far!]

Back to table of contents...

Change log

  • 2008-04-03: Added itertools.groupby() versions of problems 8, 9, 10 after some more helpful comments. Added this change log.
  • 2008-04-01: Added non-enumerate version for problem 8 after getting some helpful comments.


Share/Save/Bookmark

22 comments:

Arnar Birgisson said...

Another way to remove dupes without using enumerate:

def remove_dupes(x):
....return x[:1] + [x[i] for i in range(1,len(x)) if x[i-1] != x[i]]

(change dots to spaces, blogger.com is dumb :)

Arnar Birgisson said...

One-liner for P09:

from itertools import groupby

def group_consecutive(l):
....[list(group) for key,group in groupby(l)]

Arnar Birgisson said...

Sorry for spamming, but that can be used for P10 as well:

from itertools import groupby

def runlength_encode(l):
....return [(len(l),l[0]) for l in [list(group) for key,group in groupby(l)]]

And of course there was a missing "return" in the group_consecutive function above. :)

David said...

Thanks Arnar! It's not spamming -- they'll all helpful comments and much appreciated :)

I have added your remove dupes example to the post. Hope that's ok. :)

I've heard about the itertools module before in relation to these problems, but I was aiming to do them without imports for now. Still, it's good to see your examples of how much simpler groupby makes things.

I've barely scratched the surface of the standard Python modules in my Python experiments so far. I think I need to spend an evening or three going through the module documentation. :)

Arnar Birgisson said...

> I have added your remove dupes example to the post. Hope that's ok. :)

Of course :)

About avoiding imports, that is one of the most significant eye-opener when using dynamic languages, that you often get a huge standard library practically for free. One blog post which stirred up some discussion on this is here.

Getting to know the standard library is vital to become really good with Python. There is really no need to avoid modules such as itertools, re etc., they are usually heavily optimized and even written in C so in many cases your code will both be cleaner and faster.

David said...

@Arnar,
Good point about the imports and standard libraries.

My intention was to make sure I knew the language basics before going on to the library basics, but thinking about it again, ignoring the libraries is going to stunt my progress in learning Python a fair bit :). So I'll go back and update the post to show both (and learn a bit about itertools in the process).

I'll leave a comment when I update it in case you want to stop by again, but I'll basically just re-print your examples and add my own twisted explanation as to how it works.

Thanks for taking the time to comment :)

tef said...

Hi, your solution to problem 3 does not work like the prolog solution:

| ?- element_at(1,[1,2,3],X).

X = 1 ?

yes
| ?- element_at(Z,[1,2,3],3).

Z = 3 ?

You can specify the index or the element and it will unify the other.

Paolo Bonzini said...

Here is how to solve P09 without using enumerate. It is based on arnar's solution for remove_dupes:

l = [ 1, 2, 2, 3, 2, 2, 3, 3, 4, 5 ]
bottom = [i for i in range(0, len(l)) if i == 0 or l[i-1] != l[i]]
top = bottom[1:] + [len(l)]
[l[bottom[i]:top[i]] for i in range(0, len(bottom))]

>>> [[1], [2, 2], [3], [2, 2], [3, 3], [4], [5]]

Paolo Bonzini said...

and this gives P10:

[(l[bottom[i]],top[i]-bottom[i]) for i in range(0, len(bottom))]

Niall Glynn said...

A nicer approach to P10:

[(len(list(y)),x) for (x,y) in itertools.groupby(['a','a','a','a','b','c','c','a','a','d','e','e','e','e'])]

Mohammad Tayseer said...

You can use groupby for solving P08 too

>>> [x for x, y in groupby([1, 2, 1, 1, 1, 2, 3, 4, 5, 5, 6, 6, 6])]

[1, 2, 1, 2, 3, 4, 5, 6]

David said...

@tef,
Your comment about P03 got me to download a Prolog version to try it out :)

Do you mean you can specify either an element or an index, and Prolog will infer the other value? I'm probably missing something obvious, but I couldn't get the Prolog version to work quite like that. This works:

| ?- element_at(1,[1,2,3],X).
X = 1 ?
yes


But this doesn't:

| ?- element_at(2,[1,2,3],X).
uncaught exception: error(instantiation_error,(>)/2)


Where I would have expected X=2, as 2 is the 2nd list element.

Anyway, I think my implementation answers the original problem statement, even if the behaviour of the implementations differ. The Prolog implementation is clearly aimed at finding the element at a specific index (if index is 1, element is the current head, else recursively call with the tail of the list and index-1), but I don't know enough about Prolog to understand why it doesn't work to get the index given an element.

If you know please leave a comment. Thanks for making me think it through :)

David said...

Hi @Paolo,
What's everyone got against enumerate(), anyway? :) Thanks for the alternative implementation.

@Niall and @Mohammad,
Thanks for the itertools suggestions. @Arnar's earlier comments have convinced me to take a look groupby, and I'll update the post when done, dutifully stealing your examples as appropriate. :)

Appreciate everyone's comments :)

Greg Buchholz said...

For example #3, if instead you wrote the Prolog program like:

element_at(X,[X|_],1).
element_at(X,[_|L],K) :- element_at(X,L,K1), K is K1 + 1 .

...Then you can use it in more modes. For finding elements:

?- element_at(X,[a,b,c,d],2).

X = b

Yes

...for finding positions:

?- element_at(c,[a,b,c,d],X).

X = 3

Yes

...for checking:

?- element_at(c,[a,b,c,d],3).

Yes

...for generating lists:

?- element_at(c,X,3).

X = [_G250, _G253, c|_G257]

Yes

?- element_at(X,Y,Z).

Y = [X|_G281],
Z = 1 ;

Y = [_G280, X|_G284],
Z = 2 ;

Y = [_G280, _G283, X|_G287],
Z = 3

...for pairing up elements with their position:

?- element_at(X,[a,b,c,d],Z).

X = a,
Z = 1 ;

X = b,
Z = 2 ;

X = c,
Z = 3 ;

X = d,
Z = 4 ;

No

David said...

Thanks @Greg! That's awesome :), and how I thought the original solution would work after reading @tef's comment.

If you'll excuse my ignorance, can you explain why your version works that way, while the original doesn't?

Original:
element_at(X,[X|_],1).
element_at(X,[_|L],K) :- K > 1, K1 is K - 1, element_at(X,L,K1).


Greg's:
element_at(X,[X|_],1).
element_at(X,[_|L],K) :- element_at(X,L,K1), K is K1 + 1.


I'm guessing it's to do with the how Prolog handles K is K1 + 1 vs. K1 is K -1?

David said...

I've updated this post to add a couple of itertools.groupby examples too (for P08, P09, P10). Thanks to everyone for their feedback :)

el-tramo.be said...

Once you're done with these, it would be quite challenging to solve the puzzles from The First 10 Prolog Programming Contests.

David said...

@el-tramo.be,
Thanks for the link. I like to have a couple of exercises lying around for programming practice. :)

Greg Buchholz said...

For efficiency reasons, Prolog's operators on numbers (the "is" predicate, ">", etc.) are non-logical, so you can't use them backwards, and you can't supply them with un-instantiated variables. See Chapter 8 of the Art of Prolog. But your Prolog probably has a library for doing contraint programming, which fixes this (to a certain degree). CLP(R) is available as a library for SWI. Then you can write stuff like {8 = 2^X}., and it'll tell you that X is 3.

David said...

@Greg,
Thanks for taking the time to explain it and for the links.

After programming solely in imperative languages for so long (since university), I'm finding functional and logical languages really interesting :)

Cheers,
David

Anonymous said...

Problem 8 with zip():
>>> li = ['a','a','a','a','b','c','c','a','a','d','e','e','e','e']
>>> [li[0]] + [b for a, b in zip(li[:], li[1:]) if b != a]
['a', 'b', 'c', 'a', 'd', 'e']

David said...

Thanks Anonymous :) I found that one a bit harder to decode than the versions currently posted (had to fire up IDLE and see what it was doing :$)

Good to know about zip() though.