hslua / hslua

Haskell bindings to Lua, an embeddable scripting language.
https://hslua.org/
MIT License
130 stars 25 forks source link

Add a helper for getting the list last item #151

Closed CodeSandwich closed 1 month ago

CodeSandwich commented 1 month ago

The problem

I'm using hslua in Pandoc filters, where accessing the last item in a list is fairly common. The standard way to do it in Lua is to do list[#list], but it very quickly becomes unworkable when there are deeply nested lists. The code quickly ends up with monstrosities like blocks[#blocks].content[#blocks[#blocks].content] where the same identifiers are repeated multiple times, which hurts readability and increases the risk of introducing bugs. An alternative is to litter the code with intermediate variables, e.g. local content = blocks[#blocks].content and then content[#content].

The solution

The perfect solution would be for List to introduce a helper function List:last, which would return the last item or nil if the list is empty. The above example could be then expressed as blocks:last().content:last(), which is simple and elegant.

A more elastic approach would be to add List:get which would accept negative indexes, similarly to other languages but unlike Lua tables indexing. The above example could then be expressed as blocks:get(-1).content:get(-1).

tarleb commented 1 month ago

Something like local element, index = blocks:last() would make sense, I think. I would extend it a little to allow an optional parameter that is returned if the list is empty: A call like blocks:last().content would crash if blocks is empty, but blocks:last{}.content would just return nil. The example given above could then be made more robust by writing

(blocks.last{}.content or List{}):last()

Still not super pretty, but an improvement.

A common pattern is to use list:remove() to get the last element, but that also removes that element from the list.

I'm not sure about get. It's elegant and could provide the same default value functionality as last, but the overlap with normal table indexing makes me uneasy. OTOH, Python dictionary users would feel right at home.

CodeSandwich commented 1 month ago

The design you outlined resonates with me well.

I'm not very experienced with Lua , but to me the overlap between plain lua tables indexes and indexes in get doesn't seem problematic. Plain tables are a thin abstraction over mappings with the indexing operator [] completely bypassing it. OTOH List provides a very focused API with a stronger abstraction, which would become even stronger if you were allowed to NOT bypass it when accessing items. I don't think that anybody will be surprised if they do list[-1] = "foo" and list:get(-1) ~= "foo". As I said, I'm not very experienced and this API addition isn't really critical, it's purely a quality of life improvement.

The default value when the list is empty seems powerful, and adding it to just a single special case getter seems underwhelming. For example why shouldn't I be able to conveniently get the first item with a default? If you think that this feature is useful, then having it in a general purpose form, like in get, would make a lot of sense, even if get doesn't accept negative indexes and even if last is added.

tarleb commented 1 month ago

If you want to experiment a little while I make up my mind: place this in the init.lua file in your pandoc data directory (see pandoc -v).

local List = require 'pandoc.List'

local function get (t, k, default)
  local value = t[k]
  if value == nil then
    return default
  else
    return value
  end
end

local function last (t, default)
  return get(t, #t, default)
end

local list_types = {List, getmetatable(pandoc.Blocks{}), getmetatable(pandoc.Inlines{})}

for _, lt in ipairs(list_types) do
  lt.get = get
  lt.last = last
end

Now you should be able to use :get and :last on lists.

tarleb commented 1 month ago

I named the new function :at (as in JavaScript) instead of :get to make it clearer that it only works with indices. The next nightly build of pandoc should already come with the new function.

CodeSandwich commented 1 month ago

Thanks, it's awesome!