Array/iterator and looping behaviour in kak script

Please consider turning it into a proper blog article if you have the will and the time to do so.

The truth is that I don’t like to spend too much time in front of a computer. I don’t even have a website :sweat_smile: But I’ll take your suggestion into consideration.

Do we already have all the right editing primitives / keys we need at our disposal or do we miss some core ones that we simplify the job?

I’d really appreciate if Kakoune could treat object selections as first class citizens. Currently, there are some issues I dislike. First, <a-a> and <a-i> are unergonomical. When I started using Kakoune, I simply couldn’t do some editing as fast as in Vim just because my brain had to stop for a moment to think about the key combinations to select a word or a paragraph. I tried hard to get used to them but without luck. It was until I remapped these keys to something else. It had a big positive impact on my productivity.

Then, it would be very useful (for me, at least) if we could use object selections in combination with s, S, <a-k> and <a-K>. If I want to select all the words of a line, why do I need to do xs\w+ if Kakoune already knows what a word is? And sometimes Kakoune knows it even better than me, because it can read the extra_words_chars option. This is truth specially if I have to deal with nested blocks, since selecting them can’t be done easily and generally with regular expresssions. Why can’t I simply execute s<a-a>{ to select a brackets block?

Now to the folds!

Folding in Kakoune

There’s this hidden notion of “accumulated value”.

This notion of accumulated value is precisely what a monoid express. For instance, we can fold a list of integers because integers act as monoids respect to the addition and multiplication operations. In the same vein, booleans are monoids respect to the and and or operations, and strings are monoids respect to the concatenation operation.

The general idea is: if we know how to merge two values of the same type, we can merge all of them.

We already know that selections are monoids respect to the operations on marks, so let’s see what we can do with them. First, consider the following code:

declare-user-mode reduction
map global reduction u ': reduce u<ret>' -docstring 'take the union'
map global reduction < ': reduce <lt><ret>' -docstring 'take the first'
map global reduction > ': reduce <gt><ret>' -docstring 'take the last'
map global reduction + ': reduce +<ret>' -docstring 'take the longest'
map global reduction <minus> ': reduce -<ret>' -docstring 'take the shortest'
# Define a `<a-r>` key to apply reductions
map global normal <a-r> ': enter-user-mode reduction<ret>' -docstring 'reduce selections'

# The `reduce` command is the equivalent of Haskell's `foldr1`. It takes as
# argument the operation to be used as the monoid operation.
define-command -hidden -params 1 reduce %{
    # Save the head to the mark register. It's going to be the base case of the recursion
    execute-keys -draft -save-regs /"|@ <space>Z
    # Get the tail of the list
    execute-keys <a-space>
    # Call the recursive function applied to the tail
    reduce-with-base-case %arg{1}
    # Restore the accumulated selection from the marks register
    execute-keys z
}

# The `reduce-with-base-case` is the equivalent of Haskell's `foldr`
define-command -hidden -params 1 reduce-with-base-case %{
    try %{
        # Merge the head with the value accumulated in the marks register
        # using the operation provided as argument.
        execute-keys -draft -save-regs /"|@ %sh{ printf '<space><a-z>%sZ' "$1" }
        # Get the tail of the list
        execute-keys <a-space>
        # Recurse
        reduce-with-base-case %arg{1}
    }
}

Let’s see what are the effects of using this new <a-r> key. Consider the following scenario, a quote from Mia Couto:

O [ar] é [uma] [pele], [feita] de [poros] [por] onde [escoa] [a] luz, [gota] [por] [gota], como um suor [solar].

If I then press <a-r>u, the selections are reduced to the union of them all:

O [ar é uma pele, feita de poros por onde escoa a luz, gota por gota, como um suor solar].

This is a generalisation of the <a-_> key that also works on non-contiguous selections.

But, if I choose <a-r>+, the selections are reduced to the longest of the them:

O ar é uma pele, feita de poros por onde escoa a luz, gota por gota, como um suor [solar].

Since reduce is implemented as a right fold (that is, from right to left), it takes the rightmost one from the longest selections.

I can also choose to reduce to the shortest one (<a-r>-):

O ar é uma pele, feita de poros por onde escoa [a] luz, gota por gota, como um suor solar.

As you can see, we already can do some interesting folding operations, but we still can’t sum a list of numbers like in your example. So, let’s see what is still missing.

Folding to a scalar

Selections wrap text but are not text themselves. Currently, marks only work at the selection level: it can’t unwrap the text that’s inside it. It’s like having a list of Maybe and only being able to operate on the Maybes but not on the values they hold, as in the following Elm code:

[Just 3, Just 21, Just 4]
    |> foldr (\maybe accumulator -> if maybe == Nothing then Nothing else accumulator) (Just 17) 

The above code reduces the list to a Just 17. If I can’t inspect what is wrapped inside the Maybe monad, that’s pretty much what I can get. I can’t, for example, sum the integers the Maybes hold.

It turns out we need to define another operation for marks to be able to express more things with it. By doing so, we extend the ways by which selections can be monoids.

So, let’s imagine we have a pipe (|) operation for marks. The exact API should be thought more carefully, but for the moment suppose this operation allows us to call an external tool passing the value inside the ^ register as $accumulated and the value inside the selection as $selection in the command line; then, the union (as in <a-z>u) of the mark and the selection is replaced (c) by whatever this external tool prints to the stdout. That is, if I have the text:

O ar é uma [pele]

And the ^ register contains the selection for [ar], executing <a-z>| external-tool $accumulated $selection, the text

ar é uma pele

will be replaced by whatever the external-tool prints to the stdout. Say it prints mar, quando quebra na praia, é bonito. Then, we end up with:

O [mar, quando quebra na praia, é bonito]

Now, it’s enough to declare:

map global reduction | ': reduce |' -docstring 'reduce using an external tool'

To keep the example of the lists of numbers we were working with in this topic, having:

[12], [16], [18], [22], [28], [30]

If we execute <a-r>| echo "$accumulated + $selection" | bc, we then get:

[126]

As we wish.

Remember that our reduce command goes from right to left. For this kind of operation, it’s probably more intuitive accumulate from left to right. Fortunatelly, the change is straightforward:

define-command -hidden -params 1 reduce %{
    execute-keys ) # That's all we need to convert our `foldr` to a `foldl`
    execute-keys -draft -save-regs /"|@ <space>Z
    execute-keys <a-space>
    reduce-with-base-case %arg{1}
    execute-keys z
}

