#!/usr/bin/luajit -- Pipelines for REPL interaction in Lua. --[[ > require "pipecrimes" > =dd-{"Mina","es","linda"} /call("gsub", "[aeiou]", "i") /call("rep", 2) /call "lower" {"minimini", "isis", "lindilindi"} > ps={{x=5, y=7}, {x=5, y=4}, {x=3, y=4}} -- define some points for examples > =dd-ps/get"x" -- project onto the X-axis {5, 5, 3} > =dd-ps/where {y=4} -- select by attribute values {[2] = {x=5, y=4}, [3] = {x=3, y=4}} > =dd-ps/where {y=4, x=2} -- multiple attribute values (no match this time) {} > =dd-ps/group "x" -- group by attribute value {[5] = {{x=5, y=7}, {x=5, y=4}}, [3] = {[3] = {x=3, y=4}}} > =dd-ps/group "x"/choose -- select an item from each group {[5] = {x=5, y=7}, [3] = {x=3, y=4}} > =dd-ps/group "x"/map(get"y") -- project out Y-attribute of each group {[5] = {7, 4}, [3] = {[3] = 4}} > =dd-ps/group "x"/map(get"y")/max -- select max from each group {[5] = 7, [3] = 4} > =dd-ps/group "x"/map(get"y")/max/min -- find smallest group maximum 4 > =dd-ps/group "x"/map(get"y")/max/sum -- sum of group maxima 11 > =dd-ps/where(function(p) return 9 < p.x+p.y end) -- select by arbitrary func {{x=5, y=7}} > function bytes(s) return {s:byte(1, #s)} end > =dd-{"hey", "you"}/map(bytes) -- map arbitrary function {{104, 101, 121}, {121, 111, 117}} > =dd-{"hey", "you"}/map(bytes)/flat/map(string.char)/freq -- char frequencies {o=1, h=1, u=1, y=2, e=1} > =dd-{io.open("pipecrimes.lua"):lines()}/iterate/count -- unpack Lua iterators 350 > =dd-flines("pipecrimes.lua")/count -- shorthand for file reading: count line 350 > =dd-flines("pipecrimes.lua")/freq/count -- count unique lines 261 > =dd-{("this is a string"):gmatch("%w+")}/iterate -- unpack gmatch iterator {"this", "is", "a", "string"} --]] -- -- -- In https://news.ycombinator.com/item?id=44980126 -- btown posted the following astonishing line of -- Python: -- x = Model.objects.all() >> by_pk >> call(dict.values) -- >> to_list >> get('name') >> _map(str.title) >> -- log_debug >> to_json -- I want this in Lua, badly. Lua has operator -- overloading and closures but not a >> operator. -- To be able to handle a leftmost item that's just a -- list, I need a left-associative operator, which is -- all of them except .. and ^. That is, [-+*/%] -- and the comparison operators, although -- comparison operators are inapplicable to -- mixed types. These can all be overloaded -- https://www.lua.org/manual/5.1/manual.html#2.8 and -- there are no other candidate operators in -- https://luajit.org/extensions.html. -- So I think probably the least bad choice is /. -- We’ll also let you call it normally. local function tidy(f) local function call(this, ...) return f(...) end return setmetatable({}, { __div = f, __call = call }) end -- Transform each value in a table. -- Because f may return nil here, we generally iterate over -- pairs() rather than ipairs(). -- Previously I was passing i to f as well, but that was dissatisfying -- for /map(string.char), which unfortunately paid attention to the -- additional argument. function map(f) return tidy(function(xs) local ys = {} for i, xi in pairs(xs) do ys[i] = f(xi) end return ys end) end -- cf. https://stackoverflow.com/a/58795138 local function callable(t) if type(t) == 'function' then return true end local meta = getmetatable(t) return type(meta) == "table" and type(meta.__call) == "function" end -- Filter to values satisfying a predicate function. function where(f) if type(f) == "table" and not callable(f) then -- Shorthand syntax /where {x=53} local t = f f = function(xi) for k, v in pairs(t) do if xi[k] ~= v then return false end end return true end end return tidy(function(xs) local rv = {} for i, xi in pairs(xs) do if f(xi, i) then rv[i] = xi end end return rv end) end local dump_table -- for mutual recursion -- Produce a readable string representation of Lua -- objects. local function dump(o, out) local t = type(o) if t == "table" then dump_table(o, out) elseif t == "string" then out(("%q"):format(o)) else out(("%s"):format(o)) end end -- Table of Lua reserved words that you can’t just -- throw into a table constructor expression -- willy-nilly. See -- https://www.lua.org/manual/5.1/manual.html#2.1 local reserved = {} for word in ([[and break do else elseif end false for function if in local nil not or repeat return then true until while]]):gmatch("%a+") do reserved[word] = true end function dump_table(o, out) out("{") local index = 1 for k, v in pairs(o) do if index ~= 1 then out(', ') end if index ~= k then index = nil if type(k) == "string" and k:match("^%a%w*$") and not reserved[k] then out(k) out("=") else out("[") dump(k, out) out("] = ") end end dump(v, out) if index ~= nil then index = index + 1 end end out("}") end local function dumper(o) local ss = {} dump(o, function(s) table.insert(ss, s) end) return table.concat(ss) end -- We want to be able to put the pretty-printer at the -- *beginning* of each line, so it needs a -- lower-precedence operator than /. Like -. dd = setmetatable({dumper=dumper}, {__sub = function(self, o) return dumper(o) end}) -- Obtain a particular attribute from each object. function get(k) return map(function(x) return x[k] end) end -- Call a given :method on each object. -- XXX maybe map:method(arg1, arg2)? function call(k, ...) local args = {...} return map(function(x) return x[k](x, unpack(args)) end) end -- List the non-nil keys. keys = tidy(function(xs) local rv = {} for k, _ in pairs(xs) do table.insert(rv, k) end return rv end) -- List the non-nil values. values = tidy(function(xs) local rv = {} for _, v in pairs(xs) do table.insert(rv, v) end return rv end) flat = tidy(function(xs) local rv = {} for _, v in pairs(xs) do for _, vi in pairs(v) do table.insert(rv, vi) end end return rv end) -- Numerical aggregate functions like sum, max, and min implicitly map -- when applied to a nested collection. local function sometable(xs) for _, v in pairs(xs) do if type(v) == "table" then return true end end return false end sum = tidy(function(xs) if sometable(xs) then return map(sum)(xs) end local t = 0 for _, v in pairs(xs) do t = t + v end return t end) max = tidy(function(xs) if sometable(xs) then return map(max)(xs) end local first, rv = true for _, v in pairs(xs) do if first or v > rv then rv = v first = false end end return rv end) min = tidy(function(xs) if sometable(xs) then return map(min)(xs) end local first, rv = true for _, v in pairs(xs) do if first or v < rv then rv = v first = false end end return rv end) -- Count is an aggregate function that doesn’t work that way, where -- you have to explicitly request map(count). count = tidy(function(xs) local n = 0 for _, _ in pairs(xs) do n = n + 1 end return n end) -- `choose` picks one item from each group. Previously I used -- `get(1)` but preserving keys in `group` broke it. choose = tidy(function(xs) local ys = {} for k, v in pairs(xs) do local _, vi = next(v) ys[k] = vi end return ys end) -- XXX I feel like this function doesn’t pull its weight. Isn’t -- /freq/keys just as good? uniq = tidy(function(xs) local seen, rv = {}, {} for k, v in pairs(xs) do if not seen[v] then seen[v] = true rv[k] = v end end return rv end) -- any, all, head, XXX freq = tidy(function(xs) local ys = {} for _, v in pairs(xs) do ys[v] = 1 + (ys[v] or 0) end return ys end) -- Add a level of nesting using an attribute or function to define -- groups. group = function(kf) if type(kf) == "string" then -- Shorthand /group "somefield" local k = kf kf = function(v) return v[k] end end return tidy(function(xs) local rv = {} for ok, v in pairs(xs) do local k = kf(v) if rv[k] == nil then rv[k] = {} end rv[k][ok] = v -- preserve original keys end return rv end) end -- Convert a regular Lua iterator, as in `for x in y`, into a table. -- This assumes that the iterator returns only one item on each -- iteration, and is packaged into a table. iterate = tidy(function(iterator) local rv = {} for v in unpack(iterator) do table.insert(rv, v) end return rv end) -- Common special case of iterator conversion. function flines(fname) local f = io.open(fname) local rv = iterate({f:lines()}) f:close() return rv end -- quick smoke test when run from the command line if ({...})[1] ~= "pipecrimes" then print(dd-{5, 6, 7} / map(function(n) return n+1 end) / where(function (n) return n > 7 end)) print(dd-{71, 53} / map(function(n) return 3*n end)) local tr = {"we", "are", "the", "tramplings"} print(dd-tr /call"reverse") print(dd-tr /call ("find", "e")) print(dd-tr /group(string.len) /max) print(dd-tr /group(string.len) /min) print(dd-tr /group(string.len) /count) print(dd-tr /group(string.len) /map(count)) print(dd-tr /group(string.len) /map(count) /max) print(dd-tr /group(string.len) /map(count) /uniq) local d = {two = 2, seven = 7} print(dd-d /keys) print(dd-d /values) print(dd-d /keys /count) local ps = {{x=57, y=22}, {x=99, y=5}} print(dd-ps /get "x") print(dd-ps /flat) print(dd-{...}) end