Hi Bram and mattn, 2016/7/3 Sun 6:27:36 UTC+9 Bram Moolenaar wrote: > Ken Takata wrote: > > > 2016/5/27 Fri 22:38:34 UTC+9 Ken Takata wrote: > > > Hi mattn, > > > > > > 2016/4/18 Mon 0:30:19 UTC+9 mattn wrote: > > > > Hi, Bram and list. > > > > > > > > Updated lambda patch to be applied for latest code. > > > > > > > > https://gist.github.com/mattn/5bc8ded21e1033c9c0ea8cd5ecbbce11 > > > > > > > > This include examples for timer on help file. I'm thinking lambda() > > > > have cooperative to work with job/timer/channel. > > > > > > > > So I hope to add this into vim8. How do you think? > > > > > > > > Thanks to k-takata, haya14busa, and all of members on vim-jp. > > > > > > I have tested the lambda patch with the latest Vim and I found two > > > problems. > > > > > > 1. garbagecollect_for_testing() was renamed. > > > 2. Test_lambda_with_timer() fails on Cygwin. It seems that we need > > > margins > > > as we do in test_timers.vim. > > > > > > Please check attached patch. > > > > > > I hope that the lambda patch will be merged in Vim 8.0. > > > This makes filter(), map() and sort() easy to use. > > > It also works nicely with job/channel/timer features. > > > > I have merged my patch into your patch, and also fixed that the function > > declaration was not sorted in alphabetical order. > > Please check the attached patch. > > I have mixed feelings about this implementation. This needs more > thoughts and discussion. > > > One of the problems with the current use of a string for an expression, > as it's passed to map(), filter(), etc., is that this is a string, which > requires taking care of quotes. > > Using lambda() for that doesn't have an advantage, it's only longer: > > :call map(mylist, '"> " . v:val . " <"') > :call map(mylist, lambda('"> " . v:val . " <"')) > > Perhaps we can define the lambda not as a function call, but as an > operator. Then it will be parsed differently and we don't need to put > the expression in quotes. We do want to be able to define it inline, as > a function argument. We do need something around it, so that it's clear > where the end is. Using {} would work, it's similar to a statement > block in most languages. > > We would also like to specify arguments. We could separate the > arguments from the statements with a "gives" symbol. That could be ->. > This avoid the strange use of v:val and v:key to pass values to the > expression, like map() does. > > call Func(arg, lambda{v -> return v * 3.12}) > > That way we can do this: > call map(mylist, lambda{v -> return "> " . v . " <"}) > > In most cases only one statement is needed, but having a few more should > be possible. Using | to separate statements hopefully works: > > call Func(arg, lambda{ v -> if v < 0 | return v * 3.12 | else | return > v * -3.12 | endif}) > > Something like that. If it gets too long it's better to just define a > function. > > > The implementation also takes items from the context, thus creating a > closure. I don't think that should be specific to a lambda, defining a > function inside another function should be able to do the same thing. > After all, a lambda is just a short way of defining a nameless function. > > In the implementation it seems the dictionary storing the function-local > variables is kept for a very long time. This relies on the garbage > collector. It's better to use reference counting to be able to free the > dictionary as soon as it's unused. > > Also, the lambda always keeps the function-local variable dict, even > when it's not actually used. That makes lambdas a expensive. > It would be better to explicitly state the lambda is using its context. > Then we can also do that with ":function", so that we are not forced to > use a lambda if we want a closure. > > However, I'm not sure we need this. It is possible to bind a function > to a dictionary, which can then contain anything that's to be kept > between calls. > > > Being able to pass a function to map(), sort(), etc. is good. However, > it looks like in the implementation only VAR_FUNC is supported, not > VAR_PARTIAL. It should be both.
It seems that it's better to divide the lambda patch into several patches. E.g.: 1. lambda expression 2. closure 3. enable to pass a function to filter() and map() I wrote a patch for #3. I slightly changed the specification so that a function has both key and value as arguments. I also added some tests for filter() and map(). Please check. Regards, Ken Takata -- -- You received this message from the "vim_dev" maillist. Do not top-post! Type your reply below the text you are replying to. For more information, visit http://www.vim.org/maillist.php --- You received this message because you are subscribed to the Google Groups "vim_dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to vim_dev+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.
# HG changeset patch # Parent e8cf557dd9c2d7d7c9820048ae37d614e2b8d312 diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -3521,11 +3521,12 @@ filewritable({file}) *filewritable() directory, and we can write to it, the result is 2. -filter({expr}, {string}) *filter()* - {expr} must be a |List| or a |Dictionary|. - For each item in {expr} evaluate {string} and when the result +filter({expr1}, {expr2}) *filter()* + {expr1} must be a |List| or a |Dictionary|. + For each item in {expr1} evaluate {expr2} and when the result is zero remove the item from the |List| or |Dictionary|. - Inside {string} |v:val| has the value of the current item. + {expr2} must be a |string| or |Funcref|. If it is a |string|, + inside {expr2} |v:val| has the value of the current item. For a |Dictionary| |v:key| has the key of the current item. Examples: > :call filter(mylist, 'v:val !~ "OLD"') @@ -3535,17 +3536,22 @@ filter({expr}, {string}) *filter()* :call filter(var, 0) < Removes all the items, thus clears the |List| or |Dictionary|. - Note that {string} is the result of expression and is then + Note that {expr2} is the result of expression and is then used as an expression again. Often it is good to use a |literal-string| to avoid having to double backslashes. + If {expr2} is a |Funcref|, the first argument has the key or + the index of the current item. The second argument has the + value of the current item. The function should return TRUE + if the item should be kept. + The operation is done in-place. If you want a |List| or |Dictionary| to remain unmodified make a copy first: > :let l = filter(copy(mylist), 'v:val =~ "KEEP"') -< Returns {expr}, the |List| or |Dictionary| that was filtered. - When an error is encountered while evaluating {string} no - further items in {expr} are processed. +< Returns {expr1}, the |List| or |Dictionary| that was filtered. + When an error is encountered while evaluating {expr2} no + further items in {expr1} are processed. finddir({name}[, {path}[, {count}]]) *finddir()* @@ -5036,29 +5042,35 @@ luaeval({expr}[, {expr}]) *luaeval() See |lua-luaeval| for more details. {only available when compiled with the |+lua| feature} -map({expr}, {string}) *map()* - {expr} must be a |List| or a |Dictionary|. - Replace each item in {expr} with the result of evaluating - {string}. - Inside {string} |v:val| has the value of the current item. +map({expr1}, {expr2}) *map()* + {expr1} must be a |List| or a |Dictionary|. + Replace each item in {expr1} with the result of evaluating + {expr2}. + {expr2} must be a |string| or |Funcref|. If it is a |string|, + inside {expr2} |v:val| has the value of the current item. For a |Dictionary| |v:key| has the key of the current item and for a |List| |v:key| has the index of the current item. Example: > :call map(mylist, '"> " . v:val . " <"') < This puts "> " before and " <" after each item in "mylist". - Note that {string} is the result of an expression and is then + Note that {expr2} is the result of an expression and is then used as an expression again. Often it is good to use a |literal-string| to avoid having to double backslashes. You still have to double ' quotes + If {expr2} is a |Funcref|, the first argument has the key or + the index of the current item. The second argument has the + value of the current item. The function should return a + value to replace the item with it. + The operation is done in-place. If you want a |List| or |Dictionary| to remain unmodified make a copy first: > :let tlist = map(copy(mylist), ' v:val . "\t"') -< Returns {expr}, the |List| or |Dictionary| that was filtered. - When an error is encountered while evaluating {string} no - further items in {expr} are processed. +< Returns {expr1}, the |List| or |Dictionary| that was filtered. + When an error is encountered while evaluating {expr2} no + further items in {expr1} are processed. maparg({name}[, {mode} [, {abbr} [, {dict}]]]) *maparg()* diff --git a/src/Makefile b/src/Makefile --- a/src/Makefile +++ b/src/Makefile @@ -2031,6 +2031,7 @@ test_arglist \ test_farsi \ test_feedkeys \ test_file_perm \ + test_filter_map \ test_fnamemodify \ test_glob2regpat \ test_goto \ diff --git a/src/eval.c b/src/eval.c --- a/src/eval.c +++ b/src/eval.c @@ -11852,7 +11852,7 @@ findfilendir( } static void filter_map(typval_T *argvars, typval_T *rettv, int map); -static int filter_map_one(typval_T *tv, char_u *expr, int map, int *remp); +static int filter_map_one(typval_T *tv, typval_T *expr, int map, int *remp); /* * Implementation of map() and filter(). @@ -11860,8 +11860,7 @@ static int filter_map_one(typval_T *tv, static void filter_map(typval_T *argvars, typval_T *rettv, int map) { - char_u buf[NUMBUFLEN]; - char_u *expr; + typval_T *expr; listitem_T *li, *nli; list_T *l = NULL; dictitem_T *di; @@ -11896,14 +11895,13 @@ filter_map(typval_T *argvars, typval_T * return; } - expr = get_tv_string_buf_chk(&argvars[1], buf); + expr = &argvars[1]; /* On type errors, the preceding call has already displayed an error * message. Avoid a misleading error message for an empty string that * was not passed as argument. */ - if (expr != NULL) + if (expr->v_type != VAR_UNKNOWN) { prepare_vimvar(VV_VAL, &save_val); - expr = skipwhite(expr); /* We reset "did_emsg" to be able to detect whether an error * occurred during evaluation of the expression. */ @@ -11975,21 +11973,44 @@ filter_map(typval_T *argvars, typval_T * } static int -filter_map_one(typval_T *tv, char_u *expr, int map, int *remp) +filter_map_one(typval_T *tv, typval_T *expr, int map, int *remp) { typval_T rettv; + typval_T argv[3]; char_u *s; int retval = FAIL; + int dummy; copy_tv(tv, &vimvars[VV_VAL].vv_tv); - s = expr; - if (eval1(&s, &rettv, TRUE) == FAIL) - goto theend; - if (*s != NUL) /* check for trailing chars after expr */ - { - EMSG2(_(e_invexpr2), s); - clear_tv(&rettv); - goto theend; + argv[0] = vimvars[VV_KEY].vv_tv; + argv[1] = vimvars[VV_VAL].vv_tv; + s = expr->vval.v_string; + if (expr->v_type == VAR_FUNC) + { + if (call_func(s, (int)STRLEN(s), + &rettv, 2, argv, 0L, 0L, &dummy, TRUE, NULL, NULL) == FALSE) + goto theend; + } + else if (expr->v_type == VAR_PARTIAL) + { + partial_T *partial = expr->vval.v_partial; + + s = partial->pt_name; + if (call_func(s, (int)STRLEN(s), + &rettv, 2, argv, 0L, 0L, &dummy, TRUE, partial, NULL) + == FALSE) + goto theend; + } + else + { + s = skipwhite(s); + if (eval1(&s, &rettv, TRUE) == FAIL) + goto theend; + if (*s != NUL) /* check for trailing chars after expr */ + { + EMSG2(_(e_invexpr2), s); + goto theend; + } } if (map) { diff --git a/src/testdir/test_alot.vim b/src/testdir/test_alot.vim --- a/src/testdir/test_alot.vim +++ b/src/testdir/test_alot.vim @@ -12,6 +12,7 @@ source test_expand_dllpath.vim source test_feedkeys.vim source test_fnamemodify.vim source test_file_perm.vim +source test_filter_map.vim source test_glob2regpat.vim source test_goto.vim source test_help_tagjump.vim diff --git a/src/testdir/test_filter_map.vim b/src/testdir/test_filter_map.vim new file mode 100644 --- /dev/null +++ b/src/testdir/test_filter_map.vim @@ -0,0 +1,77 @@ +" Test filter() and map() + +" list with expression string +func Test_filter_map_list_expr_string() + " filter() + call assert_equal([2, 3, 4], filter([1, 2, 3, 4], 'v:val > 1')) + call assert_equal([3, 4], filter([1, 2, 3, 4], 'v:key > 1')) + + " map() + call assert_equal([2, 4, 6, 8], map([1, 2, 3, 4], 'v:val * 2')) + call assert_equal([0, 2, 4, 6], map([1, 2, 3, 4], 'v:key * 2')) +endfunc + +" dict with expression string +func Test_filter_map_dict_expr_string() + let dict = {"foo": 1, "bar": 2, "baz": 3} + + " filter() + call assert_equal({"bar": 2, "baz": 3}, filter(copy(dict), 'v:val > 1')) + call assert_equal({"foo": 1, "baz": 3}, filter(copy(dict), 'v:key > "bar"')) + + " map() + call assert_equal({"foo": 2, "bar": 4, "baz": 6}, map(copy(dict), 'v:val * 2')) + call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), 'v:key[0]')) +endfunc + +" list with funcref +func Test_filter_map_list_expr_funcref() + " filter() + func! s:filter1(index, val) abort + return a:val > 1 + endfunc + call assert_equal([2, 3, 4], filter([1, 2, 3, 4], function('s:filter1'))) + + func! s:filter2(index, val) abort + return a:index > 1 + endfunc + call assert_equal([3, 4], filter([1, 2, 3, 4], function('s:filter2'))) + + " map() + func! s:filter3(index, val) abort + return a:val * 2 + endfunc + call assert_equal([2, 4, 6, 8], map([1, 2, 3, 4], function('s:filter3'))) + + func! s:filter4(index, val) abort + return a:index * 2 + endfunc + call assert_equal([0, 2, 4, 6], map([1, 2, 3, 4], function('s:filter4'))) +endfunc + +" dict with funcref +func Test_filter_map_dict_expr_funcref() + let dict = {"foo": 1, "bar": 2, "baz": 3} + + " filter() + func! s:filter1(key, val) abort + return a:val > 1 + endfunc + call assert_equal({"bar": 2, "baz": 3}, filter(copy(dict), function('s:filter1'))) + + func! s:filter2(key, val) abort + return a:key > "bar" + endfunc + call assert_equal({"foo": 1, "baz": 3}, filter(copy(dict), function('s:filter2'))) + + " map() + func! s:filter3(key, val) abort + return a:val * 2 + endfunc + call assert_equal({"foo": 2, "bar": 4, "baz": 6}, map(copy(dict), function('s:filter3'))) + + func! s:filter4(key, val) abort + return a:key[0] + endfunc + call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), function('s:filter4'))) +endfunc diff --git a/src/testdir/test_partial.vim b/src/testdir/test_partial.vim --- a/src/testdir/test_partial.vim +++ b/src/testdir/test_partial.vim @@ -14,6 +14,14 @@ func MySort(up, one, two) return a:one < a:two ? 1 : -1 endfunc +func MyMap(sub, index, val) + return a:val - a:sub +endfunc + +func MyFilter(threshold, index, val) + return a:val > a:threshold +endfunc + func Test_partial_args() let Cb = function('MyFunc', ["foo", "bar"]) @@ -36,6 +44,16 @@ func Test_partial_args() call assert_equal([1, 2, 3], sort([3, 1, 2], Sort)) let Sort = function('MySort', [0]) call assert_equal([3, 2, 1], sort([3, 1, 2], Sort)) + + let Map = function('MyMap', [2]) + call assert_equal([-1, 0, 1], map([1, 2, 3], Map)) + let Map = function('MyMap', [3]) + call assert_equal([-2, -1, 0], map([1, 2, 3], Map)) + + let Filter = function('MyFilter', [1]) + call assert_equal([2, 3], filter([1, 2, 3], Filter)) + let Filter = function('MyFilter', [2]) + call assert_equal([3], filter([1, 2, 3], Filter)) endfunc func MyDictFunc(arg1, arg2) dict @@ -60,6 +78,9 @@ func Test_partial_dict() call assert_equal("hello/xxx/yyy", Cb("xxx", "yyy")) call assert_fails('Cb("fff")', 'E492:') + let Cb = function('MyDictFunc', dict) + call assert_equal({"foo": "hello/foo/1", "bar": "hello/bar/2"}, map({"foo": 1, "bar": 2}, Cb)) + let dict = {"tr": function('tr', ['hello', 'h', 'H'])} call assert_equal("Hello", dict.tr()) endfunc