This new key could also be used for operations other than on numbers. Say I have a list of words I want to check against a blacklist provided by an external tool and keep only those allowed, deleting the rest. I could select them all and execute <a-r>| external-tool $accumulated $selection, provided the external tool prints the concatenated list of good words to the stdout.

Let’s take a moment to appreciate the elegance of Kakoune. A small addition to a seemingly unrelated feature (operations on marks) and suddenly we have all we were asking for!

Folding to another list

The fold is indeed a powerful function. As you said, many functions can be expressed in terms of folds, like map, filter, length, all… But it can do it only because the accumulated value can be anything (even other data structures), not just scalars. Here’s how we can reverse a list in Haskell:

reverse = foldl (\accumulated x -> x : accumulated) []

Note that the accumulated value is a list here, not a scalar. We can say reduce and fold are not good words to express this kind of operation anymore (fold is not a good word in any case by the way). So let’s call it aggregate by now, just to make it clear we are working on a slightly variation of the preceding operation.

How can we get at least some of this power in Kakoune? How can we make folds that produce another list of selections as the accumulated value? It’s hard to say. But maybe I can give some insight into how to build a somewhat useful aggregate command.

To do so, now we must consider that the accumulated value stored in the marks register is a list of selections, not just a single one. What we need is an aggregate command that, at each selection:

  • takes the head of the list stored in the marks register and the current selection and calls an external tool passing these two values as arguments: external-tool $head-of-marks $selection;
  • this external tool is expected to print to the stdout one of the following values: u, a, <, >, + or -, each one corresponding to an operation on marks (union, append, take leftmost, take rightmost, take longest, take shortest);
  • the command then merges the head of the marks and the current selection according to the operation indicated by the output of the external tool;
  • the result of such a merge then replaces the head of the marks.

Better seeing it in action. Consider the following scenario, quoted from Machado de Assis:

Palavra [puxa] [palavra], uma [ideia] traz outra, e [assim] se faz um [livro], um [governo], ou uma [revolução].

That is, I have the following list of selections: [puxa], [palavra], [ideia], [assim], [livro], [governo], [revolução].

  • our aggregate command starts by putting the head of the selections in the marks register:

    • marks: [puxa];
    • selections: [palavra], [ideia], [assim], [livro], [governo], [revolução];
  • our aggregate command now calls external-tool passing [puxa] and [palavra] as arguments: external-tool puxa palavra;

  • say it returns a, meaning it wants these selections to be merged;

  • we now have:

    • marks: [puxa], [palavra];
    • selections: [ideia], [assim], [livro], [governo], [revolução];
  • next turn: external-tool palavra ideia;

  • it gives us u;

  • now we have:

    • marks: [puxa], [palavra, uma ideia]
    • selections: [assim], [livro], [governo], [revolução];
  • then: external-tool "palavra, uma ideia" assim;

  • we receive -;

  • what results in:

    • marks: [puxa], [assim];
    • selections: [livro], [governo], [revolução];

And so on.

We could end up with something like this:

Palavra [puxa] palavra, uma ideia traz outra, e [assim] se faz um [livro, um governo, ou uma revolução].

Conclusions

what are your thoughts about reduction/folding in Kakoune in general

Some examples here seem to be too much complicated to be used when editing. This makes me think if it would be worth of investing in such a concept. On the other hand, we must take into account that the same language used to live editing text in Kakoune is also used to script it, and perhaps for scripting it could allows some interesting extra powers.

In any case, I think it was a fun conceptual exercise to do :smile:

6 Likes