In which: Our intrepid author attempts to find some use for the FP library he has been toiling on for over four hours.

I’m quite glad that I wrote about the Library Trap yesterday, that common phenomenon where the library you find has very little that you actually need, and plenty that you don’t. I only wish I had stated it more vividly.

Yesterday after the session, I had a meeting via computer and since I was only slightly engaged in the meeting, I took a glance at the extended set application to find where the FP functions would be useful. To my dismay, if not surprise, I found very few. There are some tests that can be made a bit simpler, and probably a couple of iterative things in the actual code, but very few. Now the good news is that the main reason there are few opportunities in XSet2 is that most of its iterative operations are on instances of XSet, extended sets, and those already have all the fp-style operations I felt I needed. There are very few iterations of simple arrays or dictionaries in XSet2.

There are a few, and those did show a deficiency in the fp project, at least as seen through the eyes of someone writing the XSet2 project. Since extended sets are explicitly composed of elements under scopes, the iterations that one does almost invariably want to consider both the element and the scope, or, in programming terms, the key and the value. The fp functions, as written, do not expose the index or key, only the value.

That should be easy to change, although it does complicate the writing of the inner functions that are used to turn the fp functions to specific use. For example, consider the map function. Here’s a snippet from our test for map:

local double = function(v) return 2*v end
local input = {12,20}
local ra = ar.map(input,double)
-- result = {24,40}

Once we expose both k and v, we’d have to write the function to expect both, and–I strongly believe–to return both, since the user might want to transform the key. So the simple double function becomes something like this:

local double = function(k,v) return k, 2*v end

Simple enough, one thinks: just add the key parameter, and, in general, return it as is. Except, of course, that we so rarely write functions that return two values, we’ll often forget. I think there’s nothing for it, we’ll just have to do it.

Now, we might be able to do something tricky. For example, we could pass v first and then k. That’s the reverse of what anyone programming in lua would expect, but it would mean we could ignore the key parameter. If we reverse them in, we should reverse them out, so if you don’t return a second value, we could assume the same key as went in:

newV, newK = f(v,k)
-- work with `newV` and `newK or k`

Frankly, I think that’s too clever for its pants. Too much secret stuff going on behind the scenes. Yes, it’s cute that the keys appear if you want them and if you don’t you don’t have to look at them. But reversing the well-known Lua order, and doing magic on the returns …

We had a rule on the C3 project that if anyone ever wrote some really clever Smalltalk code and said “Hey, look at this!”, we’d delete the code. I don’t think the rule ever had to be enforced, because just stating it made us clear that clever code was not our friend. That’s what I’m concerned about here.

Still, if we were to do it that way, in almost all applications, the user wouldn’t even have to think about the keys, but if they ever did want keys, they’re right there, and if they even wanted to change the keys, that’s easy as well.

I am falling in love with this cleverness. And there’s no one here to delete what I’m doing. And, in fairness, I haven’t done it … yet.

However …

Even if I did provide that clever access to the indexes and keys, there would still be very few places in XSet2 where these functions would be useful. Some tests, yes, and that would be nice. Otherwise … let’s look:

function XSet:record(tab)
    result = XSet()
    for scope,element in pairs(tab) do
        result:addAt(tostring(element),tostring(scope))
    end
    return result
end

That could be rewritten:

function XSet:record(tab)
    local f = function(result,element,scope)
        return result:addAt(tostring(element),tostring(scope))
    end
    return tab:reduce(XSet(),f)
end

I wouldn’t want to debate whether that’s better. Looking further …

function XStatistics:sumFunction()
    return function(input, accumulator, control)
        for _i,field in ipairs(control.sum) do
            local accum = accumulator:at(field) or 0
            accum = accum + input:at(field) or 0
            accumulator:putAt(tostring(accum),field)
        end
    end
end

Here we’re fetching the accumulated value inside the loop, adding to it, and jamming it back. I don’t see a big improvement to be made there.

Two strikes, and I’ve been choosing candidates that look at least possible. I’ve skipped over innumerable less likely ones that are using XSet iterators already.

At a pretty careful glance, those are the only two even somewhat credible opportunities to use my fp stuff in XSet2, other than possibly to simplify some tests. Even there, it’s far from clear.

Let’s look at the Dungeon program, D2

Dungeon

