plenary.nvim icon indicating copy to clipboard operation
plenary.nvim copied to clipboard

Add curry and pipe functions

Open smolck opened this issue 5 years ago • 5 comments

Example usage:

-- Just to make examples cleaner
local function dump(x) print(vim.inspect(x)) end

-- curry/curry2
dump(vim.tbl_map(f.curry(tostring), {1, 2, 3, 4})) -- '{ "1", "2", "3", "4"}'

local function add(x, y) return x + y end
dump(vim.tbl_map(f.curry2(add)(5), {1, 2, 3, 4})) -- '{ 6, 7, 8, 9 }'

-- pipe
f.pipe(
  {1, 2, 3, 4, 5},
  f.curry2(vim.tbl_filter)(function(x) return x % 2 == 0 end),
  f.curry2(vim.tbl_map)(function(x) return tostring(x * 10) end),
  vim.inspect,
  print
) -- '{ "20", "40"}'

Here's a quick-and-dirty benchmark for two versions of the pipe function (the recursive one being the one I've pushed for now) vs. normal code. On my machine, pipe_iterative appears to be slightly faster than pipe_recursive, and both of the pipe functions are about a second slower than the normal code example. I do think the pipe version is more readable and easier to write though, so it's being slightly slower is mostly made up for by the readability in many cases I'd think.

local function pipe_recursive(x, ...)
  if ... ~= nil then
    return pipe_recursive(f.first(...)(x), select(2, ...))
  else
    return x
  end
end

local function pipe_iterative(x, ...)
  local ret = x
  for _, f in ipairs({...}) do
    ret = f(ret)
  end

  return ret
end

local bench = require'plenary.profile'.benchmark
local function piped_iter()
  pipe_iterative(
    {1, 2, 3, 4, 5},
    f.curry2(vim.tbl_filter)(function(x) return x % 2 == 0 end),
    f.curry2(vim.tbl_map)(function(x) return tostring(x * 10) end),
    vim.inspect
  )
end

local function piped_recursive()
  pipe_recursive(
    {1, 2, 3, 4, 5},
    f.curry2(vim.tbl_filter)(function(x) return x % 2 == 0 end),
    f.curry2(vim.tbl_map)(function(x) return tostring(x * 10) end),
    vim.inspect
  )
end

local function normal()
  vim.inspect(
    vim.tbl_map(
      function(x) return tostring(x * 10) end,
      vim.tbl_filter(
        function(x) return x % 2 == 0 end,
        {1, 2, 3, 4, 5})))
end

print('Iter', bench(1E6, piped_iter))
print('Recursive', bench(1E6, piped_recursive))
print('Normal', bench(1E6, normal))

Let me know which of the pipe implementations you want me to use in this PR (the iterative or recursive one or another one if you have something better/faster). It's quite possible I'm missing some optimizations, so I wouldn't be that surprised if these could be made faster.

smolck avatar Aug 21 '20 20:08 smolck

Do you want to try and add some tests for these as we go? I can tell you how to run & add tests if you're interested.

tjdevries avatar Aug 25 '20 14:08 tjdevries

Sure, sounds good!

smolck avatar Aug 25 '20 14:08 smolck

You available to stop by the stream tonight? It'd be a lot easier to explain there -- otherwise I can write it out at some point.

tjdevries avatar Aug 27 '20 13:08 tjdevries

I'm interested in picking this up if you're still interested in explaining how to add and run tests. I'm comfortable writing tests in general (and for functional programming staples in particular), but I'm unfamiliar with the relevant tools in Lua (and/or in neovim, insofar as the plugin relationship structures the test process.)

timtro avatar Nov 04 '21 15:11 timtro

Taking the tests you've written here as exemplary, it seems fairly straightforward. And I've found the 'test' make target, so running the tests is simple. A bit of Googling and grepping also tells be you're using a testing framework called Busted? So that gives me documentation.

I noticed there are no tests for functional.lua. So I take it I should

  • Create tests/functional_spec.lua,
  • with local f = require "plenary.functional" at the top.
  • Test the functions in f (starting with curry and pipe as in the PR) following your example, consulting the Busted documentation where needed).
  • Format everything according to the stylua.toml you provided.

If I get carried away, should I write tests for the other functions in the functional module?

Am I wrong about anything? Is there anything else I should know/do?

timtro avatar Nov 04 '21 19:11 timtro