Notes on codes, projects and everything

The Little Pythoner, Part 3

Previously, I started practising recursions by implementing a type check on lat (list of atoms), and ismember (whether an atom is a member of a given lat). Then in the third chapter, named “Cons the Magnificent”, more list manipulation methods are being introduced.

The chapter started with the definition of rember, which is an abbreviation of ‘remove a member’. In other words, by providing an atom and a lat, the function returns a new lat, with the given atom removed from the original lat.

from primitives import *
from chap2 import *

def test_rember():
    test_cases = (('mint',
                   ('lamb', 'chops', 'and', 'mint', 'jelly'),
                   ('lamb', 'chops', 'and', 'jelly')),
                  ('mint',
                   ('lamb', 'chops', 'and', 'mint', 'flavored', 'mint', 'jelly'),
                   ('lamb', 'chops', 'and', 'flavored', 'mint', 'jelly')),
                  ('toast',
                   ('bacon', 'lettuce', 'and', 'tomato'),
                   ('bacon', 'lettuce', 'and', 'tomato')),
                  ('cup',
                   ('coffee', 'cup', 'tea', 'cup', 'and', 'hick', 'cup'),
                   ('coffee', 'tea', 'cup', 'and', 'hick', 'cup')),
                  ('and',
                   ('bacon', 'lettuce', 'and', 'tomato'),
                   ('bacon', 'lettuce', 'tomato')),
                  ('sauce',
                   ('soy', 'sauce', 'and', 'tomato', 'sauce'),
                   ('soy', 'and', 'tomato', 'sauce')))

    for a, lat, expected in test_cases:
        result = rember(a, lat)
        assert result == expected

def rember(a, lat):
    return (() if isnull(lat)
            else cdr(lat) if iseq(a, car(lat))
            else cons(car(lat), rember(a, cdr(lat))))

Just in case it wasn’t explained clearly in the previous post. The first thing to define in a recursive function is always the exit condition. In this case whether the lat is an empty list. Considering we are expecting a list from this function, it should then return an empty list to be constructed with the previous results (the cdr of a list with one element is always an empty list).

Also in this case we are removing only the first occurence of the given atom. So if we eventually encounter a car of the given lat being equal, then we would return the cdr of the lat (means excluding the car of the lat, as it is equal to the given atom). Lastly, if the given lat does not fall in the either condition, we shall construct a new lat with the car, and the result of rember of the cdr.

Next we are given the introduction of firsts. So given a list of lats, we are interested in the first element in each lat.

def test_firsts():
    test_cases = (((('apple', 'peach', 'pumpkin'),
                    ('plum', 'pear', 'cherry'),
                    ('grape', 'raisin', 'pea'),
                    ('bean', 'carrot', 'eggplant')),
                   ('apple', 'plum', 'grape', 'bean')),
                  ((('a', 'b'),
                    ('c', 'd'),
                    ('e', 'f')),
                   ('a', 'c', 'e')),
                  ((), ()),
                  ((('five', 'plums'),
                    ('four', ),
                    ('eleven', 'green', 'oranges')),
                   ('five', 'four', 'eleven')),
                  (((('five', 'plums'), 'four'),
                    ('eleven', 'green', 'oranges'),
                    (('no', ), 'more')),
                   (('five', 'plums'), 'eleven', ('no', ))))

    for l, expected in test_cases:
        result = firsts(l)
        assert result == expected
        

def firsts(l):
    return (() if isnull(l)
            else cons(car(car(l)), firsts(cdr(l))))

Practically, it is almost the same as the previous rember function. Except that there’s no extra condition besides checking if the lat is an empty list. So the function consumes one lat at a time from the given list (car of the given list), and construct a new lat by taking only the car of each lat.

Next, we are tyring to do something opposite of rember, which is the insertR function, where we are attempting to insert a new atom after a given atom, and return a new lat.