I’ll just pull out a few things that look possible.

    local result = {}
    for i,e in ipairs(roomTable) do
        table.insert(result, Room(e[1],e[2],e[3],e[4], self, true))
        if annTable[i] then
            result[#result]:announce(dungeon, annTable[i])
        end
    end
    return result

The first bit there could be a reduce. I think we’d have to do two loops, which might be OK … the second bit needs a function we don’t have, apply, which is like map without saving the results.

    for i,b in ipairs(self.buttons) do
        b:draw()
    end

No help there …

function Monsters:createHangoutMonsters(runner, count, tile)
    local tab = Monster:getHangoutMonsters(runner, count, tile)
    for i,m in ipairs(tab) do
        table.insert(self.table, m)
    end
end

I don’t think we can make any hay with that. We could use map to create a new table but we’re adding to an existing one in this case. Lua doesn’t have a table.append, as far as I know. We could do it with reduce (you can do most anything with reduce) but the function that we’d need would be weird. The code as written is clear and not notably error-prone.

function Monsters:hasMonsterNearPlayer(range)
    for i,m in ipairs(self.table) do
        if m:isActive() and m:manhattanDistanceFromPlayer() <= range then return true end
    end
    return false
end

A clear candidate for exists. We’d have, roughly:

function Monsters:hasMonsterNearPlayer(range)
    local function monsterNearPlayer(monster)
        return m:isActive and m:manhattanDistanceFromPlayer() <= range 
    end
    return self.table:exists(monsterNearPlayer)
end

I don’t see that as a big win. Six lines vs six. We’d do better to remove the feature envy and send a message to the monster inquiring whether he was active near the player, like this:

function Monsters:hasMonsterNearPlayer(range)
    for i,m in ipairs(self.table) do
        if m:isActiveNearPlayer() then return true end
    end
    return false
end

To convert that one, we’d get:

function Monsters:hasMonsterNearPlayer(range)
    local function monsterNear(m) return m:isActiveNearPlayer() end
    return self.table:exists(monsterNear)
end

Not exactly compelling, is it?

function Monsters:startAllTimers()
    for i,m in ipairs(self.table) do
        m:startAllTimers()
    end
end

Well, if we had apply, we could have:

function Monsters:startAllTimers()
    local function start(m) m:startAllTimers() end
    self.table:apply(start)
end

Whee, we’d save a line.

Part of what is making these changes less than thrilling is the long form we need to write for a function. I’ve thought of no way in Lua to improve that.

Let’s look at a few more examples.

function Dungeon:connectRooms(rooms)
    for i,r in ipairs(rooms) do
        if i > 1 then
            r:connect(self, rooms[i-1])
        end
    end
end

I don’t think our existing functions could do this. With the index passed in, we could. But would it be shorter or more clear? I have my doubts.

function Dungeon:nearestContentsQueries(tile)
    for i,tile in ipairs(self:neighbors(tile)) do
        tile:doQueries()
    end
end

Here again, we want apply. But even if we had it, we’d have to write a function out that said tile:doQueries().

We could imagine an fp function applyMessage. Then we could write, here,

self:neighbors(tile):applyMessage("doQueries")

We’d have to quote the message name. This isn’t notably better.

function Tile:doQueries()
    for k,obj in pairs(self:getContents()) do
        if obj.query then
            obj:query()
        end
    end
end

Well … we could get it down to this:

function Tile:doQueries()
    local f = function(o) if o.query then o:query() end
    self:getContents():apply(f)
end

Not what I’d call a big win.

    local accepted = false
    local residents = 0
    for k,residentEntity in pairs(self:getContents()) do
        residents = residents + 1
        ta = TileArbiter(residentEntity,self, enteringEntity, enteringTile)
        acceptedTile = ta:moveTo(self)
        accepted = accepted or acceptedTile==self
    end

This amounts to a reduce with two outputs, which we can’t directly support. It’s not going to get much if any better, even if we could do it.

function Tile:getSurroundingInfo()
    local byte = 0
    local ck = { vec2(1,1),vec2(0,1),vec2(-1,1), vec2(1,0),vec2(-1,0), vec2(1,-1),vec2(0,-1),vec2(-1,-1) }
    for i,p in ipairs(ck) do
        byte = byte<<1
        local pos = self:pos() + p
        local tile = self.runner:getTile(pos)
        if tile:isFloor() then
            byte = byte|1
        end
    end
    return byte
end

This is some kind of bizarre reduce. I don’t think it gets more clear with reduce, not to say that it’s terribly clear now. (It creates a byte with 1 bits where the tile in a given position is floor.) I don’t know what it’s for and don’t plan to look.

function Tile:illuminateLine(dx,dy)
    local max = IlluminationLimit
    local pts = Bresenham:drawLine(0,0,dx,dy)
    for i,offset in ipairs(pts) do
        local pos = self:pos() + offset
        local d = self:pos():dist(pos)
        if d > max then break end
        local tile = self.runner:getTile(pos)
        tile:setVisible(d)
        if tile.kind == TileWall then break end
    end
end

This has a break, perhaps the only one I’ve ever used. Even if we had apply it wouldn’t deal with break. Of course we could provide one that did … but would it be somehow better here? I think not.

I Think My Point Has Been Made

I looked at many more for loops than I showed here. I found none, zero, not any where I think the use of my lovely fp functions would make the code more clear, less prone to error, or in any way better.

Now, it could be that the way I program is such that I don’t gain much benefit from functions like filter, map, and reduce. I do get a little benefit from exists and a tiny bit from every but even there, they’d be just as well implemented on a collection class. An apply might be nice but since function definitions are wordy, I don’t feel a compelling need for apply either.

It’s also possible that without a shorter notation for an anonymous function, Lua doesn’t benefit as much as some other language might.

I am, however, beginning to suspect that those functions aren’t as useful as they are interesting.

Still, they were fun to write, and now that I have them, I’ll stay alert for situations where they’ll really be helpful.

And if you have examples where they’re useful to you, or see places in the code above where I’m missing a chance for improved clarity or quality, do please let me know.

Tomorrow … we’ll do something else. I hope you’ll stop by.