def test_insertR():
    test_cases = (('topping',
                   'fudge',
                   ('ice', 'cream', 'with', 'fudge', 'for', 'dessert'),
                   ('ice', 'cream', 'with', 'fudge', 'topping', 'for', 'dessert')),
                  ('jalapeno',
                   'and',
                   ('tacos', 'tamales', 'and', 'salsa'),
                   ('tacos', 'tamales', 'and', 'jalapeno', 'salsa')),
                  ('e',
                   'd',
                   ('a', 'b', 'c', 'd', 'f', 'g', 'h'),
                   ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h')))

    for new, old, lat, expected in test_cases:
        result = insertR(new, old, lat)
        assert result == expected


def insertR(new, old, lat):
    return (() if isnull(lat)
            else cons(car(lat),
                      cons(new, cdr(lat)) if iseq(old, car(lat))
                      else insertR(new, old, cdr(lat))))

Just as the other functions, we start by defining the exit condition. Also since we are dealing with lists here, we always return an empty list, to be a part of cons at the end of execution. Then things start to get complicated. However, as we are always going to insert a new element to the right of the found element, we would then construct a new lat, with the same car value.

Next, the cdr of the new list, is defined by checking if the car value is equal to the old function parameter. If it is the case, we construct a new list as the cdr of original. This new list will have new function parameter as the car, and the cdr being the second parameter of the cons call.

However, if the check is failed, we recurse the insertR function with cdr of original lat. Also notice, just like rember, we are only doing this once, for now.

Next, we try insertL

def test_insertL():
    test_cases = (('topping',
                   'fudge',
                   ('ice', 'cream', 'with', 'fudge', 'for', 'dessert'),
                   ('ice', 'cream', 'with', 'topping', 'fudge', 'for', 'dessert')),
                  ('jalapeno',
                   'and',
                   ('tacos', 'tamales', 'and', 'salsa'),
                   ('tacos', 'tamales', 'jalapeno', 'and', 'salsa')),
                  ('e',
                   'd',
                   ('a', 'b', 'c', 'd', 'f', 'g', 'h'),
                   ('a', 'b', 'c', 'e', 'd', 'f', 'g', 'h')))

    for new, old, lat, expected in test_cases:
        result = insertL(new, old, lat)
        assert result == expected


def insertL(new, old, lat):
    return (() if isnull(lat)
            else cons(new, lat) if iseq(car(lat), old)
            else cons(car(lat), insertL(new, old, cdr(lat))))

While it should be similar to insertR, but the code looks much simpler. First of everything we have the exit condition, as usual. Then we check if the car of the given lat is equal to the given old parameter. Considering we are now inserting new element to the left of matched atom, we just need to construct the new list by putting new parameter as car, and the original lat as cdr.

However, if there’s no match, we send the cdr of original lat to insertL again, then construct the new list by combining it with the car of original lat.

Next we have substitution, which is the subst function. Instead of removing, or inserting a new element, we substitute an atom with another from a given lat.

def test_subst():
    test_cases = (('topping',
                   'fudge',
                   ('ice', 'cream', 'with', 'fudge', 'for', 'dessert'),
                   ('ice', 'cream', 'with', 'topping', 'for', 'dessert')),
                  ('jalapeno',
                   'and',
                   ('tacos', 'tamales', 'and', 'salsa'),
                   ('tacos', 'tamales', 'jalapeno', 'salsa')),
                  ('e',
                   'd',
                   ('a', 'b', 'c', 'd', 'f', 'g', 'h'),
                   ('a', 'b', 'c', 'e', 'f', 'g', 'h')))

    for new, old, lat, expected in test_cases:
        result = subst(new, old, lat)
        assert result == expected

def subst(new, old, lat):
    assert isatom(new) and isatom(old) and islat(lat)

    return (() if isnull(lat)
            else cons(new, cdr(lat)) if iseq(old, car(lat))
            else cons(car(lat), subst(new, old, cdr(lat))))

Skipping the exit condition, as it is always the same in this chapter. If we are observing close enough subst looks very similar to insertL. The only difference happens only when the car of lat matches the old parameter. This time, the second parameter to cons, is the cdr of original list, i.e. original list without the car element.

Then we have subst2, which does replace for either one of two cases.

def test_subst2():
    test_cases = (('vanilla',
                   'chocolate',
                   'banana',
                   ('banana', 'ice', 'cream', 'with', 'chocolate', 'topping'),
                   ('vanilla', 'ice', 'cream', 'with', 'chocolate', 'topping')), )

    for new, o1, o2, lat, expected in test_cases:
        result = subst2(new, o1, o2, lat)
        assert result == expected
        
        
def subst2(new, o1, o2, lat):
    return (() if isnull(lat)
            else cons(new, cdr(lat)) if or_(iseq(o1, car(lat)), iseq(o2, car(lat)))
            else cons(car(lat), subst(new, o1, o2, cdr(lat))))

Nothing really new here, except we have a boolean OR operation here to handle the ‘either .. or ..’ case here.

Next, we shall look at how each of the functions above construct a new list (without excessive car and cdr calls for clarity).


# Given these parameters
lat = ('lorem', 'ipsum', 'dolor', 'sit')
old, old2 = 'ipsum', 'amet'
new = 'meow'

# rember: remove an element
print(cons(lat[0],
           cons(lat[2],
                cons(lat[3],
                     ())))) # prints ('lorem', 'dolor', 'sit')

# insertR: insert a new atom to the right of the matched atom
print(cons(lat[0],
           cons(lat[1],
                cons('meow',
                     lat[2:])))) # prints ('lorem', 'ipsum', 'meow', 'dolor', 'sit')

# insertL: insert a new atom to the left of the matched atom
print(cons(lat[0],
           cons('meow',
                lat[1:]))) # prints ('lorem', 'meow', 'ipsum', 'dolor', 'sit')

# subst/subst2: substitute the matched atom with a new atom
print(cons(lat[0],
           cons('meow',
                lat[2:]))) # prints ('lorem', 'meow', 'dolor', 'sit')

However, chances are we may want the functions to apply the operations multiple times. So instead of removing only the first member, we can expand rember into multirember as follows:

def test_multirember():
    test_cases = (('cup',
                   ('coffee', 'cup', 'tea', 'cup', 'and', 'hick', 'cup'),
                   ('coffee', 'tea', 'and', 'hick')),)

    for a, lat, expected in test_cases:
        result = multirember(a, lat)
        assert result == expected
        
        
def multirember(a, lat):
    return (() if isnull(lat)
            else multirember(a, cdr(lat)) if iseq(a, car(lat))
            else cons(car(lat), multirember(a, cdr(lat))))

Practically nothing much is changed. Except the function call at the end is changed (because the function name itself is changed), the only notable change happens only when a matching atom is found. Instead of returning the cdr and call it a day, we recurse the function with the cdr of original lat.

Then we move on to multiinsertR,

def test_multiinsertR():
    test_cases = (('fried',
                   'fish',
                   ('chips', 'and', 'fish', 'or', 'fish', 'and', 'fried'),
                   ('chips', 'and', 'fish', 'fried', 'or', 'fish', 'fried', 'and', 'fried')),)

    for new, old, lat, expected in test_cases:
        result = multiinsertR(new, old, lat)
        assert result == expected

        
def multiinsertR(new, old, lat):
    return (() if isnull(lat)
            else cons(car(lat),
                      cons(new, multiinsertR(new, old, cdr(lat))) if iseq(old, car(lat))
                      else multiinsertR(new, old, cdr(lat))))

It is very similar to the original insertR. The only difference compared to the original again happens when a match is found. Instead of passing the cdr to the second argument of the nested cons, we send it for recursion.

Next we have multiinsertL,

def test_multiinsertL():
    test_cases = (('fried',
                   'fish',
                   ('chips', 'and', 'fish', 'or', 'fish', 'and', 'fried'),
                   ('chips', 'and', 'fried', 'fish', 'or', 'fried', 'fish', 'and', 'fried')),)

    for new, old, lat, expected in test_cases:
        result = multiinsertL(new, old, lat)
        assert result == expected

        
def multiinsertL(new, old, lat):
    return (() if isnull(lat)
            else cons(new, cons(old, multiinsertL(new, old, cdr(lat)))) if iseq(old, car(lat))
            else cons(car(lat), multiinsertL(new, old, cdr(lat))))

However, it is not so simple in multiinsertL. Firstly when a match happens, compared to the original insertL we do not have the cdr of original lat to deal with. Recursing the function with original lat naïvely will result in a infinite loop. So the proper way to do this is to firstly break the lat into cat and cdr pair. Then throw the cdr for recursion, before combining its result with the original cat with cons.

Last but not least, the multisubst function,

def test_multisubst():
    test_cases = (('fried',
                   'fish',
                   ('chips', 'and', 'fish', 'or', 'fish', 'and', 'fried'),
                   ('chips', 'and', 'fried', 'or', 'fried', 'and', 'fried')),)

    for new, old, lat, expected in test_cases:
        result = multisubst(new, old, lat)
        assert result == expected
        
        
def multisubst(new, old, lat):
    return (() if isnull(lat)
            else cons(new, multisubst(new, old, cdr(lat))) if iseq(old, car(lat))
            else cons(car(lat), multisubst(new, old, cdr(lat))))

Compared to the original subst function, there’s no much change here. The cdr call in the matching case is replaced with the recursive call. However, if we read the code again carefully, the second part of the cons calls for either matching or not matching case are both identical. Therefore we can rewrite the function as follows for clarity

def multisubst(new, old, lat):
    return (() if isnull(lat)
            else cons(new if iseq(old, car(lat)) else car(lat),
                      multisubst(new, old, cdr(lat))))

So this concludes the chapter for 1 dimensional list manipulation through recursion.

leave your comment

name is required

email is required

have a blog?

This blog uses scripts to assist and automate comment moderation, and the author of this blog post does not hold responsibility in the content of posted comments. Please note that activities such as flaming, ungrounded accusations as well as spamming will not be entertained.

Click to change color scheme